/* recipe-components.jsx — MyFoodCraving production recipe page components.
   Exposes RecipeHero, StepCard, IngredientsCard, UtensilsCard,
   CookingPlayer, and HealthMarquee to window globals for use by recipe.html. */

(function () {
  const { useState, useEffect, useMemo, useRef } = React;

  // ─── helpers ─────────────────────────────────────────────────────────────────
  function fmtTime(secs) {
    secs = Math.max(0, Math.round(secs));
    const m = Math.floor(secs / 60);
    const s = Math.floor(secs % 60);
    return `${m}:${String(s).padStart(2, "0")}`;
  }
  function fmtMin(secs) { return `${Math.round(secs / 60)}m`; }
  function scaleAmt(ing, serv, base) {
    const raw = String(ing.amt ?? ing.amount ?? "");
    const m = raw.match(/^([\d.]+)(.*)$/);
    if (!m) return raw;
    const v = parseFloat(m[1]) * (serv / base);
    const out = Number.isInteger(v) ? v : Math.round(v * 10) / 10;
    const unit = ing.unit ? ` ${ing.unit}` : "";
    return `${out}${m[2]}${unit}`.trim();
  }
  function ingLink(ing) {
    const base = (window.MFC_BASE || "");
    if (ing.id) return `${base}ingredient.html?id=${encodeURIComponent(ing.id)}`;
    return `${base}ingredient.html?name=${encodeURIComponent(ing.name)}`;
  }
  function utLink(u) {
    const base = (window.MFC_BASE || "");
    if (u.id) return `${base}utensil.html?id=${encodeURIComponent(u.id)}`;
    return `${base}utensil.html?name=${encodeURIComponent(u.name)}`;
  }

  // ─── simple food thumbnail ────────────────────────────────────────────────
  function FoodThumb({ emoji, name, essential, size = 42, photo }) {
    const TINTS = [
      "#FCE9D6", "#F4EBD0", "#E6F0DA", "#D8EBE4", "#DCEAF2",
      "#E8DCEF", "#F5DDE3", "#FFF0C9", "#D6ECD6", "#F0E5D8",
    ];
    const hash = (name || "").split("").reduce((a, c) => (a * 31 + c.charCodeAt(0)) | 0, 0);
    const bg = TINTS[Math.abs(hash) % TINTS.length];
    const cls = "r-thumb" + (essential ? " essential" : "") + (photo ? " has-photo" : "");
    return (
      <div
        className={cls}
        style={{ width: size, height: size, background: photo ? "transparent" : bg }}
        aria-hidden="true"
      >
        {photo ? (
          <img
            src={photo}
            alt=""
            loading="lazy"
            style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: "inherit" }}
            onError={(e) => {
              const parent = e.target.parentNode;
              parent.classList.remove("has-photo");
              parent.style.background = bg;
              e.target.replaceWith(Object.assign(document.createElement("span"), {
                textContent: emoji || "•",
                style: `font-size:${size * 0.46}px;line-height:1;`,
              }));
            }}
          />
        ) : (
          <span style={{ fontSize: size * 0.46, position: "relative", zIndex: 1, lineHeight: 1 }}>
            {emoji || "•"}
          </span>
        )}
      </div>
    );
  }

  // ─── NUTRITION GLANCE CARD ────────────────────────────────────────────────
  function NutritionGlanceCard({ recipe, nutrition, servings, onJumpToNutrition, collapsible, defaultOpen = true }) {
    const [open, setOpen] = useState(defaultOpen);
    const n = nutrition || recipe.nutrition || {};
    const cal = n.calories ?? recipe.calories;
    const pG = n.proteinG ?? n.protein ?? recipe.proteinG;
    const cG = n.carbsG ?? n.carbs ?? recipe.carbsG;
    const fG = n.fatG ?? n.fat ?? recipe.fatG;
    const totalMin = recipe.totalMinutes || recipe.minutes || 0;
    const nutriTags = recipe.nutriTags || recipe.tags || [];

    const pCal = (pG || 0) * 4;
    const cCal = (cG || 0) * 4;
    const fCal = (fG || 0) * 9;
    const totalCal = pCal + cCal + fCal || 1;
    const pPct = Math.min(100, Math.round(pCal / totalCal * 100));
    const cPct = Math.min(100, Math.round(cCal / totalCal * 100));
    const fPct = Math.min(100, Math.round(fCal / totalCal * 100));

    const RING_SIZE = 56;
    const R = 21;
    const CIRC = 2 * Math.PI * R;
    function ring(pct, color, delay) {
      const dash = CIRC * Math.max(0.02, pct / 100);
      return { dash, gap: CIRC - dash, color, delay };
    }

    function handleJumpNutrition() {
      if (onJumpToNutrition) { onJumpToNutrition(); return; }
      document.getElementById("full-nutrition")?.scrollIntoView({ behavior: "smooth", block: "start" });
    }

    const macros = [
      { label: "protein", val: pG, unit: "g", pct: pPct, color: "var(--matcha)" },
      { label: "carbs", val: cG, unit: "g", pct: cPct, color: "var(--orange)" },
      { label: "fat", val: fG, unit: "g", pct: fPct, color: "var(--butter)" },
    ];

    const showBody = !collapsible || open;
    return (
      <aside className={"r-nutri-glance" + (collapsible ? " collapsible" : "") + (open ? " open" : "")}>
        <div className="head" onClick={collapsible ? () => setOpen((o) => !o) : undefined}>
          <h2>nutrition{collapsible && !open && cal != null ? <span className="kcal-sum"> · {cal} kcal</span> : null}</h2>
          <span className="card-eyebrow">{servings > 1 ? `for ${servings} servings` : "per serving"}</span>
          {collapsible && <span className="r-chev">▾</span>}
        </div>

        {showBody && (<>

        {cal != null ? (
          <div className="r-nutri-cal">
            <span className="num">{cal}</span>
            <span className="unit">kcal</span>
            {totalMin > 0 && (
              <span className="per">{Math.round(cal / totalMin)} kcal<br />per min cooked</span>
            )}
          </div>
        ) : (
          <div className="r-nutri-cal">
            <span className="num" style={{ opacity: 0.4, fontSize: 28 }}>—</span>
            <span className="unit">kcal</span>
          </div>
        )}

        <div className="r-macro-rings">
          {macros.map(({ label, val, pct, color }) => (
            <div key={label} className="r-macro" style={{ "--ring-c": color, "--ring-p": pct }}>
              <div className="ring"><b>{label[0].toUpperCase()}</b></div>
              <div className="v">{val != null ? val : "—"}{val != null ? <sup>g</sup> : null}</div>
              <div className="l">{label}</div>
            </div>
          ))}
        </div>

        {nutriTags.length > 0 && (
          <div className="r-nutri-tags">
            {nutriTags.slice(0, 4).map((t, i) => (
              <span key={i} className={"r-nutri-tag " + (i % 3 === 1 ? "warm" : i % 3 === 2 ? "warm-y" : "")}>
                {t}
              </span>
            ))}
          </div>
        )}

        <div className="actions">
          <button className="btn sm" onClick={handleJumpNutrition}>Full nutrition ↓</button>
        </div>
        </>)}
      </aside>
    );
  }

  // ─── ORDER NOW CARD ───────────────────────────────────────────────────────

  function OrderNowCard({ recipe, user }) {
    const [status, setStatus] = useState("idle"); // idle | locating | located | error
    const [coords, setCoords] = useState(null);
    const [place, setPlace] = useState(null);     // { city, pincode, region, country } for display
    const [reused, setReused] = useState(false);  // located from a previously saved fix, no fresh prompt
    const signedIn = !!user;
    const isFinite_ = (n) => typeof n === "number" && Number.isFinite(n);

    // Reuse the location already saved on the user's profile, so signed-in users
    // aren't re-prompted across recipes. Re-runs when auth resolves (user?.id).
    useEffect(() => {
      if (!user) return;
      let cancelled = false;
      (async () => {
        const profile = await (window.MFC?.db?.getUserProfile?.() || Promise.resolve(null));
        if (cancelled || !profile) return;
        const loc = profile.location;
        if (loc && isFinite_(loc.lat) && isFinite_(loc.lng)) {
          setCoords({ lat: loc.lat, lng: loc.lng, accuracy: loc.accuracy });
          setPlace({ city: loc.city || null, pincode: loc.pincode || null, region: loc.region || null, country: loc.country || null });
          setReused(true);
          setStatus("located");
        }
      })();
      return () => { cancelled = true; };
    }, [user?.id]);

    async function requestLocation() {
      const geo = window.MFC && window.MFC.geo;
      if (!geo) { setStatus("error"); return; }
      setStatus("locating");
      let pos;
      try {
        pos = await geo.getPosition();
      } catch (_e) {
        setStatus("error");
        return;
      }
      setCoords(pos);
      setReused(false);
      setStatus("located");
      // Resolve a human-readable place (city + pincode) for display. Best-effort:
      // a geocode miss still leaves the located UI + coordinates intact.
      let resolved = {};
      try { resolved = await geo.reverseGeocode(pos.lat, pos.lng); } catch (_e) {}
      setPlace({ city: resolved.city || null, pincode: resolved.pincode || null, region: resolved.region || null, country: resolved.country || null });
      // Persist to the profile (signed-in only) so the next recipe reuses it.
      if (signedIn && window.MFC?.db?.upsertUserProfile) {
        window.MFC.db.upsertUserProfile({
          location: {
            lat: pos.lat,
            lng: pos.lng,
            accuracy: pos.accuracy,
            pincode: resolved.pincode || null,
            city: resolved.city || null,
            region: resolved.region || null,
            country: resolved.country || null,
            source: "gps",
            geocoder: resolved.geocoder || null,
            captured_at: new Date().toISOString(),
          },
        });
      }
    }

    const statusLabel =
      status === "locating" ? "locating…" :
      status === "located"  ? `${restaurants.length} nearby` :
      status === "error"    ? "location blocked" :
                              "awaiting location";

    // "Gurugram (122027)" when both known; degrade gracefully otherwise.
    const placeLabel = place
      ? (place.city && place.pincode ? `${place.city} (${place.pincode})`
         : place.city || place.pincode
         || [place.region, place.country].filter(Boolean).join(", ") || null)
      : null;

    const showPlacePill = status === "located" && placeLabel;

    return (
      <aside className="r-order">
        <div className="r-order-head">
          <h2>order now</h2>
          {showPlacePill
            ? <span className="r-order-place-pill"><span className="r-order-place-pin">⌖</span> {placeLabel}</span>
            : <span className="r-order-status">{statusLabel}</span>}
        </div>

        {status === "located" ? (
          <>
            <h3 className="r-order-title">Delivery isn't live near you <em>yet</em></h3>
            <p className="r-order-blurb">
              We found your location{placeLabel ? ` — ${placeLabel}` : ""}. Partner kitchens cooking from
              MFC recipes aren't available in your area yet — we'll surface them here the moment they are.
            </p>
          </>
        ) : (
          <>
            <h3 className="r-order-title">Find <em>this exact</em> dish near you</h3>
            <p className="r-order-blurb">
              We surface partner restaurants that cook from this same MFC recipe — same proportions,
              same ingredient list. To find the ones close enough to arrive hot, we need <b>precise location</b>.
            </p>

            <div className="r-order-actions">
              <button className="r-order-cta" type="button" onClick={requestLocation} disabled={status === "locating"}>
                <span className="r-order-cta-glyph">⌖</span>
                {status === "locating" ? "Locating…" : status === "error" ? "Try again" : "Share precise location"}
              </button>
            </div>

            {status === "error" && (
              <p className="r-order-err">Couldn't read your location — check the browser's location permission and try again.</p>
            )}
          </>
        )}
      </aside>
    );
  }

  // ─── RECIPE HERO ─────────────────────────────────────────────────────────
  // Focus = the square hero photo (left). The right column is the decision
  // panel: title, the servings slider, order-now, and the Cook-now CTA — sized
  // to sit beside the square on desktop, stacking under it on mobile. Mirrors
  // the Flutter recipe-detail hero.
  function RecipeHero({ recipe, servings, setServings, saved, onToggleSave, user, onRequestSignIn, justSaved, feedback, onSetFeedback }) {
    const titleParts = recipe.name.split(" ");
    const hero = recipe.media?.hero || {};
    const heroSrc = hero.src || recipe.heroImage || recipe.image || "";

    function handleSaveClick() {
      if (!user) { onRequestSignIn && onRequestSignIn(); return; }
      onToggleSave && onToggleSave();
    }
    function handleFeedback(sentiment) {
      if (!user) { onRequestSignIn && onRequestSignIn(); return; }
      onSetFeedback && onSetFeedback(sentiment);
    }

    return (
      <section className="r-hero">
        <div className="r-hero-left">
          <div className={"r-hero-stage" + (heroSrc ? " has-media" : "")}>
            {heroSrc ? (
              <img
                src={heroSrc}
                alt={hero.alt || recipe.name}
                onError={(e) => {
                  const parent = e.target.parentNode;
                  parent.classList.remove("has-media");
                  const fallback = document.createElement("div");
                  fallback.style.cssText = "width:100%;height:100%;display:grid;place-items:center;background:var(--cream-deep);font-family:var(--serif);font-style:italic;font-size:48px;color:var(--ink-muted);";
                  fallback.textContent = recipe.media?.emoji || "🍽";
                  e.target.replaceWith(fallback);
                }}
              />
            ) : (
              <div style={{
                width: "100%", height: "100%",
                display: "grid", placeItems: "center",
                background: "var(--cream-deep)",
                fontFamily: "var(--serif)", fontStyle: "italic",
                fontSize: 48, color: "var(--ink-muted)"
              }}>{recipe.media?.emoji || "🍽"}</div>
            )}
            <div className="r-react-bar at-top">
              <button
                className={"r-react-btn r-save-btn" + (saved ? " saved" : "") + (justSaved ? " just-saved" : "")}
                onClick={handleSaveClick}
                aria-label={saved ? "Unsave" : "Save recipe"}
                aria-pressed={saved}
                title={saved ? "Saved" : "Save"}
              >
                <span className="heart">{saved ? "♥" : "♡"}</span>
              </button>
            </div>
            <div className="r-react-bar at-bottom">
              <button
                className={"r-react-btn up" + (feedback === "up" ? " active" : "")}
                onClick={() => handleFeedback("up")}
                aria-label="Like — show me more like this"
                aria-pressed={feedback === "up"}
                title="Like"
              >👍</button>
              <button
                className={"r-react-btn down" + (feedback === "down" ? " active" : "")}
                onClick={() => handleFeedback("down")}
                aria-label="Not for me — hide from recommendations"
                aria-pressed={feedback === "down"}
                title="Not for me"
              >👎</button>
            </div>
            {heroSrc && hero.caption && <span className="r-hero-caption">{hero.caption}</span>}
          </div>
        </div>

        <div className="r-hero-right">
          <div className="r-hero-header">
            <div className="r-hero-eyebrow">
              <span>{recipe.cuisine}</span>
              {recipe.difficulty && (<><span className="dot">·</span><span>{recipe.difficulty}</span></>)}
            </div>
            <h1 className="r-hero-title">
              <em>{titleParts[0]}</em> {titleParts.slice(1).join(" ")}
            </h1>
            {recipe.tagline && <p className="r-hero-tagline">{recipe.tagline}</p>}
          </div>

          <ServingSlider servings={servings} setServings={setServings} />
          <OrderNowCard recipe={recipe} user={user} />
        </div>
      </section>
    );
  }

  // ─── STEP CARD ───────────────────────────────────────────────────────────
  function StepCard({ recipe, stepIdx, doneSteps }) {
    const step = (recipe.steps || [])[stepIdx] || {};
    const stepSrc = (step.media && step.media.src) || step.image || "";
    const total = (recipe.steps || []).length;
    const minutes = Math.round((step.duration || 0) / 60);
    const cumulativeBefore = (recipe.steps || []).slice(0, stepIdx).reduce((a, s) => a + (s.duration || 0), 0);
    const cumulativeAfter = cumulativeBefore + (step.duration || 0);
    const totalSecs = (recipe.steps || []).reduce((a, s) => a + (s.duration || 0), 0);

    return (
      <div className="r-step-card">
        <div className="r-step-head">
          <span className="r-step-tag">
            step <b>{String(stepIdx + 1).padStart(2, "0")}</b> / {String(total).padStart(2, "0")}
          </span>
          <span className="r-step-divider" />
          {totalSecs > 0 && (
            <span className="r-step-pacing">
              <span className="dot" />
              {fmtMin(cumulativeBefore)} → {fmtMin(cumulativeAfter)} of {fmtMin(totalSecs)}
            </span>
          )}
        </div>

        <h2 className="r-step-title">{step.title}</h2>
        <p className="r-step-detail">{step.detail || step.description}</p>

        <figure className="r-step-image">
          {stepSrc ? (
            <img src={stepSrc} alt={(step.media && step.media.alt) || step.title} />
          ) : (
            <div className="placeholder">step {stepIdx + 1} reference shot</div>
          )}
          <span className="cap">{(step.media && step.media.caption) || (step.title || "").toLowerCase()}</span>
        </figure>

        {step.tip && (
          <div className="r-step-tip">
            <span className="label-h">chef's note —</span>
            <p>{step.tip}</p>
          </div>
        )}

        <div className="r-step-foot">
          <div className="meta">
            {minutes > 0 && <span>this step <b>~ {minutes} min</b></span>}
            <span>·</span>
            <span><b>{(doneSteps || new Set()).size}</b> of {total} steps complete</span>
          </div>
          <span className="card-eyebrow" style={{ fontSize: 10 }}>use the player below to advance</span>
        </div>
      </div>
    );
  }

  // ─── COLLAPSIBLE CARD ────────────────────────────────────────────────────
  // Open state is internal by default; pass `open`/`setOpen` to control it from
  // outside (e.g. a "Full nutrition ↓" button expanding the full panel).
  function CollapseCard({ title, count, children, defaultOpen = true, footer, open: openProp, setOpen: setOpenProp, id }) {
    const [openState, setOpenState] = useState(defaultOpen);
    const open = openProp != null ? openProp : openState;
    const toggle = () => { if (setOpenProp) setOpenProp((o) => !o); else setOpenState((o) => !o); };
    return (
      <div className={"r-card" + (open ? " open" : "")} id={id}>
        <div className="r-card-head" onClick={toggle}>
          <h3>
            {title}
            {count !== undefined && <span className="count">· {count}</span>}
          </h3>
          <div className="r-chev">▾</div>
        </div>
        <div className="r-card-body">
          <div>
            <div className="r-card-inner">{children}</div>
            {footer && <div className="r-card-foot">{footer}</div>}
          </div>
        </div>
      </div>
    );
  }

  // ─── INGREDIENTS CARD ────────────────────────────────────────────────────
  // Servings now live in the standalone ServingSlider (the page owns the count),
  // so this card just lists the (scaled) ingredients, collapsed by default.
  function IngredientsCard({ recipe, servings }) {
    const serv = servings != null ? servings : (recipe.servings || 2);
    const ingredients = recipe.ingredients || [];

    return (
      <CollapseCard
        title="ingredients"
        count={ingredients.length}
        defaultOpen={false}
        footer={<>
          <button className="btn sm">Add to list</button>
          <button className="btn sm orange">Order all →</button>
        </>}
      >
        <div className="r-ing-list">
          {ingredients.map((ing, i) => (
            <a
              key={i}
              className="r-ing-row"
              href={ingLink(ing)}
              title={`See details for ${ing.name}`}
            >
              <FoodThumb
                emoji={ing.emoji || ing.icon || "•"}
                photo={ing.photo}
                name={ing.name}
                essential={ing.essential}
                size={42}
              />
              <span className="name">{ing.name}</span>
              <span className="r-ing-amt">
                {scaleAmt(ing, serv, recipe.servings || 2)}
              </span>
              <span className="r-ing-arrow" aria-hidden="true">
                <svg viewBox="0 0 12 12" width="11" height="11">
                  <path d="M3 2 L7 6 L3 10" fill="none" stroke="currentColor"
                    strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              </span>
            </a>
          ))}
        </div>
      </CollapseCard>
    );
  }

  // ─── UTENSILS CARD ───────────────────────────────────────────────────────
  function UtensilsCard({ recipe }) {
    const utensils = recipe.utensils || [];
    return (
      <CollapseCard title="utensils" count={utensils.length} defaultOpen={false}>
        <div className="r-ut-list">
          {utensils.map((u, i) => (
            <a
              key={i}
              className="r-ut-row"
              href={utLink(u)}
              title={`See details for ${u.name}`}
            >
              <FoodThumb
                emoji={u.emoji || u.icon || "🛠"}
                photo={u.photo}
                name={u.name}
                essential={u.essential}
                size={42}
              />
              <span className="name">{u.name}</span>
              <span className="r-ing-arrow" aria-hidden="true">
                <svg viewBox="0 0 12 12" width="11" height="11">
                  <path d="M3 2 L7 6 L3 10" fill="none" stroke="currentColor"
                    strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              </span>
            </a>
          ))}
        </div>
      </CollapseCard>
    );
  }

  // ─── HEALTH MARQUEE ──────────────────────────────────────────────────────
  function HealthMarquee({ facts }) {
    const display = useMemo(() => (facts && facts.length ? [...facts, ...facts] : []), [facts]);
    if (!display.length) return null;
    return (
      <div className="r-marquee">
        <div className="r-marquee-tag"><span className="pulse" /> health note</div>
        <div className="r-marquee-track">
          <div className="r-marquee-strip">
            {display.map((f, i) => <div key={i} className="item">{f}</div>)}
          </div>
        </div>
      </div>
    );
  }

  // ─── COOKING PLAYER ──────────────────────────────────────────────────────
  function CookingPlayer({ recipe, stepIdx, setStepIdx, doneSteps, setDoneSteps }) {
    const steps = recipe.steps || [];
    const total = steps.length;
    const step = steps[stepIdx] || { title: "", duration: 0 };
    const [running, setRunning] = useState(false);
    const [elapsed, setElapsed] = useState(0);

    useEffect(() => {
      const handler = () => setRunning(true);
      window.addEventListener("mfc:cook-now", handler);
      return () => window.removeEventListener("mfc:cook-now", handler);
    }, []);

    useEffect(() => { setElapsed(0); }, [stepIdx]);

    useEffect(() => {
      if (!running || !step.duration) return;
      const id = setInterval(() => {
        setElapsed(e => {
          const next = e + 1;
          if (next >= step.duration) {
            setDoneSteps(prev => new Set([...prev, stepIdx]));
            if (stepIdx < total - 1) {
              setTimeout(() => setStepIdx(stepIdx + 1), 400);
            } else {
              setRunning(false);
            }
            return step.duration;
          }
          return next;
        });
      }, 1000);
      return () => clearInterval(id);
    }, [running, step.duration, stepIdx, total]);

    function jump(delta) {
      const next = stepIdx + delta;
      if (next < 0 || next >= total) return;
      if (delta > 0) setDoneSteps(prev => new Set([...prev, stepIdx]));
      setStepIdx(next);
    }
    function jumpTo(i) {
      if (i === stepIdx) return;
      if (i > stepIdx) {
        const ds = new Set(doneSteps);
        for (let k = stepIdx; k < i; k++) ds.add(k);
        setDoneSteps(ds);
      }
      setStepIdx(i);
    }

    const dur = step.duration || 1;
    const segProgress = (Math.min(elapsed, dur) / dur) * 100;

    return (
      <div className={"r-player" + (running ? " playing" : "")}>
        <div className="r-player-inner">
          <div className="r-pl-controls">
            <button className="r-pl-step" onClick={() => jump(-1)} disabled={stepIdx === 0} aria-label="Previous step">
              <svg width="14" height="14" viewBox="0 0 14 14">
                <path d="M3 1v12M13 1L4 7l9 6V1z" fill="currentColor" strokeWidth="1" strokeLinejoin="round" />
              </svg>
            </button>
            <button className="r-pl-play" onClick={() => setRunning(r => !r)} aria-label={running ? "Pause" : "Start"}>
              {running ? (
                <svg width="12" height="13" viewBox="0 0 12 13">
                  <rect x="1" y="0.5" width="3.4" height="12" rx="1" fill="currentColor" />
                  <rect x="7.6" y="0.5" width="3.4" height="12" rx="1" fill="currentColor" />
                </svg>
              ) : (
                <svg width="12" height="13" viewBox="0 0 12 13">
                  <path d="M2 1.2L11 6.5L2 11.8V1.2Z" fill="currentColor" strokeWidth="1.4" strokeLinejoin="round" />
                </svg>
              )}
            </button>
            <button className="r-pl-step" onClick={() => jump(1)} disabled={stepIdx === total - 1} aria-label="Next step">
              <svg width="14" height="14" viewBox="0 0 14 14">
                <path d="M11 1v12M1 1l9 6-9 6V1z" fill="currentColor" strokeWidth="1" strokeLinejoin="round" />
              </svg>
            </button>
          </div>

          <div className="r-pl-now">
            <div className="r-pl-now-row">
              <span className="r-pl-counter">
                {String(stepIdx + 1).padStart(2, "0")}<span className="dim">/{String(total).padStart(2, "0")}</span>
              </span>
              <span className="r-pl-name" title={step.title}>{step.title}</span>
            </div>
            <div
              className="r-pl-bar"
              role="slider"
              tabIndex={0}
              aria-label="Step elapsed time — arrow keys to scrub"
              aria-valuemin={0}
              aria-valuemax={dur}
              aria-valuenow={elapsed}
              aria-valuetext={fmtTime(elapsed)}
              onKeyDown={(e) => {
                let v = elapsed;
                if (e.key === "ArrowRight" || e.key === "ArrowUp") v = Math.min(dur, elapsed + 5);
                else if (e.key === "ArrowLeft" || e.key === "ArrowDown") v = Math.max(0, elapsed - 5);
                else if (e.key === "Home") v = 0;
                else if (e.key === "End") v = dur;
                else return;
                e.preventDefault();
                setElapsed(v);
              }}
              onClick={(e) => {
                const r = e.currentTarget.getBoundingClientRect();
                const pct = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
                setElapsed(Math.round(pct * dur));
              }}
            >
              <div className="r-pl-bar-fill" style={{ width: `${segProgress}%` }} />
              <div className="r-pl-bar-knob" style={{ left: `${segProgress}%` }} />
            </div>
            <div className="r-pl-meta-row">
              <span className="r-pl-time">{fmtTime(elapsed)}</span>
              <span className="r-pl-status">
                {running ? "● cooking" : (doneSteps || new Set()).has(stepIdx) ? "✓ done" : "ready"}
              </span>
              <span className="r-pl-time end">{fmtTime(dur)}</span>
            </div>
          </div>

          <div className="r-pl-dots" role="tablist" aria-label="Steps">
            {steps.map((s, i) => {
              const isDone = (doneSteps || new Set()).has(i);
              const isNow = i === stepIdx;
              return (
                <button
                  key={i}
                  className={"r-pl-dot" + (isDone ? " done" : isNow ? " now" : "")}
                  onClick={() => jumpTo(i)}
                  aria-label={`Step ${i + 1}: ${s.title}`}
                  title={`${i + 1}. ${s.title}${s.duration ? ` · ${fmtMin(s.duration)}` : ""}`}
                />
              );
            })}
          </div>
        </div>
      </div>
    );
  }

  // ─── FULL FDA-SHAPED NUTRITION SECTION ───────────────────────────────────
  // Compact table row: name · value+unit · thin %DV bar · percent. Packs tightly
  // (auto-fill grid → multiple columns) so the panel reads dense, not sparse.
  function NutrientCell({ row }) {
    const [name, val, unit, dv, flag] = row;
    const fillPct = dv != null ? Math.min(100, Math.max(0, dv)) : 0;
    const flagClass = flag === 'high' ? ' flag-high' : flag === 'warn' ? ' flag-warn' : '';
    const barClass = flag === 'warn' ? 'warn' : (dv != null && dv > 100) ? 'over' : '';
    return (
      <div className={"r-nutri-cell" + flagClass}>
        <span className="name">{name}</span>
        <span className="val">{val}<i className="unit">{unit}</i></span>
        {dv != null ? (
          <span className="dv">
            <span className="dv-bar"><span className={barClass} style={{ width: fillPct + '%' }} /></span>
            <span className="pct">{dv}%</span>
          </span>
        ) : (
          <span className="dv nodv">no DV</span>
        )}
      </div>
    );
  }

  function NutritionSection({ nutrition, embedded }) {
    const [tab, setTab] = useState('all');

    // Macros + micros in one list, so every nutrient shows together (matching
    // the ingredient detail page). Per-group tabs filter within it.
    const groups = (nutrition && nutrition.groups)
      || [...((nutrition && nutrition.macro) || []), ...((nutrition && nutrition.micro) || [])];
    if (!groups.length) return null;

    const totalFields = groups.reduce((n, g) => n + g.rows.length, 0);
    const visibleGroups = tab === 'all' ? groups : groups.filter(g => g.name === tab);

    return (
      <section id="full-nutrition" className={"r-nutri-full" + (embedded ? " embedded" : "")}>
        {embedded ? (
          <p className="r-nutri-full-prov">{nutrition.basis} · {nutrition.source}</p>
        ) : (
          <div className="r-nutri-full-head">
            <div>
              <div className="eyebrow-comment" style={{ marginBottom: 6 }}>
                full nutrient profile · {nutrition.servings > 1 ? `${nutrition.servings} servings` : 'per serving'}
              </div>
              <h2>The whole <em>FDA panel</em></h2>
              <p className="r-nutri-full-blurb">
                Every nutrient derived from each ingredient's USDA FoodData Central
                record, scaled to {nutrition.servings > 1 ? `${nutrition.servings} servings` : 'a single serving'}.
                Rows with no data are hidden.
              </p>
            </div>
            <div className="r-nutri-full-source">
              <strong>{nutrition.basis}</strong>
              {nutrition.source}
            </div>
          </div>
        )}

        {groups.length > 1 && (
          <div className="r-nutri-tabs">
            <button
              className={"r-nutri-tab" + (tab === 'all' ? ' active' : '')}
              onClick={() => setTab('all')}
            >
              All <span className="badge">{totalFields}</span>
            </button>
            {groups.map(g => (
              <button
                key={g.name}
                className={"r-nutri-tab" + (tab === g.name ? ' active' : '')}
                onClick={() => setTab(g.name)}
              >
                {g.name} <span className="badge">{g.rows.length}</span>
              </button>
            ))}
          </div>
        )}

        {visibleGroups.map(g => (
          <div key={g.name} className="r-nutri-group">
            <div className="r-nutri-group-title">
              {g.name}
              <span className="grp-count">{g.rows.length} fields</span>
            </div>
            <div className="r-nutri-grid r-nutri-grid--cells">
              {g.rows.map((row, i) => <NutrientCell key={i} row={row} />)}
            </div>
          </div>
        ))}

        <div className="r-nutri-legend">
          <div className="item"><span className="sw high" /> high · ≥ 20% DV</div>
          <div className="item"><span className="sw warn" /> watch · over recommended limit</div>
          <div className="item"><span className="sw regular" /> regular contribution</div>
          <span className="dv-ref">%DV based on a 2,000 kcal reference diet</span>
        </div>

        <p className="r-nutri-disclaim">
          <em>Note —</em> values are estimates based on average ingredient profiles
          and the amounts in this recipe. They do not account for cooking losses,
          brand variation, or individual portion weighing.
        </p>
      </section>
    );
  }

  // ─── SERVINGS SLIDER CARD ─────────────────────────────────────────────────
  // Standalone control that scales the whole recipe (ingredients + nutrition).
  // Slider 1 → max (6), defaults to 2. Mirrors the Flutter ServingSelectorCard.
  function ServingSlider({ servings, setServings, min = 1, max = 6 }) {
    const val = Math.max(min, Math.min(max, servings || 2));
    const fillPct = ((val - min) / (max - min)) * 100;
    return (
      <div className="r-serv-card">
        <div className="r-serv-card-head">
          <div>
            <h3>servings</h3>
            <p>slide to scale ingredients &amp; nutrition</p>
          </div>
          <span className="r-serv-badge">{val}</span>
        </div>
        <input
          className="r-serv-slider"
          type="range"
          min={min}
          max={max}
          step={1}
          value={val}
          style={{ "--fill": fillPct + "%" }}
          onChange={(e) => setServings(Number(e.target.value))}
          aria-label="Servings"
        />
        <div className="r-serv-ticks">
          {Array.from({ length: max - min + 1 }, (_, i) => min + i).map((n) => (
            <span key={n} className={n === val ? "on" : ""}>{n}</span>
          ))}
        </div>
      </div>
    );
  }

  // ─── COOK NOW CTA ─────────────────────────────────────────────────────────
  function CookCta({ recipe, resumeStep, onCookNow }) {
    const steps = (recipe.steps || []).length;
    const mins = recipe.totalMinutes || recipe.minutes || 0;
    const resuming = resumeStep > 0 && resumeStep < steps;
    const sub = [resuming ? `resume · step ${resumeStep + 1}` : "guided",
      steps ? `${steps} steps` : null, mins ? `~${mins} min` : null].filter(Boolean).join("  ·  ");
    return (
      <button className="r-cook-cta" type="button" onClick={onCookNow} disabled={!steps}>
        <span className="r-cook-cta-play" aria-hidden="true">
          <svg width="13" height="14" viewBox="0 0 13 14"><path d="M2 1.4 L11.5 7 L2 12.6 Z" fill="currentColor" strokeWidth="1.4" strokeLinejoin="round" /></svg>
        </span>
        <span className="r-cook-cta-txt">
          <b>{resuming ? "Resume cooking" : "Cook now"}</b>
          <span>{sub}</span>
        </span>
        <span className="r-cook-cta-arrow" aria-hidden="true">→</span>
      </button>
    );
  }

  // ─── METHOD (collapsed steps overview) ────────────────────────────────────
  function MethodCard({ recipe, onPlayStep }) {
    const steps = recipe.steps || [];
    return (
      <CollapseCard title="method" count={steps.length} defaultOpen={false}>
        <div className="r-method-list">
          {steps.map((s, i) => (
            <button key={i} className="r-method-row" type="button" onClick={() => onPlayStep(i)}>
              <span className="r-method-n">{i + 1}</span>
              <span className="r-method-body">
                <span className="r-method-title">{s.title || `Step ${i + 1}`}</span>
                {s.duration ? <span className="r-method-dur">~ {Math.round(s.duration / 60)} min</span> : null}
              </span>
              <span className="r-method-play" aria-hidden="true">
                <svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="9" fill="none" stroke="currentColor" strokeWidth="1.3" /><path d="M8 6.5 L14 10 L8 13.5 Z" fill="currentColor" /></svg>
              </span>
            </button>
          ))}
        </div>
      </CollapseCard>
    );
  }

  // ─── FULL-SCREEN COOKING PLAYER ───────────────────────────────────────────
  // Immersive "now playing" overlay: the step photo fills the screen, caption +
  // a big countdown sit over a scrim, transport advances steps, and the Web
  // Speech API narrates each step. Mirrors the Flutter CookingPlayerScreen.
  function CookingPlayerOverlay({ recipe, stepIdx, setStepIdx, doneSteps, setDoneSteps, onClose }) {
    const steps = recipe.steps || [];
    const total = steps.length;
    const step = steps[stepIdx] || {};
    const [running, setRunning] = useState(false);
    const [elapsed, setElapsed] = useState(0);
    const [narrate, setNarrate] = useState(true);

    const stepSrc = (step.media && step.media.src) || step.image || "";
    const dur = step.duration || 0;
    const remaining = Math.max(0, dur - elapsed);
    const segPct = dur ? Math.min(100, (elapsed / dur) * 100) : 0;

    const speak = () => {
      if (!narrate || !window.speechSynthesis) return;
      try {
        window.speechSynthesis.cancel();
        const txt = `Step ${stepIdx + 1}. ${step.title || ""}. ${step.detail || step.description || ""}`;
        const u = new SpeechSynthesisUtterance(txt);
        u.rate = 1.0; u.lang = "en-US";
        window.speechSynthesis.speak(u);
      } catch (_e) {}
    };
    const stopSpeak = () => { try { window.speechSynthesis && window.speechSynthesis.cancel(); } catch (_e) {} };

    useEffect(() => { setElapsed(0); }, [stepIdx]);
    useEffect(() => { speak(); return stopSpeak; }, [stepIdx]); // narrate on step enter
    useEffect(() => stopSpeak, []); // stop on unmount
    useEffect(() => { // lock body scroll while open
      const prev = document.body.style.overflow;
      document.body.style.overflow = "hidden";
      return () => { document.body.style.overflow = prev; };
    }, []);

    useEffect(() => {
      if (!running || !dur) return;
      const id = setInterval(() => {
        setElapsed((e) => {
          const next = e + 1;
          if (next >= dur) {
            setDoneSteps((p) => new Set([...p, stepIdx]));
            if (stepIdx < total - 1) setTimeout(() => setStepIdx(stepIdx + 1), 400);
            else setRunning(false);
            return dur;
          }
          return next;
        });
      }, 1000);
      return () => clearInterval(id);
    }, [running, dur, stepIdx, total]);

    function toggleNarrate() {
      const next = !narrate;
      setNarrate(next);
      if (next) { const u = new SpeechSynthesisUtterance(`Step ${stepIdx + 1}. ${step.title || ""}. ${step.detail || step.description || ""}`); u.rate = 1.0; u.lang = "en-US"; try { window.speechSynthesis.cancel(); window.speechSynthesis.speak(u); } catch (_e) {} }
      else stopSpeak();
    }
    function jump(delta) {
      const n = stepIdx + delta;
      if (n < 0 || n >= total) return;
      if (delta > 0) setDoneSteps((p) => new Set([...p, stepIdx]));
      setRunning(false); setStepIdx(n);
    }
    function jumpTo(i) {
      if (i === stepIdx) return;
      if (i > stepIdx) { const ds = new Set(doneSteps); for (let k = stepIdx; k < i; k++) ds.add(k); setDoneSteps(ds); }
      setRunning(false); setStepIdx(i);
    }
    function finish() { setDoneSteps((p) => new Set([...p, stepIdx])); stopSpeak(); onClose(); }

    return (
      <div className="r-cook-overlay" role="dialog" aria-modal="true" aria-label="Guided cooking">
        <div className="r-cook-bg" style={stepSrc ? { backgroundImage: `url(${stepSrc})` } : {}}>
          {!stepSrc && <span className="r-cook-emoji">{recipe.media?.emoji || "🍳"}</span>}
        </div>
        <div className="r-cook-scrim" />
        <div className="r-cook-ui">
          <div className="r-cook-top">
            <button className="r-cook-icon" onClick={() => { stopSpeak(); onClose(); }} aria-label="Close player">✕</button>
            <span className="r-cook-counter">{String(stepIdx + 1).padStart(2, "0")} / {String(total).padStart(2, "0")}</span>
            <button className={"r-cook-icon" + (narrate ? " on" : "")} onClick={toggleNarrate} aria-label={narrate ? "Mute narration" : "Read steps aloud"}>{narrate ? "🔊" : "🔇"}</button>
          </div>

          <div className="r-cook-caption">
            <div className="r-cook-eyebrow">STEP {String(stepIdx + 1).padStart(2, "0")}<span>· {dur ? fmtMin(dur) : "untimed step"}</span></div>
            <h2 className="r-cook-title">{step.title}</h2>
            {(step.detail || step.description) && <p className="r-cook-detail">{step.detail || step.description}</p>}
            {step.tip && (
              <div className="r-cook-tip"><span className="lbl">chef's note</span><p>{step.tip}</p></div>
            )}
          </div>

          <div className="r-cook-controls">
            {dur > 0 ? (
              <>
                <div className="r-cook-time">
                  <b>{fmtTime(remaining)}</b>
                  <span className={running ? "on" : ""}>{running ? "● cooking" : "paused"}</span>
                  <i>of {fmtTime(dur)}</i>
                </div>
                <div
                  className="r-cook-bar"
                  role="slider"
                  tabIndex={0}
                  aria-label="Elapsed time — arrow keys to scrub"
                  aria-valuemin={0}
                  aria-valuemax={dur}
                  aria-valuenow={elapsed}
                  aria-valuetext={fmtTime(elapsed)}
                  onKeyDown={(e) => {
                    let v = elapsed;
                    if (e.key === "ArrowRight" || e.key === "ArrowUp") v = Math.min(dur, elapsed + 5);
                    else if (e.key === "ArrowLeft" || e.key === "ArrowDown") v = Math.max(0, elapsed - 5);
                    else if (e.key === "Home") v = 0;
                    else if (e.key === "End") v = dur;
                    else return;
                    e.preventDefault();
                    setElapsed(v);
                  }}
                  onClick={(e) => { const r = e.currentTarget.getBoundingClientRect(); const pct = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)); setElapsed(Math.round(pct * dur)); }}
                >
                  <div className="r-cook-bar-fill" style={{ width: segPct + "%" }} />
                </div>
              </>
            ) : (
              <div className="r-cook-untimed">tap ▸ to move on when this step is done</div>
            )}

            <div className="r-cook-dots">
              {steps.map((s, i) => (
                <button key={i} className={"r-cook-dot" + (doneSteps.has(i) ? " done" : i === stepIdx ? " now" : "")} onClick={() => jumpTo(i)} aria-label={`Step ${i + 1}`} />
              ))}
            </div>

            <div className="r-cook-transport">
              <button className="r-cook-tbtn" onClick={() => jump(-1)} disabled={stepIdx === 0} aria-label="Previous step">
                <svg width="16" height="16" viewBox="0 0 16 16"><path d="M4 2v12M14 2L5 8l9 6V2z" fill="currentColor" strokeWidth="1" strokeLinejoin="round" /></svg>
              </button>
              <button className="r-cook-tbtn play" onClick={() => setRunning((r) => !r)} aria-label={running ? "Pause" : "Start"}>
                {running
                  ? <svg width="16" height="17" viewBox="0 0 16 17"><rect x="2" y="1" width="4.2" height="15" rx="1.2" fill="currentColor" /><rect x="9.8" y="1" width="4.2" height="15" rx="1.2" fill="currentColor" /></svg>
                  : <svg width="16" height="17" viewBox="0 0 16 17"><path d="M3 1.5 L14 8.5 L3 15.5 Z" fill="currentColor" strokeWidth="1.5" strokeLinejoin="round" /></svg>}
              </button>
              <button className="r-cook-tbtn" onClick={() => (stepIdx < total - 1 ? jump(1) : finish())} aria-label={stepIdx < total - 1 ? "Next step" : "Finish"}>
                {stepIdx < total - 1
                  ? <svg width="16" height="16" viewBox="0 0 16 16"><path d="M12 2v12M2 2l9 6-9 6V2z" fill="currentColor" strokeWidth="1" strokeLinejoin="round" /></svg>
                  : <svg width="16" height="16" viewBox="0 0 16 16"><path d="M3 8.5l3.5 3.5L13 4.5" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg>}
              </button>
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ─── expose to window ────────────────────────────────────────────────────
  Object.assign(window, {
    RecipeHero,
    StepCard,
    CollapseCard,
    IngredientsCard,
    UtensilsCard,
    CookingPlayer,
    CookingPlayerOverlay,
    ServingSlider,
    CookCta,
    MethodCard,
    NutritionGlanceCard,
    HealthMarquee,
    NutritionSection,
  });
})();
