// ====================================================================
// lw-core.jsx — Last Wishes recording: shared engine + primitives
// Loaded before all five flow files. Exposes data, the camera/recording
// hook (with a clean no-camera fallback), and Helm-styled primitives.
// ====================================================================
const { useState: useStateLW, useEffect: useEffectLW, useRef: useRefLW,
        useCallback: useCbLW, useMemo: useMemoLW } = React;

// ── One-time CSS (lw- prefixed so it can't collide) ─────────────────
if (typeof document !== "undefined" && !document.getElementById("lw-core-styles")) {
  const s = document.createElement("style");
  s.id = "lw-core-styles";
  s.textContent = `
    @keyframes lwBlink { 0%,100%{opacity:1} 50%{opacity:.25} }
    @keyframes lwBar { 0%,100%{transform:scaleY(.35)} 50%{transform:scaleY(1)} }
    /* Entrance animations are transform-only so content is ALWAYS visible even
       if the animation never ticks (e.g. a backgrounded/hidden document pauses
       CSS animations). Never hide must-see content behind opacity:0. */
    @keyframes lwFade { from{translate:0 10px} to{translate:0 0} }
    @keyframes lwFadeNoY { from{translate:0 0} to{translate:0 0} }
    @keyframes lwRing { 0%{transform:scale(.7);opacity:.9} 100%{transform:scale(1.7);opacity:0} }
    @keyframes lwScrollDemo { from{transform:translateY(0)} to{transform:translateY(var(--lw-scroll-to,-60%))} }
    @keyframes lwPulseSoft { 0%,100%{opacity:.45} 50%{opacity:1} }
    @keyframes lwBeatScrim { 0%{opacity:0} 18%{opacity:1} 78%{opacity:1} 100%{opacity:0} }
    @keyframes lwBeatCard { 0%{opacity:0; translate:0 16px} 16%{opacity:1; translate:0 0} 76%{opacity:1; translate:0 0} 100%{opacity:0; translate:0 -10px} }
    .lw-fade { animation: lwFade .5s cubic-bezier(.2,.7,.2,1) both; }
    .lw-fade-plain { animation: lwFadeNoY .5s ease both; }
    .lw-stage *::-webkit-scrollbar { width:8px; }
    .lw-stage *::-webkit-scrollbar-thumb { background:rgba(0,29,0,.18); border-radius:4px; }
    .lw-press { transition: transform .12s ease, box-shadow .12s ease, background .15s ease, opacity .15s ease; }
    .lw-press:active { transform: translateY(1px); }
  `;
  document.head.appendChild(s);
}

// ── Cast: the policy owner (recording) + her beneficiaries ──────────
const LW_PO = { name: "Suzie Garcia", firstName: "Suzie",
  photo: (window.HELM_PORTRAITS && window.HELM_PORTRAITS.f) || "lw-assets/suzie.png" };
const LW_BENES = [
  { id: "john",  name: "John Garcia",  firstName: "John",  rel: "Husband",
    photo: "lw-assets/john.png", initials: "JG", accent: "#9888BF" },
  { id: "sarah", name: "Sarah Garcia", firstName: "Sarah", rel: "Daughter, 15",
    photo: null, initials: "SG", accent: "#70B6BA" },
  { id: "rebecca", name: "Rebecca Torres", firstName: "Rebecca", rel: "Sister",
    photo: null, initials: "RT", accent: "#D38D54" },
];
const lwBene = (id) => LW_BENES.find(b => b.id === id) || LW_BENES[0];

// ── Two themes per beneficiary ──────────────────────────────────────
const LW_THEMES = [
  { id: "hope",   label: "A message of hope",      order: "Video 1 of 2",
    blurb: "Warmth, encouragement, and your hopes for their future.", accent: "#9888BF" },
  { id: "payout", label: "Guidance for the payout", order: "Video 2 of 2",
    blurb: "What this money is for, in your own words.", accent: "#70B6BA" },
];
const lwTheme = (id) => LW_THEMES.find(t => t.id === id) || LW_THEMES[0];

// ── Talking-point prompts (AI-suggested; PO improvises) ─────────────
// Personalised by recipient first name. Each prompt has a short hint the
// PO can lean on. These are SUGGESTIONS, not a script.
function lwPrompts(beneOrId, themeId) {
  const b = (beneOrId && typeof beneOrId === "object") ? beneOrId : lwBene(beneOrId);
  const n = b.firstName;
  const child = /daughter|son|child/i.test(b.rel);
  if (themeId === "payout") {
    return [
      { t: `What this money is really for`, h: `Peace of mind — not a windfall. Say what you hope it protects.`,
        ex: `This isn't a windfall, ${n}. It's a cushion — so you never have to make a hard choice in a hard week.`,
        alts: [`Think of it as breathing room, not a prize.`, `It's here so the worry stops, not so life gets fancy.`] },
      { t: `Take care of the mortgage first`, h: `The house on Maple St. Keeping it is keeping home.`,
        ex: `Pay off the house first. Keeping our home is keeping us — that roof has all our memories under it.`,
        alts: [`The mortgage comes first. I want you safe under that roof.`, `Clear the house payment, then you can breathe.`] },
      child
        ? { t: `${n}'s education is set aside`, h: `College, and whatever she wants to become after.`,
            ex: `Your education is already taken care of — college, and whatever you decide to become after that.`,
            alts: [`Don't worry about tuition. That part is handled.`, `School is paid for. Go learn everything you can.`] }
        : { t: `Keep the day-to-day steady`, h: `Groceries, bills, the small things that add up.`,
            ex: `Set aside enough for a year of the ordinary stuff — groceries, the bills, the small things.`,
            alts: [`Cover the everyday things so nothing skips a beat.`, `Keep the lights on and the fridge full. That's plenty.`] },
      { t: `Don't rush any big decisions`, h: `Give it a year. Nothing has to change overnight.`,
        ex: `Give it a full year before you change anything big. Nothing has to happen overnight.`,
        alts: [`Don't sell, don't move, don't decide anything for twelve months.`, `Sit with it a while. The money will keep.`] },
      { t: `Who to lean on for advice`, h: `Name someone you trust — a person, not a firm.`,
        ex: `Talk to my brother Ray before any big money moves. He's steady, and he loves you.`,
        alts: [`Run big decisions past someone who loves you first.`, `Find one person you trust completely, and lean on them.`] },
      { t: `Treat yourselves to one good thing`, h: `That trip you kept putting off. You have my blessing.`,
        ex: `And take the trip we kept putting off. I mean it — you have my blessing to enjoy this.`,
        alts: [`Do one thing that's purely joyful. You've earned it.`, `Spend a little on something that makes you smile. For me.`] },
    ];
  }
  // hope
  return [
    { t: `A value to carry forward`, h: `The one thing you most hope ${n} keeps.`,
      ex: `If you hold onto one thing of mine, ${n}, let it be the way we always made time for the people we love.`,
      alts: [`Stay generous. That's the thing I'd want to live on in you.`, `Keep looking after people. That's our family's whole heart.`] },
    { t: `A favorite memory of us`, h: `Pick a small, specific one. The specific ones land.`,
      ex: `I still think about that rainy week when the power went out and we played cards by candlelight. I was so happy.`,
      alts: [`Remember the morning we got lost driving to the coast and didn't care.`, `Our slow Sunday breakfasts — nothing fancy, just us.`] },
    child
      ? { t: `Something you can't wait for ${n} to do`, h: `Graduation, first apartment, finding her people.`,
          ex: `I can't wait for you to find the thing that lights you up — and then chase it with everything you've got.`,
          alts: [`I hope you find your people, the ones who really see you.`, `Go make the life you daydream about. All of it.`] }
      : { t: `Something you hope ${n} finally does`, h: `The plan you both kept putting off.`,
          ex: `Do the thing you keep talking yourself out of. You'd be wonderful at it — I always knew that.`,
          alts: [`Go back to the place that makes you feel alive. Don't wait.`, `Pick up the thing you set down. It's still yours.`] },
    { t: `Why ${n} is free to live their own life`, h: `Permission to be happy. Say it plainly.`,
      ex: `You have my blessing to be happy, ${n}. Don't put your life on hold for me — that's the last thing I'd want.`,
      alts: [`Grief isn't loyalty. Go live, and I'll be cheering you on.`, `Be happy. Truly. That's how you honor me.`] },
    { t: `What made you proud of ${n}`, h: `A moment you think about often.`,
      ex: `The way you show up for the people who need you — I was never prouder of anyone in my life.`,
      alts: [`You kept your word, always. That's a rare thing.`, `You stayed kind even when life wasn't. I noticed.`] },
    { t: `How you want ${n} to remember you`, h: `Not perfect — just yours.`,
      ex: `Don't remember me as someone who was sick. Remember me laughing too loud and loving you completely.`,
      alts: [`Remember me happy, with dirt on my hands in the garden.`, `Just remember I was yours, and you were mine.`] },
  ];
}

// ── Example clips (models the PO can watch before recording) ────────
// Short, warm scripts in Helm's in-product voice. "Watching" plays the
// transcript as gentle captions over a poster (no real audio asset).
const LW_EXAMPLES = [
  { id: "ex-hope", who: "Maria, 58", theme: "A message of hope", dur: 38,
    poster: "lw-assets/ambient-1.jpg",
    lines: [
      "Hey sweetheart. If you're watching this, I want you to know the first thing on my mind is you.",
      "Remember the summer we drove to the coast with no plan? That's how I want you to live — open.",
      "You don't owe anyone a sad life. Go be happy. That's the whole assignment.",
      "I'm so proud of you. I always was.",
    ] },
  { id: "ex-payout", who: "David, 61", theme: "Guidance for the payout", dur: 31,
    poster: "lw-assets/ambient-2.jpg",
    lines: [
      "This money isn't a prize — it's a cushion, so you never have to make a hard choice in a hard week.",
      "Pay off the house first. Then breathe. Nothing else is urgent.",
      "Talk to your uncle Ray before any big moves. He's steady.",
      "And take the trip. I mean it.",
    ] },
];

// ====================================================================
// Camera + recording engine
// ====================================================================
// One shared MediaStream across all flows — a single permission grant.
let _lwStreamPromise = null;

// Audio constraints — CRITICAL for recording while a video conference is live.
//
// `audio: true` defaults echoCancellation / noiseSuppression / autoGainControl
// all ON. With echo cancellation ON, Chromium opens the mic through the OS
// "communications" (AEC) audio path — the SAME exclusive path Zoom / Google Meet
// grab during a call. The OS allows only one owner of that path at a time, so the
// conferencing app wins and the browser gets a live-but-SILENT audio track. Video
// (a separate device) is unaffected, and MediaRecorder muxes the silent track in
// without error — which is exactly the "video records, audio is missing" symptom.
//
// Disabling these flags opens the mic on the shared MULTIMEDIA path instead, which
// coexists with the conferencing app — so real audio is captured during a call.
// (It also yields more faithful, unprocessed sound, which suits a keepsake video.)
const LW_AUDIO_CONSTRAINTS = {
  echoCancellation: false,
  noiseSuppression: false,
  autoGainControl: false,
};

function lwAcquireStream() {
  if (_lwStreamPromise) return _lwStreamPromise;
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
    return Promise.reject(new Error("unsupported"));
  }
  _lwStreamPromise = navigator.mediaDevices
    .getUserMedia({
      video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
      audio: LW_AUDIO_CONSTRAINTS,
    })
    // If the browser rejects the explicit audio constraints (very old engines),
    // fall back to plain `audio: true` rather than losing the mic entirely.
    .catch(() => navigator.mediaDevices.getUserMedia({
      video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
      audio: true,
    }))
    .catch((e) => { _lwStreamPromise = null; throw e; });
  return _lwStreamPromise;
}

// Live mic-level monitor. Taps the stream's audio track through a Web Audio
// AnalyserNode and reports a smoothed 0..1 level via onLevel on every frame.
// Lets the studio prove audio is flowing — and catch a silent track (e.g. the mic
// got hijacked) instead of silently shipping an empty soundtrack. Returns a stop fn.
function lwMonitorAudioLevel(stream, onLevel) {
  const noop = () => {};
  if (!stream || typeof onLevel !== "function") return noop;
  const Ctx = window.AudioContext || window.webkitAudioContext;
  const track = stream.getAudioTracks && stream.getAudioTracks()[0];
  if (!Ctx || !track) { onLevel(0, { hasTrack: !!track }); return noop; }

  let ctx, raf, stopped = false;
  try {
    ctx = new Ctx();
    if (ctx.state === "suspended") ctx.resume().catch(() => {});
    const src = ctx.createMediaStreamSource(stream);
    const analyser = ctx.createAnalyser();
    analyser.fftSize = 1024;
    analyser.smoothingTimeConstant = 0.78;
    src.connect(analyser);
    const buf = new Uint8Array(analyser.fftSize);
    const freq = new Uint8Array(analyser.frequencyBinCount);
    const NB = 7;                                        // bands for the equalizer
    let smooth = 0;
    const loop = () => {
      if (stopped) return;
      analyser.getByteTimeDomainData(buf);
      let sum = 0;
      for (let i = 0; i < buf.length; i++) { const v = (buf[i] - 128) / 128; sum += v * v; }
      const rms = Math.sqrt(sum / buf.length);          // 0..~1
      // Gain tuned so normal speaking voice reaches ~halfway up the meter.
      smooth = smooth * 0.55 + Math.min(1, rms * 10) * 0.45;
      // Per-band magnitudes across the vocal range, for a pulsing equalizer.
      analyser.getByteFrequencyData(freq);
      const usable = Math.floor(freq.length * 0.46);     // ignore the airy top end
      const per = Math.max(1, Math.floor(usable / NB));
      const bands = [];
      for (let b = 0; b < NB; b++) {
        let s = 0; for (let k = 0; k < per; k++) s += freq[b * per + k] || 0;
        bands.push(Math.min(1, (s / per / 255) * 1.7));
      }
      onLevel(smooth, { hasTrack: true, muted: track.muted, rms, bands });
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
  } catch (e) {
    return noop;
  }
  return () => {
    stopped = true;
    if (raf) cancelAnimationFrame(raf);
    if (ctx) ctx.close().catch(() => {});
  };
}
function lwPickMime() {
  const opts = ["video/webm;codecs=vp9,opus", "video/webm;codecs=vp8,opus", "video/webm", "video/mp4"];
  if (typeof MediaRecorder === "undefined") return null;
  return opts.find((m) => { try { return MediaRecorder.isTypeSupported(m); } catch { return false; } }) || "";
}

// useRecorder — drives one recording surface.
// status: idle | requesting | ready | recording | recorded | denied | unsupported
function useRecorder() {
  const [status, setStatus] = useStateLW("idle");
  const [mode, setMode] = useStateLW(null);          // 'live' | 'mock'
  const [elapsed, setElapsed] = useStateLW(0);       // seconds (float)
  const [recordedUrl, setRecordedUrl] = useStateLW(null);
  const [clips, setClips] = useStateLW([]);          // [{url,dur,mock}]
  const streamRef = useRefLW(null);
  const recRef = useRefLW(null);
  const chunksRef = useRefLW([]);
  const tickRef = useRefLW(null);
  const startRef = useRefLW(0);

  const stopTick = () => { if (tickRef.current) { cancelAnimationFrame(tickRef.current); tickRef.current = null; } };
  const runTick = () => {
    const loop = () => { setElapsed((Date.now() - startRef.current) / 1000); tickRef.current = requestAnimationFrame(loop); };
    tickRef.current = requestAnimationFrame(loop);
  };

  const requestCamera = useCbLW(async () => {
    if (streamRef.current) { setStatus("ready"); setMode("live"); return "live"; }
    setStatus("requesting");
    try {
      const stream = await lwAcquireStream();
      streamRef.current = stream;
      setMode("live"); setStatus("ready");
      return "live";
    } catch (e) {
      setMode("mock");
      setStatus(e && e.message === "unsupported" ? "unsupported" : "denied");
      return "denied";
    }
  }, []);

  const useWithoutCamera = useCbLW(() => { setMode("mock"); setStatus("ready"); }, []);

  const start = useCbLW(() => {
    setElapsed(0); startRef.current = Date.now(); runTick(); setStatus("recording");
    if (mode === "live" && streamRef.current && typeof MediaRecorder !== "undefined") {
      try {
        chunksRef.current = [];
        const mime = lwPickMime();
        const rec = new MediaRecorder(streamRef.current, mime ? { mimeType: mime } : undefined);
        rec.ondataavailable = (ev) => { if (ev.data && ev.data.size) chunksRef.current.push(ev.data); };
        rec.start();
        recRef.current = rec;
      } catch (e) { recRef.current = null; }
    }
  }, [mode]);

  // stop returns a promise resolving to the finished clip {url,dur,mock}
  const stop = useCbLW(() => {
    stopTick();
    const dur = (Date.now() - startRef.current) / 1000;
    return new Promise((resolve) => {
      const rec = recRef.current;
      if (rec && rec.state !== "inactive") {
        rec.onstop = () => {
          const blob = new Blob(chunksRef.current, { type: rec.mimeType || "video/webm" });
          const url = URL.createObjectURL(blob);
          const clip = { url, dur, mock: false };
          setRecordedUrl(url); setClips((c) => [...c, clip]); setStatus("recorded");
          recRef.current = null; resolve(clip);
        };
        rec.stop();
      } else {
        const clip = { url: null, dur, mock: true };
        setRecordedUrl(null); setClips((c) => [...c, clip]); setStatus("recorded");
        resolve(clip);
      }
    });
  }, []);

  const retake = useCbLW((revoke = true) => {
    // never revoke here — a take may already be adopted into segClips (review/redo)
    setRecordedUrl(null); setElapsed(0); setStatus("ready");
  }, []);

  const resetAll = useCbLW(() => {
    // clear recorder state WITHOUT revoking blob urls — saved videos keep their
    // clips in segClips/savedVideos and must stay playable after a reset
    setClips([]); setRecordedUrl(null); setElapsed(0);
    setStatus(streamRef.current || mode === "mock" ? "ready" : "idle");
  }, [clips, recordedUrl, mode]);

  return { status, mode, elapsed, recordedUrl, clips, streamRef,
           requestCamera, useWithoutCamera, start, stop, retake, resetAll, setClips };
}

const lwFmt = (s) => {
  s = Math.max(0, Math.floor(s || 0));
  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
};

// ====================================================================
// Shared primitives
// ====================================================================
const LWStar = ({ size = 20, on = "dark" }) => (
  <img src="assets/icons/icon_ai.svg" alt="" style={{
    width: size, height: size,
    filter: on === "light" ? "brightness(0) invert(1)" : "none",
  }} />
);

// Live camera / recorded playback / fallback poster — the video layer.
// Flows lay their own chrome on top. `mirror` flips the live preview.
function LWCamera({ rec, mirror = true, radius = 0, poster = LW_PO.photo, fit = "cover", children }) {
  const liveRef = useRefLW(null);
  const { status, mode, recordedUrl, streamRef } = rec;
  const showingPlayback = status === "recorded";
  const showingLive = (status === "ready" || status === "recording") && mode === "live";

  useEffectLW(() => {
    if (showingLive && liveRef.current && streamRef.current) {
      liveRef.current.srcObject = streamRef.current;
      liveRef.current.play().catch(() => {});
    }
  }, [showingLive]);

  return (
    <div className="lw-stage" style={{
      position: "absolute", inset: 0, borderRadius: radius, overflow: "hidden",
      background: "#0c160c",
    }}>
      {/* Poster (idle / mock / mock-playback) */}
      {(!showingLive && !(showingPlayback && recordedUrl)) && (
        <img src={poster} alt="" style={{
          position: "absolute", inset: 0, width: "100%", height: "100%",
          objectFit: fit, filter: status === "recording" ? "none" : "saturate(.92)",
          opacity: status === "idle" || status === "requesting" ? 0.5 : 0.92,
        }} />
      )}
      {/* Live preview */}
      {showingLive && (
        <video ref={liveRef} muted playsInline style={{
          position: "absolute", inset: 0, width: "100%", height: "100%",
          objectFit: fit, transform: mirror ? "scaleX(-1)" : "none", background: "#0c160c",
        }} />
      )}
      {/* Recorded playback */}
      {showingPlayback && recordedUrl && (
        <video src={recordedUrl} controls playsInline style={{
          position: "absolute", inset: 0, width: "100%", height: "100%",
          objectFit: fit, background: "#0c160c",
        }} />
      )}
      {children}
    </div>
  );
}

// Big red shutter — toggles record/stop. Pulsing ring while recording.
function RecordButton({ recording, onClick, size = 84, label }) {
  return (
    <button onClick={onClick} className="lw-press" aria-label={recording ? "Stop recording" : "Start recording"}
      style={{
        position: "relative", width: size, height: size, borderRadius: "50%",
        background: "rgba(254,252,245,0.14)", border: "none", cursor: "pointer",
        display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
      }}>
      {recording && <span style={{
        position: "absolute", inset: 0, borderRadius: "50%",
        border: "2px solid rgba(232,7,7,.55)", animation: "lwRing 1.4s ease-out infinite",
      }} />}
      <span style={{
        width: recording ? size * 0.34 : size * 0.74,
        height: recording ? size * 0.34 : size * 0.74,
        borderRadius: recording ? Math.round(size * 0.10) : "50%",
        background: "rgb(232,7,7)", transition: "all .22s cubic-bezier(.3,.8,.3,1)",
        boxShadow: "0 0 0 4px rgba(254,252,245,.9)",
      }} />
      {label && <span style={{
        position: "absolute", top: "calc(100% + 10px)", fontSize: 13, fontWeight: 600,
        color: "var(--helm-white)", whiteSpace: "nowrap", letterSpacing: ".01em",
      }}>{label}</span>}
    </button>
  );
}

// REC pill + timer
const RecBadge = ({ elapsed }) => (
  <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 14px 7px 12px",
    borderRadius: 999, background: "rgba(12,22,12,.62)", backdropFilter: "blur(6px)" }}>
    <span style={{ width: 9, height: 9, borderRadius: "50%", background: "rgb(232,7,7)", animation: "lwBlink 1.1s infinite" }} />
    <span style={{ fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 600, color: "#fff",
      fontVariantNumeric: "tabular-nums", letterSpacing: ".02em" }}>{lwFmt(elapsed)}</span>
  </div>
);

// Audio-style equalizer bars (decorative, signals "live mic")
const Waveform = ({ active, color = "rgba(254,252,245,.9)", bars = 5, h = 22 }) => (
  <div style={{ display: "flex", alignItems: "center", gap: 3, height: h }}>
    {Array.from({ length: bars }).map((_, i) => (
      <span key={i} style={{
        width: 3, height: h, borderRadius: 2, background: color, transformOrigin: "center",
        animation: active ? `lwBar ${0.7 + (i % 3) * 0.22}s ${i * 0.08}s ease-in-out infinite` : "none",
        transform: active ? undefined : "scaleY(.3)", opacity: active ? 1 : .5,
      }} />
    ))}
  </div>
);

// Recipient avatar — portrait or initials chip
function Avatar({ bene, size = 44, radius }) {
  const r = radius != null ? radius : Math.round(size * 0.26);
  // Track a failed photo load so a dropped/aborted request (common when the
  // studio mounts at the same moment the camera permission prompt fires)
  // falls back to the initials chip instead of a broken-image icon.
  const [failed, setFailed] = useStateLW(false);
  useEffectLW(() => { setFailed(false); }, [bene.photo]);
  if (bene.photo && !failed) {
    return <img src={bene.photo} alt={bene.name} onError={() => setFailed(true)} style={{
      width: size, height: size, borderRadius: r, objectFit: "cover", flexShrink: 0,
      boxShadow: "0 0 0 1px rgba(0,29,0,.08)" }} />;
  }
  return (
    <div style={{ width: size, height: size, borderRadius: r, flexShrink: 0,
      background: "var(--helm-ecru)", color: "var(--helm-dark-green)",
      display: "flex", alignItems: "center", justifyContent: "center",
      fontFamily: "var(--font-serif)", fontWeight: 600, fontSize: size * 0.36,
      boxShadow: `inset 0 0 0 1.5px ${bene.accent}55` }}>
      {bene.initials}
    </div>
  );
}

// Buttons — deep-purple primary (relationship moments), ghost secondary
function PrimaryBtn({ children, onClick, accent = "var(--helm-dark-purple)", full, style, disabled }) {
  return (
    <button onClick={onClick} disabled={disabled} className="lw-press" style={{
      display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 10,
      padding: "15px 26px", borderRadius: 999, border: "none",
      background: disabled ? "var(--helm-dark-ecru)" : accent,
      color: disabled ? "var(--helm-dark-grey)" : "var(--helm-white)",
      fontFamily: "var(--font-sans)", fontWeight: 600, fontSize: 16,
      cursor: disabled ? "default" : "pointer", width: full ? "100%" : "auto",
      boxShadow: disabled ? "none" : "0 1px 0 rgba(0,29,0,.12)", ...style,
    }}>{children}</button>
  );
}
function GhostBtn({ children, onClick, full, style }) {
  return (
    <button onClick={onClick} className="lw-press" style={{
      display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8,
      padding: "14px 24px", borderRadius: 999, background: "transparent",
      border: "1px solid var(--helm-dark-green)", color: "var(--helm-dark-green)",
      fontFamily: "var(--font-sans)", fontWeight: 600, fontSize: 15, cursor: "pointer",
      width: full ? "100%" : "auto", ...style,
    }}>{children}</button>
  );
}

// Helm top chrome for the brand-moment screens
function LWChrome({ title, sub, onClose, tint = "#001D00" }) {
  return (
    <div style={{ position: "absolute", top: 0, left: 0, right: 0, height: 60, zIndex: 6,
      display: "flex", alignItems: "center", justifyContent: "space-between",
      padding: "0 28px", background: tint, color: "var(--helm-white)" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
        <img src="assets/logo/helm-logotype-lemon.svg" alt="Helm" style={{ height: 20 }} />
        {title && <>
          <span style={{ width: 1, height: 20, background: "rgba(254,252,245,.28)" }} />
          <span style={{ fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 500, opacity: .92 }}>{title}</span>
        </>}
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 18 }}>
        {sub && <span style={{ fontSize: 13, opacity: .6, fontFamily: "var(--font-sans)" }}>{sub}</span>}
        {onClose && <button onClick={onClose} className="lw-press" aria-label="Close" style={{
          background: "none", border: "none", color: "rgba(254,252,245,.7)", cursor: "pointer",
          width: 28, height: 28, borderRadius: 14, fontSize: 20, lineHeight: 1 }}>×</button>}
      </div>
    </div>
  );
}

// Purple brand-moment background with the Helm wave
function WaveBg({ children, base = "var(--helm-dark-purple)", style }) {
  return (
    <div style={{ position: "absolute", inset: 0, overflow: "hidden", background: base, ...style }}>
      <img src="lw-assets/wave.svg" alt="" style={{
        position: "absolute", left: "50%", top: 0, height: "100%", transform: "translateX(-50%)",
        minWidth: "100%", objectFit: "cover", opacity: .9, pointerEvents: "none" }} />
      {children}
    </div>
  );
}

// Theme-aware "what good looks like" tips card
const LW_TIPS = [
  "Look at the lens, not yourself — it feels like eye contact to them.",
  "Start by saying their name. It makes the whole thing land softer.",
  "You can pause, laugh, or tear up. Keep rolling — it's real, and that's the point.",
  "Two minutes is plenty. Short and honest beats long and polished.",
];

// Example-clip player — plays the transcript as captions over a poster.
function ExampleClip({ ex, onClose }) {
  const [i, setI] = useStateLW(-1);
  const [playing, setPlaying] = useStateLW(false);
  const tRef = useRefLW(null);
  const per = Math.max(2.4, ex.dur / ex.lines.length);

  const play = () => {
    setPlaying(true); setI(0);
    let k = 0;
    tRef.current = setInterval(() => {
      k += 1;
      if (k >= ex.lines.length) { clearInterval(tRef.current); setPlaying(false); return; }
      setI(k);
    }, per * 1000);
  };
  useEffectLW(() => () => tRef.current && clearInterval(tRef.current), []);
  const pct = i < 0 ? 0 : Math.min(100, ((i + 1) / ex.lines.length) * 100);

  return (
    <div onClick={onClose} style={{ position: "absolute", inset: 0, zIndex: 120,
      background: "rgba(12,18,12,.55)", backdropFilter: "blur(8px)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 40 }} className="lw-fade-plain">
      <div onClick={(e) => e.stopPropagation()} style={{
        width: 720, maxWidth: "100%", borderRadius: 24, overflow: "hidden",
        background: "#0c160c", boxShadow: "0 24px 80px rgba(0,0,0,.5)", position: "relative" }}>
        <div style={{ position: "relative", aspectRatio: "16/10", overflow: "hidden" }}>
          <img src={ex.poster} alt="" style={{ position: "absolute", inset: 0, width: "100%", height: "100%",
            objectFit: "cover", filter: "saturate(.9) brightness(.82)" }} />
          {/* caption */}
          <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, padding: "120px 40px 36px",
            background: "linear-gradient(transparent, rgba(8,14,8,.86))" }}>
            <div style={{ minHeight: 96 }}>
              {i >= 0 && <p key={i} className="lw-fade" style={{ margin: 0, color: "#fdfdf4",
                fontFamily: "var(--font-serif)", fontSize: 26, lineHeight: "36px", letterSpacing: "-.01em",
                textShadow: "0 1px 12px rgba(0,0,0,.5)" }}>{ex.lines[i]}</p>}
              {i < 0 && <p style={{ margin: 0, color: "rgba(253,253,244,.8)", fontFamily: "var(--font-sans)",
                fontSize: 16 }}>An example from someone in a similar moment.</p>}
            </div>
          </div>
          {/* play overlay */}
          {!playing && i < 0 && (
            <button onClick={play} className="lw-press" aria-label="Play example" style={{
              position: "absolute", inset: 0, margin: "auto", width: 76, height: 76, borderRadius: "50%",
              border: "none", cursor: "pointer", background: "rgba(254,252,245,.94)",
              display: "flex", alignItems: "center", justifyContent: "center" }}>
              <svg width="26" height="26" viewBox="0 0 24 24" fill="#001D00"><path d="M7 4.5v15l13-7.5z" /></svg>
            </button>
          )}
          {/* replay */}
          {!playing && i >= 0 && (
            <button onClick={play} className="lw-press" style={{ position: "absolute", top: 16, right: 16,
              border: "none", cursor: "pointer", background: "rgba(12,18,12,.6)", color: "#fff",
              borderRadius: 999, padding: "8px 14px", fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 600,
              display: "flex", alignItems: "center", gap: 6 }}>
              <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M3 8a5 5 0 1 0 1.5-3.5"/><path d="M3 2v3h3"/></svg>
              Replay
            </button>
          )}
        </div>
        {/* progress + meta */}
        <div style={{ height: 4, background: "rgba(254,252,245,.14)" }}>
          <div style={{ height: "100%", width: pct + "%", background: "var(--helm-lemon)", transition: "width .4s linear" }} />
        </div>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "16px 22px",
          background: "#0c160c", color: "rgba(254,252,245,.85)", fontFamily: "var(--font-sans)" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
            <span style={{ fontSize: 14, fontWeight: 600 }}>{ex.who}</span>
            <span style={{ fontSize: 13, opacity: .55 }}>· {ex.theme}</span>
          </div>
          <button onClick={onClose} style={{ background: "none", border: "none", color: "rgba(254,252,245,.7)",
            cursor: "pointer", fontSize: 14, fontWeight: 600, fontFamily: "var(--font-sans)" }}>Close</button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  LW_PO, LW_BENES, lwBene, LW_THEMES, lwTheme, lwPrompts, LW_EXAMPLES, LW_TIPS,
  useRecorder, lwFmt, LWCamera, RecordButton, RecBadge, Waveform, Avatar,
  PrimaryBtn, GhostBtn, LWChrome, WaveBg, LWStar, ExampleClip,
  lwMonitorAudioLevel,
});
