/* eslint-disable */
/* ============================================================
   Shared components — icons, player, chapters, transcript, notes
   ============================================================ */

const { useState, useEffect, useRef, useMemo } = React;

// Robust clipboard copy — falls back to execCommand trick for iframes, older browsers, etc.
async function copyToClipboard(text) {
  try {
    if (navigator.clipboard && window.isSecureContext) {
      await navigator.clipboard.writeText(text);
      return true;
    }
  } catch (e) { /* fall through */ }
  try {
    const ta = document.createElement("textarea");
    ta.value = text;
    ta.style.position = "fixed";
    ta.style.top = "-9999px";
    ta.style.left = "-9999px";
    ta.setAttribute("readonly", "");
    document.body.appendChild(ta);
    ta.select();
    ta.setSelectionRange(0, ta.value.length);
    const ok = document.execCommand("copy");
    document.body.removeChild(ta);
    return ok;
  } catch (e) { return false; }
}
window.copyToClipboard = copyToClipboard;

/* ---------- Icons (inline SVG, Lucide-style rounded) ---------- */
const Icon = ({ name, size = 20, stroke = 1.75, className = "" }) => {
  const paths = {
    play: <path d="M7 5v14l12-7z" fill="currentColor" stroke="none" />,
    pause: <g><rect x="6" y="5" width="4" height="14" fill="currentColor" stroke="none" rx="1" /><rect x="14" y="5" width="4" height="14" fill="currentColor" stroke="none" rx="1" /></g>,
    skipBack: <g><polygon points="19,5 8,12 19,19" fill="currentColor" stroke="none" /><rect x="5" y="5" width="2" height="14" fill="currentColor" stroke="none" /></g>,
    skipFwd: <g><polygon points="5,5 16,12 5,19" fill="currentColor" stroke="none" /><rect x="17" y="5" width="2" height="14" fill="currentColor" stroke="none" /></g>,
    back15: <g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8" /><polyline points="3 3 3 8 8 8" /></g>,
    fwd30: <g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7L21 8" /><polyline points="21 3 21 8 16 8" /></g>,
    heart: <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" fill="none" />,
    share: <g fill="none"><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /><line x1="15.41" y1="6.51" x2="8.59" y2="10.49" /></g>,
    download: <g fill="none"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></g>,
    bookmark: <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" fill="none" />,
    chevronRight: <polyline points="9 18 15 12 9 6" fill="none" />,
    chevronDown: <polyline points="6 9 12 15 18 9" fill="none" />,
    chevronUp: <polyline points="18 15 12 9 6 15" fill="none" />,
    menu: <g fill="none"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></g>,
    search: <g fill="none"><circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></g>,
    arrowUpRight: <g fill="none"><line x1="7" y1="17" x2="17" y2="7" /><polyline points="7 7 17 7 17 17" /></g>,
    sliders: <g fill="none"><line x1="4" y1="21" x2="4" y2="14" /><line x1="4" y1="10" x2="4" y2="3" /><line x1="12" y1="21" x2="12" y2="12" /><line x1="12" y1="8" x2="12" y2="3" /><line x1="20" y1="21" x2="20" y2="16" /><line x1="20" y1="12" x2="20" y2="3" /><line x1="1" y1="14" x2="7" y2="14" /><line x1="9" y1="8" x2="15" y2="8" /><line x1="17" y1="16" x2="23" y2="16" /></g>,
    x: <g fill="none"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></g>,
    mic: <g fill="none"><rect x="9" y="2" width="6" height="12" rx="3" /><path d="M19 10v2a7 7 0 0 1-14 0v-2" /><line x1="12" y1="19" x2="12" y2="23" /><line x1="8" y1="23" x2="16" y2="23" /></g>,
    rss: <g fill="none"><path d="M4 11a9 9 0 0 1 9 9" /><path d="M4 4a16 16 0 0 1 16 16" /><circle cx="5" cy="19" r="1.5" fill="currentColor" stroke="none" /></g>,
    mail: <g fill="none"><rect x="3" y="5" width="18" height="14" rx="2" /><polyline points="3 7 12 13 21 7" /></g>,
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round" className={className}>
      {paths[name]}
    </svg>
  );
};

/* ---------- Top Nav ---------- */
const TopNav = ({ onOpenSearch, onOpenAsk, onOpenSubscribe }) => {
  const [menuOpen, setMenuOpen] = useState(false);

  // Close on resize to desktop
  useEffect(() => {
    const onResize = () => { if (window.innerWidth > 900) setMenuOpen(false); };
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  // Lock body scroll while menu is open
  useEffect(() => {
    if (menuOpen) {
      const prev = document.body.style.overflow;
      document.body.style.overflow = "hidden";
      return () => { document.body.style.overflow = prev; };
    }
  }, [menuOpen]);

  const scrollToTopics = () => {
    setMenuOpen(false);
    setTimeout(() => {
      const target = document.querySelector('.topics-grid')?.closest('section');
      if (target) {
        const rect = target.getBoundingClientRect();
        window.scrollTo({ top: window.scrollY + rect.top - 80, behavior: 'smooth' });
      }
    }, 60);
  };

  return (
    <>
    <nav className="topnav">
      <div className="topnav-inner">
        <div className="topnav-brand">
          <div className="topnav-mark">
            <span className="topnav-dot" />
            The Hole Shebang
          </div>
          <span className="topnav-umbrella">a Blueberry Therapy podcast</span>
        </div>
        <div className="topnav-links">
          <a href="https://www.blueberrytherapy.ca/" className="topnav-back" target="_top" rel="noopener">
            <span aria-hidden="true" style={{ marginRight: 4 }}>←</span> Blueberry Therapy
          </a>
          <a href="#topics" onClick={(e) => { e.preventDefault(); scrollToTopics(); }}>Topics</a>
          <a href="#" className="topnav-ask" onClick={(e) => { e.preventDefault(); onOpenAsk && onOpenAsk(); }}>Ask a question</a>
          <a href="https://blueberrytherapyshop.ca/" target="_blank" rel="noopener noreferrer">Shop</a>
        </div>
        <div className="topnav-actions">
          <button className="btn btn-ghost btn-sm topnav-search" onClick={onOpenSearch} aria-label="Search">
            <Icon name="search" size={14} /> <span className="topnav-btn-label">Search</span>
          </button>
          <button className="btn btn-primary btn-sm topnav-subscribe" onClick={onOpenSubscribe} aria-label="Subscribe">
            <Icon name="rss" size={14} /> <span className="topnav-btn-label">Subscribe</span>
          </button>
          <button
            className="topnav-menu-btn"
            onClick={() => setMenuOpen(v => !v)}
            aria-label={menuOpen ? "Close menu" : "Open menu"}
            aria-expanded={menuOpen}
          >
            <Icon name={menuOpen ? "x" : "menu"} size={20} />
          </button>
        </div>
      </div>
    </nav>

    {/* Mobile menu drawer — rendered outside the sticky topnav so it escapes the topnav's stacking context */}
    <div className={`topnav-mobile-menu ${menuOpen ? "open" : ""}`} role="menu">
        <a href="https://www.blueberrytherapy.ca/" className="topnav-back-mobile" target="_top" rel="noopener" onClick={() => setMenuOpen(false)} role="menuitem">
          <span aria-hidden="true" style={{ marginRight: 6 }}>←</span> Blueberry Therapy
        </a>
        <a href="#topics" onClick={(e) => { e.preventDefault(); scrollToTopics(); }} role="menuitem">
          Topics
        </a>
        <a href="#" className="topnav-ask-mobile" onClick={(e) => { e.preventDefault(); setMenuOpen(false); onOpenAsk && onOpenAsk(); }} role="menuitem">
          Ask a question
        </a>
        <a href="https://blueberrytherapyshop.ca/" target="_blank" rel="noopener noreferrer" onClick={() => setMenuOpen(false)} role="menuitem">
          Shop
        </a>
        <div className="topnav-mobile-divider" />
        <a href="#" onClick={(e) => { e.preventDefault(); setMenuOpen(false); onOpenSearch && onOpenSearch(); }} role="menuitem">
          <Icon name="search" size={16} /> Search episodes
        </a>
        <a href="#" onClick={(e) => { e.preventDefault(); setMenuOpen(false); onOpenSubscribe && onOpenSubscribe(); }} role="menuitem">
          <Icon name="rss" size={16} /> Subscribe
        </a>
      </div>
      {menuOpen && <div className="topnav-mobile-backdrop" onClick={() => setMenuOpen(false)} />}
    </>
  );
};

/* ---------- Format seconds as mm:ss ---------- */
const fmt = (s) => {
  const m = Math.floor(s / 60);
  const r = Math.floor(s % 60);
  return `${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}`;
};

/* ---------- Ask a Question modal ---------- */
const AskQuestionModal = ({ open, onClose, onToast }) => {
  const [stage, setStage] = React.useState("form"); // form | sent
  const [question, setQuestion] = React.useState("");
  const [name, setName] = React.useState("");
  const [email, setEmail] = React.useState("");
  const [anon, setAnon] = React.useState(true);
  const [topic, setTopic] = React.useState("");
  const [sending, setSending] = React.useState(false);
  const [errorMsg, setErrorMsg] = React.useState("");

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  React.useEffect(() => {
    if (open) {
      setStage("form");
      setErrorMsg("");
      setSending(false);
      document.body.style.overflow = "hidden";
    } else {
      document.body.style.overflow = "";
    }
    return () => { document.body.style.overflow = ""; };
  }, [open]);

  if (!open) return null;

  const submit = async (e) => {
    e.preventDefault();
    setErrorMsg("");
    const workerUrl = window.WORKER_URL;
    if (!workerUrl) {
      setErrorMsg("Form endpoint not configured.");
      return;
    }
    setSending(true);
    try {
      const res = await fetch(`${workerUrl}/submit-question`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          question, topic, name, email, anonymous: anon,
          website: "", // honeypot
        }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        throw new Error(data.error || `Submission failed (${res.status})`);
      }
      setStage("sent");
      onToast && onToast("Question submitted — thank you");
    } catch (err) {
      setErrorMsg(err.message || "Something went wrong. Please try again.");
    } finally {
      setSending(false);
    }
  };

  return (
    <div className="ask-overlay" onClick={(e) => { if (e.target.classList.contains("ask-overlay")) onClose(); }}>
      <div className="ask-modal" role="dialog" aria-modal="true" aria-labelledby="ask-title">
        <button className="ask-close" onClick={onClose} aria-label="Close">×</button>
        {stage === "form" ? (
          <>
            <div className="ask-eyebrow">Ask a question</div>
            <h2 id="ask-title" className="ask-title">What have you been afraid to Google?</h2>
            <p className="ask-lead">
              Kristen reads every submission. Some become full episodes. Some get answered in the Friday newsletter. Nothing gets dismissed.
            </p>
            <form className="ask-form" onSubmit={submit}>
              <label className="ask-field">
                <span className="ask-label">Your question</span>
                <textarea
                  required
                  maxLength={1200}
                  rows={5}
                  placeholder="Be as direct as you want. Medical words are welcome here."
                  value={question}
                  onChange={(e) => setQuestion(e.target.value)}
                />
                <span className="ask-count">{question.length} / 1200</span>
              </label>

              <label className="ask-field">
                <span className="ask-label">Topic (optional)</span>
                <select value={topic} onChange={(e) => setTopic(e.target.value)}>
                  <option value="">Pick one…</option>
                  {(window.TOPICS || []).map(t => <option key={t.name} value={t.name}>{t.name}</option>)}
                  <option value="Other">Something else</option>
                </select>
              </label>

              <div className="ask-row">
                <label className="ask-field">
                  <span className="ask-label">Name {anon && <em style={{ fontWeight: 400, color: "var(--ink-soft)" }}>— kept private</em>}</span>
                  <input
                    type="text"
                    placeholder={anon ? "First name or a pseudonym" : "Your name"}
                    value={name}
                    onChange={(e) => setName(e.target.value)}
                  />
                </label>
                <label className="ask-field">
                  <span className="ask-label">Email (optional)</span>
                  <input
                    type="email"
                    placeholder="So Kristen can reply if needed"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                  />
                </label>
              </div>

              <label className="ask-check">
                <input type="checkbox" checked={anon} onChange={(e) => setAnon(e.target.checked)} />
                <span>Keep me anonymous if this becomes an episode</span>
              </label>

              <p className="ask-fineprint">
                This is not a medical appointment. For personalized care, <a href="https://blueberrytherapy.janeapp.com/" target="_blank" rel="noopener noreferrer">book at Blueberry Therapy</a>.
              </p>

              <input type="text" name="website" tabIndex={-1} autoComplete="off" style={{ position: "absolute", left: "-9999px", width: "1px", height: "1px", opacity: 0 }} aria-hidden="true" />

              {errorMsg && <p style={{ color: "#c0392b", fontFamily: "'Lora', serif", fontSize: 14, margin: 0 }}>{errorMsg}</p>}

              <div className="ask-actions">
                <button type="button" className="btn btn-ghost" onClick={onClose} disabled={sending}>Cancel</button>
                <button type="submit" className="btn btn-primary" disabled={sending}>
                  {sending ? "Sending…" : "Send question"}
                </button>
              </div>
            </form>
          </>
        ) : (
          <div className="ask-sent">
            <div className="ask-sent-mark">✓</div>
            <h2 className="ask-title">Question received.</h2>
            <p className="ask-lead">
              Thank you. Every question you send makes this podcast a little braver, and helps someone else feel less alone in theirs. Kristen reads every one.
            </p>
            <div className="ask-actions">
              <button className="btn btn-primary" onClick={onClose}>Close</button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

/* ---------- Big Player (with real interactions) ---------- */
const Player = ({ state, dispatch, variant = "rounded" }) => {
  const { isPlaying, time, duration, speed } = state;
  const barRef = useRef(null);
  const { activeEpisode } = React.useContext(window.ActiveEpisodeContext);

  const pct = (time / duration) * 100;

  const onSeek = (e) => {
    const rect = barRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const ratio = Math.max(0, Math.min(1, x / rect.width));
    dispatch({ type: "seek", time: ratio * duration });
  };

  return (
    <div className="player" style={variant === "square" ? { borderRadius: 0 } : {}}>
      <div className="player-art">
        <span className="art-ep">{(activeEpisode?.number) || CURRENT_EPISODE.number}</span>
      </div>
      <div className="player-main">
        <div className="player-eyebrow">Now playing · S{activeEpisode?.seasonNum || CURRENT_EPISODE.season}E{String(activeEpisode?.number || CURRENT_EPISODE.number).padStart(3, "0")}</div>
        <div className="player-title">{activeEpisode?.title || CURRENT_EPISODE.title}</div>
        <div className="player-progress">
          <span className="progress-time">{fmt(time)}</span>
          <div className="progress-bar" ref={barRef} onClick={onSeek}>
            <div className="progress-fill" style={{ width: `${pct}%` }} />
            <div className="progress-thumb" style={{ left: `${pct}%` }} />
          </div>
          <span className="progress-time">-{fmt(duration - time)}</span>
        </div>
      </div>
      <div className="player-controls">
        <button className="ctrl-btn" onClick={() => dispatch({ type: "skip", delta: -15 })} aria-label="Back 15s">
          <Icon name="back15" size={18} />
        </button>
        <button className="ctrl-btn play" onClick={() => dispatch({ type: "toggle" })} aria-label={isPlaying ? "Pause" : "Play"}>
          <Icon name={isPlaying ? "pause" : "play"} size={22} />
        </button>
        <button className="ctrl-btn" onClick={() => dispatch({ type: "skip", delta: 30 })} aria-label="Forward 30s">
          <Icon name="fwd30" size={18} />
        </button>
        <div className="ctrl-extra">
          <button onClick={() => dispatch({ type: "cycleSpeed" })} className={speed !== 1 ? "active" : ""}>{speed}x</button>
          <button onClick={() => dispatch({ type: "toggleBookmark" })} className={state.bookmarked ? "active" : ""}>
            {state.bookmarked ? "★ saved" : "☆ save"}
          </button>
        </div>
      </div>
    </div>
  );
};

/* ---------- Chapters ---------- */
const ChapterList = ({ state, dispatch }) => (
  <div className="chapter-list">
    {CHAPTERS.map((c, i) => {
      const nextSec = CHAPTERS[i + 1]?.seconds ?? state.duration;
      const isActive = state.time >= c.seconds && state.time < nextSec;
      return (
        <div key={c.id} className={`chapter ${isActive ? "active" : ""}`} onClick={() => dispatch({ type: "seek", time: c.seconds })}>
          <div className="chapter-time">{c.time}</div>
          <div>
            <div className="chapter-title">{c.title}</div>
            <div className="chapter-desc">{c.desc}</div>
          </div>
          <div className="chapter-icon">
            <Icon name={isActive && state.isPlaying ? "pause" : "play"} size={16} />
          </div>
        </div>
      );
    })}
  </div>
);

/* ---------- Transcript ---------- */
// Parse WebVTT into [{seconds, time, text}] cues
function parseVtt(text) {
  const lines = text.replace(/\r/g, "").split("\n");
  const cues = [];
  let i = 0;
  while (i < lines.length) {
    const l = lines[i];
    const m = l && l.match(/(\d{1,2}):(\d{2}):(\d{2})\.\d+\s+-->/);
    if (m) {
      const h = +m[1], mm = +m[2], ss = +m[3];
      const seconds = h * 3600 + mm * 60 + ss;
      const time = h > 0 ? `${h}:${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}` : `${mm}:${String(ss).padStart(2, "0")}`;
      i++;
      const parts = [];
      while (i < lines.length && lines[i].trim() !== "") {
        parts.push(lines[i]);
        i++;
      }
      const raw = parts.join(" ").replace(/<[^>]+>/g, "").trim();
      // Detect "SPEAKER: text" format
      const speakerMatch = raw.match(/^([A-Z][A-Z0-9 .'-]{1,20}):\s*(.+)/);
      cues.push({
        seconds,
        time,
        speaker: speakerMatch ? speakerMatch[1].trim() : "",
        text: speakerMatch ? speakerMatch[2] : raw,
      });
    }
    i++;
  }
  return cues;
}

const WORKER_BASE = (typeof window !== "undefined" && window.WORKER_URL) || "";

const Transcript = ({ state, dispatch, episode }) => {
  const [cues, setCues] = useState(null);
  const [status, setStatus] = useState("idle"); // idle | loading | ready | missing | error
  const transcriptUrl = episode?.transcriptUrl;

  useEffect(() => {
    if (!transcriptUrl) { setCues(null); setStatus(episode ? "missing" : "idle"); return; }
    // Read WORKER_URL at call time (not module-load time — script load order can race)
    const base = (typeof window !== "undefined" && window.WORKER_URL) || "";
    if (!base) { setStatus("error"); return; }
    setStatus("loading");
    fetch(`${base}/transcript?url=${encodeURIComponent(transcriptUrl)}`)
      .then(r => r.ok ? r.text() : Promise.reject(r.status))
      .then(text => { setCues(parseVtt(text)); setStatus("ready"); })
      .catch(() => setStatus("error"));
  }, [transcriptUrl]);

  // Fallback to hand-written transcript for the default episode 047
  const useFallback = !episode;
  const data = useFallback ? TRANSCRIPT : (cues || []);

  if (status === "loading") {
    return <div className="transcript" style={{ padding: 32, color: "var(--ink-soft)", fontStyle: "italic" }}>Loading transcript…</div>;
  }
  if (status === "missing") {
    return (
      <div className="transcript" style={{ padding: 32, color: "var(--ink-soft)" }}>
        <p style={{ fontStyle: "italic", marginBottom: 8 }}>Transcript coming soon for this episode.</p>
        <p style={{ fontSize: 14 }}>We're working our way through the back catalogue — new episodes ship with transcripts from here on out.</p>
      </div>
    );
  }
  if (status === "error") {
    return <div className="transcript" style={{ padding: 32, color: "var(--ink-soft)", fontStyle: "italic" }}>Transcript couldn't be loaded. Try refreshing.</div>;
  }

  return (
    <div className="transcript">
      {data.map((line, i) => {
        const nextSec = data[i + 1]?.seconds ?? state.duration;
        const isActive = state.time >= line.seconds && state.time < nextSec;
        return (
          <div key={i} className={`transcript-line ${isActive ? "active" : ""}`} onClick={() => dispatch({ type: "seek", time: line.seconds })}>
            <div className="transcript-time">{line.time}</div>
            <div>
              {line.speaker && <span className="transcript-speaker">{line.speaker}</span>}
              <span className="transcript-text">{line.text}</span>
            </div>
          </div>
        );
      })}
    </div>
  );
};

/* ---------- Notes body (reusable) ---------- */
const NotesBody = ({ episode }) => (
  <div className="notes-body">
    {episode ? (
      <>
        <p style={{ fontSize: "20px", fontStyle: "italic", color: "var(--ink-soft)" }}>
          Episode {String(episode.number).padStart(3, "0")} · {episode.duration} · Aired {episode.dateLong || episode.date}
        </p>

        <h3>About this episode</h3>
        {episode.descriptionHtml ? (
          <div className="show-notes-html" dangerouslySetInnerHTML={{ __html: episode.descriptionHtml }} />
        ) : episode.fullDescription ? (
          episode.fullDescription.split(/\n\n+/).map((para, i) => (
            <p key={i} style={{ whiteSpace: "pre-wrap" }}>{para}</p>
          ))
        ) : (
          <p>{episode.sub}</p>
        )}
      </>
    ) : (
      <>
        <p style={{ fontSize: "20px", fontStyle: "italic", color: "var(--ink-soft)" }}>
          There are two things people say after their first real pelvic floor appointment. The first is, "why did nobody tell me this." The second is usually a swear word.
        </p>

        <h3>About this episode</h3>
        <p>
          Kristen has been assessing pelvic floors for twenty-five years. In this episode we throw out the kegel myth, walk through what the pelvic floor actually is, and answer the question a lot of people quietly Google at night: is it normal to leak a little when I laugh? (Short answer: common, yes. Normal, no.)
        </p>

        <div className="pull-quote serif-italic">
          A tight pelvic floor is not a strong pelvic floor. Half the people I see with leaks need to learn how to let go, not clench harder.
          <cite>— Kristen Parise, 24:05</cite>
        </div>

        <h3>What we cover</h3>
        <ul>
          <li>The hammock analogy everyone understands in about four seconds.</li>
          <li>Why the "just do kegels" advice is not only wrong, it makes some people worse.</li>
          <li>How pregnancy, birth, and perimenopause each change the muscle in different ways.</li>
          <li>What a pelvic floor assessment actually involves — consent-first, no surprises.</li>
          <li>One small thing you can try this week that is not a kegel.</li>
        </ul>

        <div className="callout">
          <div className="callout-label">Client-friendly tip · 47:02</div>
          <div className="callout-body">
            Before you pee, take one slow exhale through slightly pursed lips. This nudges your pelvic floor into a relaxed position so you are not fighting your own muscles on the way out.
          </div>
        </div>

        <h3>A note on language</h3>
        <p>
          We use words like vagina, vulva, orgasm, and incontinence because those are the medical realities. If you find this episode helpful, the most useful thing you can do is send it to one person who has been sitting on a question.
        </p>
      </>
    )}
  </div>
);

/* ---------- Episode sidebar (reusable) ---------- */
const EpisodeSidebar = ({ onToast, episode, episodes, sticky = false }) => {
  const ctx = React.useContext(window.ActiveEpisodeContext);
  const playEpisode = ctx?.playEpisode;
  const relatedEp = React.useMemo(() => {
    const list = (episodes && episodes.length) ? episodes : [];
    if (!list.length || !episode) return null;
    const curNum = episode.number;
    const pool = list.filter(e => e.num !== String(curNum).padStart(3, "0") && e.num !== String(curNum));
    if (!pool.length) return null;
    const TOPICS = window.TOPICS || [];
    const matches = window.episodeMatchesTopic;
    if (!matches) return pool[(Number(curNum) || 0) % pool.length];
    const curTitleOnly = { title: episode.title, sub: episode.subtitle };
    const curFull = { title: episode.title, sub: episode.subtitle, fullDescription: episode.fullDescription || episode.description };
    const scored = TOPICS.map(t => ({
      topic: t,
      score: matches(curTitleOnly, t) ? 2 : matches(curFull, t) ? 1 : 0,
    })).filter(s => s.score > 0).sort((a, b) => b.score - a.score);
    for (const { topic } of scored) {
      const strongCandidates = pool.filter(e => matches({ title: e.title, sub: e.sub }, topic));
      if (strongCandidates.length) return strongCandidates[(Number(curNum) || 0) % strongCandidates.length];
      const anyCandidates = pool.filter(e => matches(e, topic));
      if (anyCandidates.length) return anyCandidates[(Number(curNum) || 0) % anyCandidates.length];
    }
    return pool[(Number(curNum) || 0) % pool.length];
  }, [episode, episodes]);

  return (
    <aside className={sticky ? "episode-sidebar sticky" : "episode-sidebar"}>
      <div className="sidebar-card">
        <h4>On the mic</h4>
        <div className="guest">
          <img
            src="assets/kristen-mat.jpg"
            alt="Kristen Parise"
            className="guest-avatar"
            style={{ borderRadius: "50%", width: 64, height: 64, objectFit: "cover", objectPosition: "50% 15%" }}
          />
          <div>
            <div className="guest-name">Kristen Parise</div>
            <div className="guest-role">Pelvic health physiotherapist</div>
          </div>
        </div>
        <p>Twenty-five years in practice. Owns Blueberry Therapy in Dundas, Ontario. Told to whisper, she chose to shout.</p>
        <a className="btn btn-ghost btn-sm" href="https://www.blueberrytherapy.ca/kristen-parise" target="_blank" rel="noopener noreferrer">Read bio <Icon name="arrowUpRight" size={12} /></a>
      </div>

      <div className="sidebar-card">
        <h4>Links referenced</h4>
        <ul className="link-list">
          <li><a href="http://pelvicfloorguide.blueberrytherapy.ca/" target="_blank" rel="noopener noreferrer">5 Things Nobody Tells You About Your Pelvic Floor &amp; Sex</a><span className="link-type">Guide</span></li>
          <li><a href="https://blueberrytherapy.janeapp.com/" target="_blank" rel="noopener noreferrer">Booking at Blueberry Therapy</a><span className="link-type">Clinic</span></li>
          <li><a href="https://blueberrytherapyshop.ca/" target="_blank" rel="noopener noreferrer">Shop at Blueberry Therapy</a><span className="link-type">Shop</span></li>
          {relatedEp && (
            <li>
              <a href="#" onClick={(e) => {
                e.preventDefault();
                if (playEpisode) { playEpisode(relatedEp); }
                else { onToast && onToast(`Loading episode ${relatedEp.num}`); }
              }}>
                Episode {relatedEp.num}: {relatedEp.title}
              </a>
              <span className="link-type">Ep. {relatedEp.num}</span>
            </li>
          )}
        </ul>
      </div>

      <div className="sidebar-card">
        <h4>Share this episode</h4>
        <div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
          <button className="btn btn-ghost btn-sm" onClick={async () => {
            const ok = await copyToClipboard(window.location.href);
            onToast(ok ? "Link copied" : "Couldn't copy — try long-pressing the address bar");
          }}><Icon name="share" size={12} /> Copy link</button>
          {episode?.audioUrl && (
            <a
              className="btn btn-ghost btn-sm"
              href={episode.audioUrl}
              download={`HoleShebang-Ep${String(episode.number || "").padStart(3, "0")}.mp3`}
              target="_blank"
              rel="noopener noreferrer"
              onClick={() => onToast && onToast("Opening episode audio")}
              style={{ textDecoration: "none" }}
            ><Icon name="download" size={12} /> MP3</a>
          )}
        </div>
      </div>
    </aside>
  );
};

/* ---------- Show Notes (legacy, kept for back-compat) ---------- */
const ShowNotes = ({ onToast, episode, episodes }) => (
  <div className="notes-grid">
    <NotesBody episode={episode} />
    <EpisodeSidebar onToast={onToast} episode={episode} episodes={episodes} />
  </div>
);

/* ---------- Episode Details — accordion layout ---------- */
const EpisodeDetails = ({ onToast, episode, episodes, state, dispatch }) => {
  const [notesOpen, setNotesOpen] = useState(false);
  const [transcriptOpen, setTranscriptOpen] = useState(false);

  // Build a teaser — first sentence-ish of the description
  const teaser = React.useMemo(() => {
    const raw = episode?.fullDescription || episode?.description || episode?.sub || "";
    if (!raw) return "There are two things people say after their first real pelvic floor appointment. The first is, 'why did nobody tell me this.' The second is usually a swear word.";
    const text = raw.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
    // Take up to ~220 chars, break on sentence end
    if (text.length <= 220) return text;
    const slice = text.slice(0, 220);
    const lastStop = Math.max(slice.lastIndexOf(". "), slice.lastIndexOf("! "), slice.lastIndexOf("? "));
    return (lastStop > 80 ? slice.slice(0, lastStop + 1) : slice) + "…";
  }, [episode]);

  return (
    <div className="episode-details-grid">
      <div className="episode-details-main">
        {/* Teaser — always visible */}
        <div className="episode-teaser">
          <p className="episode-teaser-lead">{teaser}</p>
        </div>

        {/* Show notes accordion */}
        <div className={`accordion ${notesOpen ? "open" : ""}`}>
          <button
            className="accordion-header"
            onClick={() => setNotesOpen(v => !v)}
            aria-expanded={notesOpen}
          >
            <div className="accordion-header-text">
              <span className="accordion-eyebrow">Full show notes</span>
              {!notesOpen && (
                <span className="accordion-hint">Click to read the good stuff</span>
              )}
            </div>
            <Icon name={notesOpen ? "chevronUp" : "chevronDown"} size={18} />
          </button>
          {notesOpen && (
            <div className="accordion-body">
              <NotesBody episode={episode} />
            </div>
          )}
        </div>

        {/* Transcript accordion */}
        <div className={`accordion ${transcriptOpen ? "open" : ""}`}>
          <button
            className="accordion-header"
            onClick={() => setTranscriptOpen(v => !v)}
            aria-expanded={transcriptOpen}
          >
            <div className="accordion-header-text">
              <span className="accordion-eyebrow">Full transcript</span>
              {!transcriptOpen && (
                <span className="accordion-hint">Click to read every single word</span>
              )}
            </div>
            <Icon name={transcriptOpen ? "chevronUp" : "chevronDown"} size={18} />
          </button>
          {transcriptOpen && (
            <div className="accordion-body">
              <Transcript state={state} dispatch={dispatch} episode={episode} />
            </div>
          )}
        </div>
      </div>

      <EpisodeSidebar onToast={onToast} episode={episode} episodes={episodes} sticky={true} />
    </div>
  );
};

/* ---------- Related Episodes ---------- */
const EpisodeRow = ({ ep, onPlay }) => (
  <div className="ep-row" onClick={() => onPlay(ep)}>
    <div className={`ep-art ${ep.artColor}`}>{ep.art}</div>
    <div>
      <div className="ep-title">{ep.title}</div>
      <div className="ep-sub">{ep.sub}</div>
    </div>
    <div className="ep-dur">{ep.dur} · {ep.date}</div>
    <button className="ep-play"><Icon name="play" size={14} /></button>
  </div>
);

/* ---------- Tweaks ---------- */
const Tweaks = ({ tweaks, setTweaks, visible, onClose }) => {
  if (!visible) return null;
  return (
    <div className="tweaks-panel">
      <div className="tweaks-header">
        <h3>Tweaks</h3>
        <button onClick={onClose} style={{ background: "none", border: "none", cursor: "pointer", color: "var(--ink-soft)" }}>
          <Icon name="x" size={14} />
        </button>
      </div>

      <div className="tweak-row">
        <label className="tweak-label">Base</label>
        <div className="tweak-segmented">
          <button className={tweaks.mode === "light" ? "active" : ""} onClick={() => setTweaks({ ...tweaks, mode: "light" })}>Cream</button>
          <button className={tweaks.mode === "dark" ? "active" : ""} onClick={() => setTweaks({ ...tweaks, mode: "dark" })}>Navy</button>
        </div>
      </div>

      <div className="tweak-row">
        <label className="tweak-label">Type scale</label>
        <input type="range" min="0.85" max="1.15" step="0.05" value={tweaks.typeScale}
               className="tweak-slider"
               onChange={(e) => setTweaks({ ...tweaks, typeScale: parseFloat(e.target.value) })} />
        <div className="tweak-value">{Math.round(tweaks.typeScale * 100)}%</div>
      </div>

      <div className="tweak-row">
        <label className="tweak-label">Accent intensity</label>
        <div className="tweak-segmented">
          <button className={tweaks.accent === "soft" ? "active" : ""} onClick={() => setTweaks({ ...tweaks, accent: "soft" })}>Soft</button>
          <button className={tweaks.accent === "default" ? "active" : ""} onClick={() => setTweaks({ ...tweaks, accent: "default" })}>Default</button>
          <button className={tweaks.accent === "bold" ? "active" : ""} onClick={() => setTweaks({ ...tweaks, accent: "bold" })}>Bold</button>
        </div>
      </div>

      <div style={{ fontSize: "11px", color: "var(--ink-soft)", marginTop: "12px", lineHeight: 1.4 }}>
        Variation switcher lives on the right edge of the screen. <br/>Keyboard: <kbd style={{ fontFamily: "monospace", background: "var(--bg-alt)", padding: "1px 4px", borderRadius: 3 }}>Space</kbd> play/pause.
      </div>
    </div>
  );
};

/* ---------- Mini player (sticky footer while scrolling) ---------- */
const MiniPlayer = ({ state, dispatch }) => {
  const pct = (state.time / state.duration) * 100;
  const { activeEpisode } = React.useContext(window.ActiveEpisodeContext);
  const num = activeEpisode?.number || CURRENT_EPISODE.number;
  const title = activeEpisode?.title || CURRENT_EPISODE.title;
  return (
    <div className="mini-player">
      <div className="mini-progress"><div className="mini-progress-fill" style={{ width: `${pct}%` }} /></div>
      <div className="mini-player-art">{num}</div>
      <div style={{ minWidth: 0 }}>
        <div className="mini-player-title">{title}</div>
        <div className="mini-player-meta">{fmt(state.time)} / {fmt(state.duration)} · {state.speed}x</div>
      </div>
      <div className="mini-player-ctrls">
        <button className="ctrl-btn" style={{ width: 36, height: 36 }} onClick={() => dispatch({ type: "skip", delta: -15 })}><Icon name="back15" size={16} /></button>
        <button className="ctrl-btn play" style={{ width: 44, height: 44 }} onClick={() => dispatch({ type: "toggle" })}><Icon name={state.isPlaying ? "pause" : "play"} size={18} /></button>
        <button className="ctrl-btn" style={{ width: 36, height: 36 }} onClick={() => dispatch({ type: "skip", delta: 30 })}><Icon name="fwd30" size={16} /></button>
      </div>
    </div>
  );
};

/* ---------- Footer ---------- */
const Footer = ({ onOpenAsk }) => {
  // Scroll to sections within the page
  const scrollTo = (e, selector, offset = 80) => {
    e.preventDefault();
    const el = document.querySelector(selector);
    if (el) {
      const rect = el.getBoundingClientRect();
      window.scrollTo({ top: window.scrollY + rect.top - offset, behavior: "smooth" });
    }
  };
  return (
  <footer className="footer">
    <div className="container-wide">
      <div className="footer-grid">
        <div>
          <div className="topnav-mark" style={{ marginBottom: "16px" }}>
            <span className="topnav-dot" />
            The Hole Shebang
          </div>
          <p style={{ fontFamily: "Lora, serif", fontSize: 14, maxWidth: "40ch", color: "var(--ink-soft)", lineHeight: 1.6 }}>
            A podcast about pelvic health, sex, and the parts of the body we were told not to talk about. Hosted by Kristen Parise of Blueberry Therapy.
          </p>
          <a href="https://www.blueberrytherapy.ca/" className="footer-back" target="_top" rel="noopener">
            <span aria-hidden="true" style={{ marginRight: 6 }}>←</span> Back to Blueberry Therapy
          </a>
        </div>
        <div>
          <h5>Listen</h5>
          <ul>
            <li><a href="https://podcasts.apple.com/us/podcast/the-hole-shebang/id1766397933" target="_blank" rel="noopener noreferrer">Apple Podcasts</a></li>
            <li><a href="https://open.spotify.com/show/3Qjlm6VuV0rqvqwmJlGl0t" target="_blank" rel="noopener noreferrer">Spotify</a></li>
            <li><a href="https://iheart.com/podcast/212158113" target="_blank" rel="noopener noreferrer">iHeartRadio</a></li>
            <li><a href="https://feeds.libsyn.com/544492/rss" target="_blank" rel="noopener noreferrer">RSS feed</a></li>
          </ul>
        </div>
        <div>
          <h5>Explore</h5>
          <ul>
            <li><a href="#" onClick={(e) => scrollTo(e, "#previous-episodes")}>All episodes</a></li>
            <li><a href="#" onClick={(e) => scrollTo(e, ".topics-grid")}>By topic</a></li>
            <li><a href="#" onClick={(e) => scrollTo(e, ".host-section")}>Host</a></li>
            <li><a href="#" onClick={(e) => { e.preventDefault(); onOpenAsk && onOpenAsk(); }}>Ask a question</a></li>
          </ul>
        </div>
        <div>
          <h5>Blueberry</h5>
          <ul>
            <li><a href="https://www.blueberrytherapy.ca/" target="_blank" rel="noopener noreferrer">The clinic</a></li>
            <li><a href="https://blueberrytherapy.janeapp.com/" target="_blank" rel="noopener noreferrer">Book a visit</a></li>
            <li><a href="https://blueberrytherapyshop.ca/" target="_blank" rel="noopener noreferrer">Shop</a></li>
            <li><a href="https://www.blueberrytherapy.ca/" target="_blank" rel="noopener noreferrer">Contact</a></li>
          </ul>
        </div>
      </div>
      <div className="footer-bottom">
        <span>© 2026 Blueberry Therapy · 14 Cross St. Unit B, Dundas, ON</span>
        <span>Recorded on the traditional territories of the Mississaugas, Haudenosaunee, and Anishinaabe peoples.</span>
      </div>
    </div>
  </footer>
  );
};

/* ---------- Search Modal ---------- */
// Builds a unified searchable list from the live episode feed (fallback to static data).
// Matches on title, subtitle, guest, tags, full description, and topic names.
const buildSearchIndex = (liveEpisodes) => {
  // Prefer live episodes from Libsyn; fall back to static sample data only if the feed hasn't loaded.
  if (liveEpisodes && liveEpisodes.length) {
    // IMPORTANT: spread the full episode so clicking a result can hand the
    // complete object (audioUrl, number, description, etc.) back to the player.
    return liveEpisodes.map((ep, i) => ({
      ...ep,
      id: ep.uid || `live-${i}`,
      guest: "", // guest is buried in title/description; no dedicated feed field
      tags: [],
      isCurrent: i === 0,
    }));
  }
  // Fallback: static data (dev mode / feed not loaded)
  const items = [];
  const cur = window.CURRENT_EPISODE;
  if (cur) {
    items.push({
      id: `current-${cur.number}`,
      num: String(cur.number).padStart(3, "0"),
      season: `S${cur.season}`,
      title: cur.title,
      sub: cur.subtitle,
      fullDescription: "",
      guest: cur.guest?.name || "",
      tags: cur.tags || [],
      date: cur.date,
      dur: cur.duration,
      isCurrent: true,
    });
  }
  return items;
};

const SearchModal = ({ open, onClose, episodes, onSelectEpisode }) => {
  const [query, setQuery] = React.useState("");
  const inputRef = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  React.useEffect(() => {
    if (open) {
      setQuery("");
      document.body.style.overflow = "hidden";
      // autofocus input
      setTimeout(() => inputRef.current?.focus(), 50);
    } else {
      document.body.style.overflow = "";
    }
    return () => { document.body.style.overflow = ""; };
  }, [open]);

  const index = React.useMemo(() => buildSearchIndex(episodes), [open, episodes]);
  const topics = window.TOPICS || [];

  const q = query.trim().toLowerCase();
  const results = React.useMemo(() => {
    if (!q) return [];
    // Normalize curly quotes so "Kristen's" matches "Kristen’s" etc.
    const norm = (s) => (s || "").toLowerCase().replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"');
    const nq = norm(q);
    return index.filter((it) => {
      const hay = norm(
        [it.title, it.sub, it.guest, it.num, it.season, it.fullDescription, ...(it.tags || [])].join(" ")
      );
      return hay.includes(nq);
    });
  }, [q, index]);

  const matchingTopics = React.useMemo(() => {
    if (!q) return [];
    return topics.filter((t) =>
      t.name.toLowerCase().includes(q) ||
      t.terms.some((term) => term.toLowerCase().includes(q))
    );
  }, [q, topics]);

  if (!open) return null;

  // Highlight matching substring in a string.
  const highlight = (text) => {
    if (!q) return text;
    const idx = text.toLowerCase().indexOf(q);
    if (idx < 0) return text;
    return (
      <>
        {text.slice(0, idx)}
        <mark className="search-hit">{text.slice(idx, idx + q.length)}</mark>
        {text.slice(idx + q.length)}
      </>
    );
  };

  return (
    <div className="search-overlay" onClick={onClose}>
      <div className="search-modal" role="dialog" aria-label="Search episodes" onClick={(e) => e.stopPropagation()}>
        <div className="search-inputwrap">
          <Icon name="search" size={18} />
          <input
            ref={inputRef}
            type="text"
            className="search-input"
            placeholder="Search episodes, guests, topics…"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
          />
          <button className="search-close" onClick={onClose} aria-label="Close">
            <Icon name="x" size={16} />
          </button>
        </div>

        <div className="search-body">
          {!q && (
            <div className="search-empty">
              <div className="search-empty-title">Try searching for</div>
              <div className="search-empty-chips">
                {["postpartum", "kegel", "perimenopause", "prolapse", "prenatal", "prolapse"].filter((v, i, a) => a.indexOf(v) === i).map((s) => (
                  <button key={s} className="search-chip" onClick={() => setQuery(s)}>{s}</button>
                ))}
              </div>
              <div className="search-kbd">
                <kbd>esc</kbd> to close
              </div>
            </div>
          )}

          {q && results.length === 0 && matchingTopics.length === 0 && (
            <div className="search-noresults">
              No matches for <em>"{query}"</em>. Try a guest name, topic, or a word from the title.
            </div>
          )}

          {matchingTopics.length > 0 && (
            <div className="search-section">
              <div className="search-section-title">Topics</div>
              <div className="search-topics">
                {matchingTopics.map((t) => (
                  <span key={t.name} className="search-topic">{highlight(t.name)}</span>
                ))}
              </div>
            </div>
          )}

          {results.length > 0 && (
            <div className="search-section">
              <div className="search-section-title">
                {results.length} episode{results.length === 1 ? "" : "s"}
              </div>
              <ul className="search-results">
                {results.map((it) => (
                  <li
                    key={it.id}
                    className="search-result search-result-clickable"
                    onClick={() => onSelectEpisode && onSelectEpisode(it)}
                    role="button"
                    tabIndex={0}
                    onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelectEpisode && onSelectEpisode(it); } }}
                  >
                    <div className="search-result-meta">
                      <span className="search-result-num">#{it.num}</span>
                      <span className="search-result-dur">{it.dur}</span>
                    </div>
                    <div className="search-result-title">{highlight(it.title)}</div>
                    {it.sub && <div className="search-result-sub">{highlight(it.sub)}</div>}
                    {it.guest && (
                      <div className="search-result-guest">
                        <Icon name="mic" size={12} /> {highlight(it.guest)}
                      </div>
                    )}
                    {it.isCurrent && <span className="search-result-badge">Latest</span>}
                  </li>
                ))}
              </ul>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

/* ---------- Subscribe Modal ---------- */
// Simple grid of podcast platforms with placeholder URLs.
// Replace the href values with your real show URLs when you publish.
const SUBSCRIBE_LINKS = [
  { name: "Apple Podcasts", href: "https://podcasts.apple.com/us/podcast/the-hole-shebang/id1766397933", color: "#B150E2", note: "Most popular on iPhone" },
  { name: "Spotify",        href: "https://open.spotify.com/show/3Qjlm6VuV0rqvqwmJlGl0t", color: "#1DB954", note: "Tap follow to get new episodes" },
  { name: "iHeartRadio",    href: "https://iheart.com/podcast/212158113", color: "#C6002B", note: "Free streaming, no account needed" },
  { name: "Pocket Casts",   href: "https://pca.st/3tfjv09a", color: "#F43E37", note: "Cross-platform favourite" },
  { name: "RSS feed",       href: "https://feeds.libsyn.com/544492/rss", color: "#F26522", note: "Paste into any podcast app" },
];

const SubscribeModal = ({ open, onClose }) => {
  const [copied, setCopied] = React.useState(false);

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  React.useEffect(() => {
    if (open) {
      setCopied(false);
      document.body.style.overflow = "hidden";
    } else {
      document.body.style.overflow = "";
    }
    return () => { document.body.style.overflow = ""; };
  }, [open]);

  if (!open) return null;

  const copyRss = (e) => {
    e.preventDefault();
    const rss = SUBSCRIBE_LINKS.find((s) => s.name === "RSS feed")?.href || "";
    if (!rss || rss === "#") {
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
      return;
    }
    navigator.clipboard?.writeText(rss);
    setCopied(true);
    setTimeout(() => setCopied(false), 1800);
  };

  return (
    <div className="ask-overlay" onClick={onClose}>
      <div className="ask-modal subscribe-modal" role="dialog" aria-label="Subscribe" onClick={(e) => e.stopPropagation()}>
        <button className="ask-close" onClick={onClose} aria-label="Close">×</button>
        <div className="ask-eyebrow">Subscribe</div>
        <h2 className="ask-title">Get new episodes in your app of choice.</h2>
        <p className="ask-lead">New episodes every Thursday. Pick a podcast app, and we'll open it up for you. Your app will handle the rest.</p>

        <ul className="subscribe-list">
          {SUBSCRIBE_LINKS.map((s) => {
            const isRss = s.name === "RSS feed";
            return (
              <li key={s.name}>
                <a
                  className="subscribe-link"
                  href={s.href}
                  target="_blank"
                  rel="noopener noreferrer"
                  onClick={isRss ? copyRss : undefined}
                >
                  <span className="subscribe-dot" style={{ background: s.color }} />
                  <span className="subscribe-name">
                    <span>{s.name}</span>
                    <span className="subscribe-note">{isRss && copied ? "Copied to clipboard" : s.note}</span>
                  </span>
                  <Icon name={isRss ? "rss" : "arrowUpRight"} size={16} />
                </a>
              </li>
            );
          })}
        </ul>

        <div className="subscribe-footer">
          Don't see your app? The RSS feed works in any podcast player.
        </div>
      </div>
    </div>
  );
};

/* ---------- Newsletter signup (submits to Cloudflare Worker → Airtable Subscribers) ---------- */
const NewsletterForm = ({ onToast, darkInput = false }) => {
  const [email, setEmail] = React.useState("");
  const [sending, setSending] = React.useState(false);
  const [done, setDone] = React.useState(false);
  const [error, setError] = React.useState("");

  const submit = async (e) => {
    e.preventDefault();
    setError("");
    const workerUrl = window.WORKER_URL;
    if (!workerUrl) {
      setError("Form endpoint not configured.");
      return;
    }
    setSending(true);
    try {
      const res = await fetch(`${workerUrl}/subscribe`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, source: "Podcast site", website: "" }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setError(data.error || "Something went wrong. Try again?");
        setSending(false);
        return;
      }
      setDone(true);
      setSending(false);
      onToast && onToast("You're on the list");
    } catch (err) {
      setError("Couldn't reach the server. Try again?");
      setSending(false);
    }
  };

  if (done) {
    return (
      <div className="newsletter-done">
        <div className="newsletter-done-title">You're on the list.</div>
        <div className="newsletter-done-sub">Friday mornings just got more interesting.</div>
      </div>
    );
  }

  const darkStyle = darkInput
    ? { background: "rgba(248,244,236,0.12)", borderColor: "rgba(248,244,236,0.2)", color: "var(--cream)" }
    : undefined;

  return (
    <form className="newsletter" onSubmit={submit}>
      {/* honeypot */}
      <input
        type="text"
        name="website"
        tabIndex="-1"
        autoComplete="off"
        style={{ position: "absolute", left: "-9999px", width: 1, height: 1, opacity: 0 }}
        aria-hidden="true"
      />
      <input
        type="email"
        placeholder="you@example.com"
        required
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        disabled={sending}
        style={darkStyle}
      />
      <button type="submit" className="btn btn-primary" disabled={sending}>
        {sending ? "Subscribing…" : "Subscribe"}
      </button>
      {error && <div className="newsletter-error">{error}</div>}
    </form>
  );
};

Object.assign(window, {
  Icon, TopNav, Player, ChapterList, Transcript, ShowNotes, NotesBody, EpisodeSidebar, EpisodeDetails, EpisodeRow, Tweaks, MiniPlayer, Footer, fmt, AskQuestionModal, SearchModal, SubscribeModal, NewsletterForm,
});
