/* AlbumPage, 12-section editorial dossier.
   Falls back to a short placeholder for non-featured albums.
   VERIFY flags are rendered inline as small warnings the editor can click through. */

const { useState: useAState, useEffect: useAEffect, useRef: useARef } = React;

function renderText(str) {
  if (!str) return null;
  // Split on [VERIFY ...] markers and render them as pills.
  const parts = [];
  const re = /\[VERIFY([^\]]*)\]/g;
  let last = 0, m, i = 0;
  while ((m = re.exec(str)) !== null) {
    if (m.index > last) parts.push(<span key={i++} dangerouslySetInnerHTML={{__html: str.slice(last, m.index)}} />);
    parts.push(<span key={i++} className="verify-pill" title={m[1].trim() || "Editor check needed"}>VERIFY{m[1].trim() ? " · " + m[1].trim() : ""}</span>);
    last = m.index + m[0].length;
  }
  if (last < str.length) parts.push(<span key={i++} dangerouslySetInnerHTML={{__html: str.slice(last)}} />);
  return parts;
}

function parseTrackTime(str) {
  if (!str) return 180;
  const m = String(str).match(/(\d+):(\d+)/);
  if (!m) return 180;
  return (+m[1]) * 60 + (+m[2]);
}
function fmtTime(s) {
  s = Math.max(0, Math.floor(s));
  const m = Math.floor(s / 60), r = s - m * 60;
  return m + ":" + (r < 10 ? "0" + r : r);
}

function AlbumPage({ album, onHome, onList, onAlbum, all }) {
  const full = window.ESSAYS_FULL && window.ESSAYS_FULL[album.slug];
  const [progress, setProgress] = useAState(0);
  const [commentCount, setCommentCount] = useAState(null); // null until loaded
  const [openTrack, setOpenTrack] = useAState(null);
  const [essayReadingIdx, setEssayReadingIdx] = useAState(-1); // -1 idle, 0 = dek, 1..N = paragraph index
  const [playingTrack, setPlayingTrack] = useAState(null); // track number currently playing
  const [elapsed, setElapsed] = useAState(0); // seconds into current track
  const [duration, setDuration] = useAState(0); // real duration of current audio
  const [loading, setLoading] = useAState(false);
  const [previewCache, setPreviewCache] = useAState({}); // {trackN: url | null}
  const audioRef = useARef(null);
  const playingTrackRef = useARef(null);
  const previewCacheRef = useARef({});

  useAEffect(() => { playingTrackRef.current = playingTrack; }, [playingTrack]);
  useAEffect(() => { previewCacheRef.current = previewCache; }, [previewCache]);

  // Load this album's comment count (for the jump CTA at the top).
  useAEffect(() => {
    let active = true;
    setCommentCount(null);
    if (window.BBR_supabase) {
      window.BBR_supabase
        .from("comments")
        .select("id", { count: "exact", head: true })
        .eq("slug", album.slug)
        .then(({ count }) => { if (active) setCommentCount(typeof count === "number" ? count : 0); });
    }
    return () => { active = false; };
  }, [album.slug]);

  // Lazily create the Audio element once (also lets first user click "warm" it up)
  const ensureAudio = () => {
    if (!audioRef.current) {
      const a = new Audio();
      a.preload = "auto";
      // iOS Safari requires inline playback to be explicitly allowed, otherwise
      // play() calls (especially deferred ones) are silently rejected.
      a.setAttribute("playsinline", "");
      a.setAttribute("webkit-playsinline", "");
      a.playsInline = true;
      a.crossOrigin = "anonymous";
      audioRef.current = a;
    }
    return audioRef.current;
  };

  // Reset playback when album changes
  useAEffect(() => {
    setPlayingTrack(null);
    setElapsed(0);
    setDuration(0);
    setPreviewCache({});
    if (audioRef.current) { audioRef.current.pause(); audioRef.current.src = ""; }
  }, [album.slug]);

  // Listen for global "audio started" events from other players (WDIB covers, etc.)
  // and pause the tracklist if someone else has started.
  useAEffect(() => {
    const handler = (ev) => {
      const sender = ev?.detail?.id;
      if (sender && sender !== "tracklist" && audioRef.current && !audioRef.current.paused) {
        try { audioRef.current.pause(); } catch (e) {}
        setPlayingTrack(null);
      }
    };
    window.addEventListener("bbr-audio-play", handler);
    return () => window.removeEventListener("bbr-audio-play", handler);
  }, []);

  // Attach audio event listeners (re-attached when album changes so closures stay fresh)
  useAEffect(() => {
    if (!full) return;
    const audio = ensureAudio();
    const onTime = () => setElapsed(audio.currentTime || 0);
    const onLoaded = () => { setDuration(audio.duration || 0); setLoading(false); };
    const onPlay = () => setLoading(false);
    const onEnded = () => {
      const cur = playingTrackRef.current;
      const idx = full.tracks.findIndex(x => x.n === cur);
      const nxt = full.tracks[idx + 1];
      if (nxt) {
        const url = previewCacheRef.current[nxt.n];
        setPlayingTrack(nxt.n);
        setElapsed(0); setDuration(0);
        if (url) {
          audio.src = url;
          audio.currentTime = 0;
          // Already user-activated by the original click, chained play works
          audio.play().catch(() => {});
        } else {
          setLoading(true); // will start when prefetch lands (via the watcher effect)
        }
      } else {
        setPlayingTrack(null);
      }
    };
    const onError = () => setLoading(false);
    audio.addEventListener("timeupdate", onTime);
    audio.addEventListener("loadedmetadata", onLoaded);
    audio.addEventListener("playing", onPlay);
    audio.addEventListener("ended", onEnded);
    audio.addEventListener("error", onError);
    return () => {
      audio.removeEventListener("timeupdate", onTime);
      audio.removeEventListener("loadedmetadata", onLoaded);
      audio.removeEventListener("playing", onPlay);
      audio.removeEventListener("ended", onEnded);
      audio.removeEventListener("error", onError);
    };
  }, [full]);

  // Prefetch all iTunes preview URLs once on album load (staggered to be polite)
  useAEffect(() => {
    if (!full) return;
    let cancelled = false;
    full.tracks.forEach((t, i) => {
      setTimeout(() => {
        if (cancelled) return;
        // Skip if already cached
        if (previewCacheRef.current[t.n] !== undefined) return;
        const term = encodeURIComponent(album.artist + " " + t.title);
        const artistParam = encodeURIComponent(album.artist);
        fetch("/api/preview?term=" + term + "&artist=" + artistParam)
          .then(r => r.json())
          .then(j => {
            if (cancelled) return;
            const url = j && j.previewUrl ? j.previewUrl : null;
            setPreviewCache(p => (p[t.n] !== undefined ? p : { ...p, [t.n]: url }));
          })
          .catch(() => {
            if (!cancelled) setPreviewCache(p => (p[t.n] !== undefined ? p : { ...p, [t.n]: null }));
          });
      }, i * 70);
    });
    return () => { cancelled = true; };
  }, [full, album.artist]);

  // Watcher: if user clicked play before the URL was cached, start playback once it arrives
  useAEffect(() => {
    if (playingTrack == null || !audioRef.current) return;
    const url = previewCache[playingTrack];
    const audio = audioRef.current;
    if (url && audio.src !== url && !audio.src.startsWith("blob:")) {
      audio.src = url;
      audio.currentTime = 0;
      // iOS needs an explicit load() after a src swap before play() will honour it.
      try { audio.load(); } catch (e) {}
      audio.play().catch(() => setLoading(false));
    } else if (url === null) {
      setLoading(false);
    }
  }, [previewCache, playingTrack]);

  // Synchronous play within the user gesture, this is the key to avoiding autoplay blocking
  const togglePlay = (n) => {
    const audio = ensureAudio();

    if (playingTrack === n) {
      audio.pause();
      setPlayingTrack(null);
      return;
    }

    // Announce that the tracklist is starting playback so other audio sources
    // (e.g. WDIB cover previews) pause themselves.
    window.dispatchEvent(new CustomEvent("bbr-audio-play", { detail: { id: "tracklist" } }));

    const t = full && full.tracks.find(x => x.n === n);
    if (!t) return;

    audio.pause();
    setPlayingTrack(n);
    setElapsed(0);
    setDuration(0);
    setLoading(true);

    const cached = previewCache[n];
    if (cached) {
      // Real URL ready, set + play synchronously inside the click handler
      audio.src = cached;
      audio.currentTime = 0;
      audio.play().catch(() => setLoading(false));
    } else if (cached === null) {
      // No preview available
      setLoading(false);
    } else {
      // Prefetch hasn't landed yet. "Warm" the audio element inside this user gesture
      // so the watcher effect's later play() call counts as user-activated. Some browsers
      // require a real src+play during the gesture, so we use a tiny silent WAV.
      try {
        audio.src =
          "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAAA";
        audio.play().catch(() => {});
      } catch (e) {}
      // Kick off the lookup right now (don't wait for the staggered background
      // prefetch) so the real URL lands as fast as possible and the watcher
      // swaps it in.
      if (full) {
        const tk = full.tracks.find(x => x.n === n);
        if (tk) {
          const term = encodeURIComponent(album.artist + " " + tk.title);
          const artistParam = encodeURIComponent(album.artist);
          fetch("/api/preview?term=" + term + "&artist=" + artistParam)
            .then(r => r.json())
            .then(j => {
              setPreviewCache(p => (p[n] !== undefined ? p : { ...p, [n]: j && j.previewUrl ? j.previewUrl : null }));
            })
            .catch(() => setPreviewCache(p => (p[n] !== undefined ? p : { ...p, [n]: null })));
        }
      }
    }
  };

  useAEffect(() => {
    const onScroll = () => {
      const h = document.documentElement;
      const max = h.scrollHeight - h.clientHeight;
      setProgress(max > 0 ? Math.min(1, h.scrollTop / max) : 0);
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener("scroll", onScroll);
  }, [album.slug]);

  useAEffect(() => {
    window.scrollTo(0, 0);
    setOpenTrack(null);
    const k = (e) => { if (e.key === "Escape") onList(); };
    window.addEventListener("keydown", k);
    return () => window.removeEventListener("keydown", k);
  }, [album.slug]);

  const nextAlbum =
    all.find(a => a.rank === album.rank - 1) ||
    all.find(a => a.rank === album.rank + 1) ||
    all[0];

  const prevAlbum = all.find(a => a.rank === album.rank + 1) || null;

  // Whole-page reading estimate (essay + every long-form section).
  const readingMins = (() => {
    if (!full) return null;
    const parts = [];
    if (full.essay) { parts.push(full.essay.dek); (full.essay.paragraphs || []).forEach(p => parts.push(p)); }
    if (full.placeInHistory) { parts.push(full.placeInHistory.opening, full.placeInHistory.closing); (full.placeInHistory.context || []).forEach(c => { parts.push(c.event, c.relevance); }); }
    (full.tracks || []).forEach(t => { parts.push(t.note); if (t.listen) parts.push(t.listen.for); });
    (full.cultural || []).forEach(c => parts.push(c.moment));
    if (full.dossier) { parts.push(full.dossier.studios, full.dossier.timeline, full.dossier.budget, full.dossier.gear); (full.dossier.collaborators || []).forEach(c => parts.push(c.role)); (full.dossier.myths || []).forEach(m => parts.push(m.claim, m.detail)); }
    ["sameEra", "sameVibe", "deepCuts"].forEach(k => ((full.recommendations && full.recommendations[k]) || []).forEach(r => parts.push(r.why)));
    (full.prompts || []).forEach(p => parts.push(p));
    const words = parts.filter(Boolean).map(stripForSpeech).join(" ").split(/\s+/).filter(Boolean).length;
    return Math.max(1, Math.round(words / 220));
  })();

  const startEssayListen = () => {
    const el = document.getElementById("essay");
    if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 120, behavior: "smooth" });
    setTimeout(() => window.dispatchEvent(new CustomEvent("bbr-essay-play")), 220);
  };

  const sharePrompt = (text) => {
    window.dispatchEvent(new CustomEvent("bbr-prefill-comment", { detail: { text: stripForSpeech(text) } }));
    const target = document.querySelector(".comment-form") || document.getElementById("comments");
    if (target) window.scrollTo({ top: target.getBoundingClientRect().top + window.scrollY - 120, behavior: "smooth" });
  };

  // Build the list of sections for the floating nav (only ones that will render)
  const navSections = full ? [
    { id: "key-stats",       numeral: "I",    label: "Key stats" },
    { id: "essay",           numeral: "II",   label: "The essay" },
    ...(full.placeInHistory && window.PlaceInHistory ? [{ id: "place-in-history", numeral: "III", label: "Place in history" }] : []),
    { id: "track-by-track",  numeral: "IV",  label: "Song-by-song guide" },
    ...(full.cover && window.AboutCover ? [{ id: "about-cover", numeral: "V", label: "The cover" }] : []),
    { id: "collector",       numeral: "VI",   label: "Collector's corner" },
    { id: "making-of",       numeral: "VII",  label: "Making of" },
    { id: "influence",       numeral: "VIII", label: "Influence web" },
    ...(full.whoDidItBetter && full.whoDidItBetter.length && window.WhoDidItBetter ? [{ id: "who-did-it-better", numeral: "IX", label: "Who did it better?" }] : []),
    { id: "charts",          numeral: "X",    label: "Charts" },
    { id: "cultural",        numeral: "XI",   label: "Cultural footprint" },
    { id: "recommendations", numeral: "XII",  label: "If you love this" },
    { id: "prompts",         numeral: "XIII", label: "Discussion prompts" },
    ...(window.CommentsSection ? [{ id: "comments", numeral: "XIV", label: "Reader comments" }] : []),
  ] : [];

  const jumpToTracks = () => {
    const el = document.getElementById("track-by-track");
    if (!el) return;
    const top = el.getBoundingClientRect().top + window.scrollY - 120;
    window.scrollTo({ top, behavior: "smooth" });
  };

  // Placeholder if no full dossier
  if (!full) {
    return (
      <div className="album-page">
        {window.AlbumAccent && <AlbumAccent album={album} />}
        <AlbumCrumbs album={album} onList={onList} onHome={onHome} all={all} onAlbum={onAlbum} />
        <AffiliateStrip album={album} />
        {window.StickyBuyBar && <StickyBuyBar album={album} />}
        <header className="hero">
          <div>
            <div className="rank-line">
              <span className="pill">№ {String(album.rank).padStart(3, "0")} of 100</span>
              <span>{album.genre} · {album.year} · {album.label}</span>
            </div>
            <h1>{album.title}</h1>
            <div className="artist">{album.artist}</div>
            <p className="dek">This entry is indexed in the canon. We're loading its full dossier, if it doesn't appear, refresh the page. Our flagship deep dive, Marvin Gaye's What's Going On, is on the homepage and shows the treatment every entry on this list receives.</p>
            <div style={{ display: "flex", gap: 14, marginTop: 20, flexWrap: "wrap" }}>
              <button className="toolbar-btn" onClick={onList}>← Back to the list</button>
              <button className="toolbar-btn ghost" onClick={onHome}>Home</button>
            </div>
          </div>
          <div className="cover-stack">
            <div className="vinyl" />
            <div className="cover-main"><Sleeve album={album} size={460} /></div>
          </div>
          <div className="hero-num">{String(album.rank).padStart(2, "0")}</div>
        </header>
      </div>
    );
  }

  return (
    <div className="album-page">
      <div className="scroll-progress"><span style={{ width: (progress * 100) + "%" }} /></div>

      {window.AlbumAccent && <AlbumAccent album={album} />}

      <AlbumCrumbs album={album} onList={onList} onHome={onHome} all={all} onAlbum={onAlbum} />

      <AffiliateStrip album={album} />

      {window.StickyBuyBar && <StickyBuyBar album={album} />}

      <DossierNav sections={navSections} />


      {/* The slim StickyBuyBar (below) is the single floating buy element on every
          page; the larger StickyBuyRail is intentionally not rendered to avoid two. */}

      {/* ======= HERO ======= */}
      <header className="hero">
        <div>
          <div className="rank-line">
            <span className="pill">№ {String(album.rank).padStart(3, "0")} of 100</span>
            <span>{album.genre} · {album.year} · {album.country}</span>
          </div>
          <h1>{album.title}</h1>
          <div className="artist">{album.artist}</div>

          {readingMins && (
            <div className="hero-readtime">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></svg>
              {readingMins} min read <span className="hrt-sep">·</span> {navSections.length} sections
            </div>
          )}

          {/* Section 1, one-line critical summary */}
          <div className="tagline-block">
            <div className="tagline-label">The critical summary, in one line</div>
            <p className="tagline">{renderText(full.tagline)}</p>
          </div>

          <div className="hero-jumps">
            <button className="hero-jump-cta" onClick={jumpToTracks}>
              <span className="hero-jump-icon" aria-hidden="true">
                <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M7 5v14l12-7z"/></svg>
              </span>
              Skip to the song-by-song guide
              <span className="hero-jump-meta">{full.tracks.length} tracks · {full.stats.runtime}</span>
            </button>
            <button className="hero-jump-link hero-listen" onClick={startEssayListen}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3zM16 7.5a4 4 0 0 1 0 9z"/></svg>
              Listen to this essay
            </button>
            <a className="hero-jump-link" href="#essay" onClick={(e) => { e.preventDefault(); const el = document.getElementById("essay"); if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 120, behavior: "smooth" }); }}>
              or read the essay first
              <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
            </a>
            {window.CommentsSection && (
              <button className="hero-jump-link hero-comments-jump" onClick={() => { const el = document.getElementById("comments"); if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 100, behavior: "smooth" }); }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-8.5 8.5 8.5 8.5 0 0 1-3.8-.9L3 21l1.9-5.7a8.5 8.5 0 0 1-.9-3.8A8.38 8.38 0 0 1 12.5 3 8.38 8.38 0 0 1 21 11.5z"/></svg>
                {commentCount === null ? "Comments" : (commentCount + " comment" + (commentCount === 1 ? "" : "s"))}
              </button>
            )}
          </div>

          <div className="hero-meta">
            <div className="cell"><b>Released</b>{full.stats.released}</div>
            <div className="cell"><b>Label</b>{full.stats.label}</div>
            <div className="cell"><b>Runtime</b>{full.stats.runtime}</div>
            <div className="cell"><b>Produced by</b>{renderText(full.stats.producers)}</div>
          </div>

          {window.ScoreBreakdown && <ScoreBreakdown album={album} onHowToScore={() => { window.__scrollToScore = true; onHome(); }} />}

          {full.signals && (
            <div className="signals">
              <Scorecard
                kind="momentum"
                label="Cultural momentum"
                sub={full.signals.momentum.period}
                value={(full.signals.momentum.direction === "down" ? "↓ " : full.signals.momentum.direction === "flat" ? "→ " : "↑ ") + full.signals.momentum.value + "%"}
                direction={full.signals.momentum.direction}
                source={full.signals.momentum.source}
                detail={full.signals.momentum.detail}
              />
              <Scorecard
                kind="heat"
                label="Collector heat index"
                sub={full.signals.collectorHeat.period}
                value={full.signals.collectorHeat.score}
                badge={full.signals.collectorHeat.label}
                delta={(full.signals.collectorHeat.direction === "down" ? "↓ " : full.signals.collectorHeat.direction === "flat" ? "→ " : "↑ ") + full.signals.collectorHeat.delta}
                direction={full.signals.collectorHeat.direction}
                source={full.signals.collectorHeat.source}
                detail={full.signals.collectorHeat.detail}
              />
            </div>
          )}
        </div>
        <div className="cover-stack">
          <div className="vinyl" />
          <div className="cover-main">
            <Sleeve album={album} size={460} />
          </div>
        </div>
        <div className="hero-num">{String(album.rank).padStart(2, "0")}</div>
      </header>

      {window.MobileTOC && <MobileTOC sections={navSections} minutes={readingMins} />}

      <main className="dossier">

        {/* ======= 2. KEY STATS, key/value grid ======= */}
        <Section id="key-stats" numeral="I" label="The file" title="Key stats">
          <dl className="keystats">
            <div><dt>Released</dt><dd>{renderText(full.stats.released)}</dd></div>
            <div><dt>Label</dt><dd>{renderText(full.stats.label)}</dd></div>
            <div><dt>Producer(s)</dt><dd>{renderText(full.stats.producers)}</dd></div>
            <div><dt>Runtime</dt><dd>{renderText(full.stats.runtime)}</dd></div>
            <div><dt>Peak, US</dt><dd>{renderText(full.stats.peakUS)}</dd></div>
            <div><dt>Peak, UK</dt><dd>{renderText(full.stats.peakUK)}</dd></div>
            <div><dt>Certified sales</dt><dd>{renderText(full.stats.certified)}</dd></div>
            <div><dt>Awards</dt><dd>{renderText(full.stats.awards)}</dd></div>
          </dl>
        </Section>

        {/* ======= 3. THE ESSAY ======= */}
        <Section id="essay" numeral="II" label="The essay" title="Long-form">
          {window.EssayReader && (
            <EssayReader
              dek={full.essay.dek}
              paragraphs={full.essay.paragraphs}
              onSegmentChange={setEssayReadingIdx}
              albumTitle={album.title}
              artist={album.artist}
              album={album}
              primary={true}
              audioId="readaloud-essay"
            />
          )}
          <p className={"dek-large" + (essayReadingIdx === 0 ? " is-reading" : "")}>{renderText(full.essay.dek)}</p>
          <div className="longform">
            {full.essay.paragraphs.map((p, i) => (
              <p key={i} className={(i === 0 ? "lede" : "") + (essayReadingIdx === i + 1 ? " is-reading" : "")}>{renderText(p)}</p>
            ))}          </div>
        </Section>

        {/* ======= 3. PLACE IN HISTORY (III) ======= */}
        {full.placeInHistory && window.PlaceInHistory && (
          <Section id="place-in-history" numeral="III" label="Place in history" title="The moment around the record">
            {window.EssayReader && (
              <EssayReader
                dek={full.placeInHistory.opening}
                paragraphs={[
                  ...(full.placeInHistory.context || []).map(c => (c.event ? c.event + ". " : "") + (c.relevance || "")),
                  full.placeInHistory.closing || ""
                ].filter(Boolean)}
                albumTitle={album.title}
                artist={album.artist}
                audioId="readaloud-pih"
              />
            )}
            <PlaceInHistory data={full.placeInHistory} albumTitle={album.title} />
          </Section>
        )}

        {/* ======= 4. TRACK-BY-TRACK ======= */}
        <Section id="track-by-track" numeral="IV" label="Track by track" title="The guide">
          <div className="tracks-tools">
            <button
              className="tracks-playall"
              onClick={() => {
                if (playingTrack != null) { setPlayingTrack(null); if (audioRef.current) audioRef.current.pause(); return; }
                if (full.tracks[0]) togglePlay(full.tracks[0].n);
              }}
            >
              <span className="tracks-playall-icon" aria-hidden="true">
                {playingTrack != null ? (
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg>
                ) : (
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M7 5v14l12-7z"/></svg>
                )}
              </span>
              {playingTrack != null ? "Stop playback" : "Play album"}
            </button>
            <span className="tracks-tools-note">
              {(() => {
                // `playingTrack` is reset to null on album change via an effect,
                // but effects run after render — so for one render the stale
                // track number can miss the new album's list. Guard the lookup.
                const np = playingTrack != null && full.tracks.find(x => x.n === playingTrack);
                return np
                  ? <>Now playing, <em>{np.title}</em></>
                  : <>{full.tracks.length} tracks · {full.stats.runtime}</>;
              })()}
            </span>
          </div>
          <ol className="tracks-grid">
            {full.tracks.map(t => {
              const open = openTrack === t.n;
              const isPlaying = playingTrack === t.n;
              const isLoading = isPlaying && loading;
              const previewUrl = previewCache[t.n];
              const hasNoPreview = previewUrl === null;
              const playDur = isPlaying && duration > 0 ? duration : parseTrackTime(t.time);
              const pct = isPlaying && playDur > 0 ? Math.min(100, (elapsed / playDur) * 100) : 0;
              return (
                <li key={t.n} className={(open ? "open " : "") + (isPlaying ? "is-playing" : "")}>
                  <header onClick={() => setOpenTrack(open ? null : t.n)}>
                    <button
                      className={"tplay" + (isPlaying ? " on" : "") + (isLoading ? " loading" : "")}
                      onClick={(e) => { e.stopPropagation(); togglePlay(t.n); }}
                      aria-label={(isPlaying ? "Pause " : "Play ") + t.title}
                    >
                      <span className="tplay-num">{String(t.n).padStart(2, "0")}</span>
                      <span className="tplay-icon" aria-hidden="true">
                        {isLoading ? (
                          <span className="tplay-spinner" />
                        ) : isPlaying ? (
                          <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg>
                        ) : (
                          <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M7 5v14l12-7z"/></svg>
                        )}
                      </span>
                    </button>
                    <span className="tname">{t.title}</span>
                    <span className="tcreds">{renderText(t.credits)}</span>
                    <span className="tlen">
                      {isPlaying ? (
                        <span className="tlen-playing">
                          <span className="tlen-elapsed">{fmtTime(elapsed)}</span>
                          <span className="tlen-sep">/</span>
                          <span className="tlen-total">{fmtTime(playDur)}</span>
                          {!hasNoPreview && !isLoading && <span className="tlen-tag">preview</span>}
                          {hasNoPreview && <span className="tlen-tag warn">no clip</span>}
                        </span>
                      ) : t.time}
                    </span>
                    <span className="tprog" aria-hidden="true">
                      <span className="tprog-fill" style={{ width: pct + "%" }} />
                    </span>
                  </header>
                  <div className="tbody">
                    <p className="tnote">{renderText(t.note)}</p>
                    <p className="tlisten"><b>Listen for —</b> <span className="tstamp">{t.listen.at}</span> — {renderText(t.listen.for)}</p>
                    <div className="tlisten-links" onClick={(e) => e.stopPropagation()}>
                      <span className="tlisten-links-label">Listen on</span>
                      <a className="tlisten-link sp" href={"https://open.spotify.com/search/" + encodeURIComponent(album.artist + " " + t.title)} target="_blank" rel="noopener">Spotify</a>
                      <a className="tlisten-link am" href={"https://music.apple.com/us/search?term=" + encodeURIComponent(album.artist + " " + t.title)} target="_blank" rel="noopener">Apple Music</a>
                      <a className="tlisten-link yt" href={"https://www.youtube.com/results?search_query=" + encodeURIComponent(album.artist + " " + t.title)} target="_blank" rel="noopener">YouTube</a>
                      <a className="tlisten-link tl" href={"https://tidal.com/search?q=" + encodeURIComponent(album.artist + " " + t.title)} target="_blank" rel="noopener">Tidal</a>
                    </div>
                    <TrackRating
                      slug={album.slug}
                      trackN={t.n}
                      seedAvg={t.rating || (3.8 + ((t.n * 7) % 12) / 10)}
                      seedCount={t.ratingCount || (120 + (t.n * 47) % 380)}
                    />
                  </div>
                </li>
              );
            })}
          </ol>
        </Section>

        {/* ======= 5. ABOUT THE COVER ======= */}
        {full.cover && window.AboutCover && (
          <Section id="about-cover" numeral="V" label="About the cover" title="The sleeve, read closely">
            <AboutCover data={full.cover} album={album} />
          </Section>
        )}

        {/* ======= 6. COLLECTOR'S CORNER ======= */}
        <Section id="collector" numeral="VI" label="Collector's corner" title="Pressings, values, what to buy">
          {full.collector.pressings && window.PressingLedger ? (
            <>
              <PressingLedger pressings={full.collector.pressings} />
              <h4 className="sub-h">Watch out for</h4>
              <ul className="avoid-list">
                {full.collector.avoid.map((a, i) => (
                  <li key={i}>{renderText(a)}</li>
                ))}
              </ul>
              <div className="reissue-note">
                <div className="ri-label">The editor's take, buy or skip</div>
                <p>{renderText(full.collector.reissue)}</p>
              </div>
            </>
          ) : (
            <>
              <div className="holy-grail">
                <div className="hg-label">The holy grail pressing</div>
                <p className="hg-detail">{renderText(full.collector.holyGrail)}</p>
              </div>
              <div className="values-strip">
                <div><b>Mint</b><span>{renderText(full.collector.values.mint)}</span></div>
                <div><b>VG+</b><span>{renderText(full.collector.values.vgplus)}</span></div>
                <div><b>VG</b><span>{renderText(full.collector.values.vg)}</span></div>
              </div>
              <h4 className="sub-h">Watch out for</h4>
              <ul className="avoid-list">
                {full.collector.avoid.map((a, i) => (
                  <li key={i}>{renderText(a)}</li>
                ))}
              </ul>
              <div className="reissue-note">
                <div className="ri-label">Modern reissues, buy / skip</div>
                <p>{renderText(full.collector.reissue)}</p>
              </div>
            </>
          )}
        </Section>

        {/* ======= 7. MAKING-OF DOSSIER ======= */}
        <Section id="making-of" numeral="VII" label="Making of" title="The dossier">
          {/* Photo gallery, artist + behind-the-scenes (WGO) */}
          {full.gallery && window.MakingGallery && (
            <MakingGallery images={full.gallery} />
          )}

          {/* Illustrated scenes, only when scenes data exists (WGO) */}
          {full.dossier.scenes && window.SceneGrid && (
            <SceneGrid scenes={full.dossier.scenes} />
          )}

          <div className="dossier-grid">
            <div className="d-block">
              <h4>Studios</h4>
              <p>{renderText(full.dossier.studios)}</p>
            </div>
            <div className="d-block">
              <h4>Timeline</h4>
              <p>{renderText(full.dossier.timeline)}</p>
            </div>
            <div className="d-block">
              <h4>Budget &amp; return</h4>
              <p>{renderText(full.dossier.budget)}</p>
            </div>
            <div className="d-block">
              <h4>Gear &amp; technique</h4>
              <p>{renderText(full.dossier.gear)}</p>
            </div>
          </div>

          <h4 className="sub-h">Session personnel beyond the headliner</h4>
          <ul className="personnel-list">
            {full.dossier.collaborators.map((c, i) => (
              <li key={i}>
                <span className="pname">{c.name}</span>
                <span className="prole">{renderText(c.role)}</span>
              </li>
            ))}
          </ul>

          <h4 className="sub-h">Myth vs. fact</h4>
          <ul className="myths">
            {full.dossier.myths.map((m, i) => (
              <li key={i} className={"myth status-" + m.status.replace(/\s+/g, '-')}>
                <span className={"myth-status"}>{m.status}</span>
                <div>
                  <p className="myth-claim">&ldquo;{renderText(m.claim)}&rdquo;</p>
                  <p className="myth-detail">{renderText(m.detail)}</p>
                </div>
              </li>
            ))}
          </ul>
        </Section>

        {/* ======= 8. INFLUENCE WEB ======= */}
        <Section id="influence" numeral="VIII" label="Influence web" title="The current flows both ways">
          <div className="influence-cols">
            <div>
              <h4 className="sub-h">Six records it shaped <span className="arr">↓</span></h4>
              <ul className="infl">
                {full.influence.downstream.map((d, i) => (
                  <li key={i}>
                    <span className="infl-title">{d.title}</span>
                    <span className="infl-how">{renderText(d.how)}</span>
                  </li>
                ))}
              </ul>
            </div>
            <div>
              <h4 className="sub-h">Four records that shaped it <span className="arr">↑</span></h4>
              <ul className="infl">
                {full.influence.upstream.map((u, i) => (
                  <li key={i}>
                    <span className="infl-title">{u.title}</span>
                    <span className="infl-how">{renderText(u.how)}</span>
                  </li>
                ))}
              </ul>
            </div>
          </div>
        </Section>

        {/* ======= 9. WHO DID IT BETTER? ======= */}
        {full.whoDidItBetter && full.whoDidItBetter.length > 0 && window.WhoDidItBetter && (
          <Section id="who-did-it-better" numeral="IX" label="Who did it better?" title="The famous covers, judged">
            <WhoDidItBetter covers={full.whoDidItBetter} artist={album.artist} />
          </Section>
        )}

        {/* ======= 10. CHARTS ======= */}
        <Section id="charts" numeral="X" label="Charts &amp; commercial" title="By the numbers">
          {full.charts.peakData && window.ChartsVisual ? (
            <ChartsVisual
              peakData={full.charts.peakData}
              salesMilestones={full.charts.salesMilestones}
              globalSales={full.charts.globalSales}
              datapoint={full.charts.datapoint}
              released={full.stats && full.stats.released}
            />
          ) : (
            <>
              <table className="charts-table">
                <thead>
                  <tr><th>Market</th><th>Peak</th><th>Weeks on chart</th><th>Certified</th></tr>
                </thead>
                <tbody>
                  {full.charts.rows.map((r, i) => (
                    <tr key={i}>
                      <td>{r.market}</td>
                      <td>{renderText(r.peak)}</td>
                      <td>{r.weeks}</td>
                      <td>{r.cert}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
              <div className="charts-footer">
                <div><b>Global sales</b><p>{renderText(full.charts.globalSales)}</p></div>
                <div><b>One interesting data point</b><p>{renderText(full.charts.datapoint)}</p></div>
              </div>
            </>
          )}
        </Section>

        {/* ======= 11. CULTURAL FOOTPRINT ======= */}
        <Section id="cultural" numeral="XI" label="Cultural footprint" title="Where the record showed up">
          <ul className="timeline">
            {full.cultural.map((c, i) => (
              <li key={i}>
                <span className="tl-date">{c.date}</span>
                <p>{renderText(c.moment)}</p>
              </li>
            ))}
          </ul>
        </Section>

        {/* ======= 12. IF YOU LOVE THIS, TRY ======= */}
        <Section id="recommendations" numeral="XII" label="If you love this" title="Six places to go next">
          <div className="recs-cols">
            {(full.recommendations.sameEra && full.recommendations.sameEra.length > 0) && (
              <div>
                <h4 className="sub-h">Same era</h4>
                {full.recommendations.sameEra.map((r, i) => (
                  window.RecLink
                    ? <RecLink key={i} rec={r} all={all} onAlbum={onAlbum} />
                    : <div className="rec" key={i}><div className="rec-title">{r.title}</div><div className="rec-why">{renderText(r.why)}</div></div>
                ))}
              </div>
            )}
            {(full.recommendations.sameVibe && full.recommendations.sameVibe.length > 0) && (
              <div>
                <h4 className="sub-h">Same vibe, different era</h4>
                {full.recommendations.sameVibe.map((r, i) => (
                  window.RecLink
                    ? <RecLink key={i} rec={r} all={all} onAlbum={onAlbum} />
                    : <div className="rec" key={i}><div className="rec-title">{r.title}</div><div className="rec-why">{renderText(r.why)}</div></div>
                ))}
              </div>
            )}
            {(full.recommendations.deepCuts && full.recommendations.deepCuts.length > 0) && (
              <div>
                <h4 className="sub-h">Deeper cut most people miss</h4>
                {full.recommendations.deepCuts.map((r, i) => (
                  window.RecLink
                    ? <RecLink key={i} rec={r} all={all} onAlbum={onAlbum} />
                    : <div className="rec" key={i}><div className="rec-title">{r.title}</div><div className="rec-why">{renderText(r.why)}</div></div>
                ))}
              </div>
            )}
          </div>
        </Section>

        {/* ======= 13. DISCUSSION PROMPTS ======= */}
        <Section id="prompts" numeral="XIII" label="For the comments" title="Three things worth arguing about">
          <ol className="prompts">
            {full.prompts.map((p, i) => (
              <li key={i}>
                <span className="pnum">Q{i + 1}</span>
                <div className="prompt-body">
                  <p>{renderText(p)}</p>
                  {window.CommentsSection && (
                    <button className="prompt-share" onClick={() => sharePrompt(p)}>
                      Share your take
                      <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
                    </button>
                  )}
                </div>
              </li>
            ))}
          </ol>
        </Section>

        {/* ======= 13. READER COMMENTS + RATINGS ======= */}
        {window.CommentsSection && (
          <Section id="comments" numeral="XIV" label="Comments" title="The readers weigh in">
            <CommentsSection slug={album.slug} />
          </Section>
        )}

      </main>

      {window.NewsletterSignup && (
        <div style={{ maxWidth: 1200, margin: "0 auto", padding: "0 56px 8px" }}>
          <NewsletterSignup variant="banner" source={"album:" + album.slug} />
        </div>
      )}

      <footer className="endnote">
        {prevAlbum && (
          <a className="endnote-prev" onClick={() => onAlbum(prevAlbum)}>
            <span className="ep-eyebrow">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"><path d="M19 12H5M11 18l-6-6 6-6"/></svg>
              Previous in the countdown
            </span>
            <span className="ep-title">№ {String(prevAlbum.rank).padStart(2, "0")}, <em>{prevAlbum.title}</em></span>
            <span className="ep-artist">{prevAlbum.artist}</span>
          </a>
        )}
        <div className="kicker">Up next in the countdown</div>
        <h3>№ {String(nextAlbum.rank).padStart(2, "0")}, <em>{nextAlbum.title}</em></h3>
        <p>{window.BLURBS[nextAlbum.rank] || `${nextAlbum.artist}'s ${nextAlbum.year} landmark, ${nextAlbum.genre.toLowerCase()} from ${nextAlbum.country}.`}</p>
        <div style={{ display: "flex", gap: 14, justifyContent: "center", flexWrap: "wrap" }}>
          {(window.ESSAYS_FULL && window.ESSAYS_FULL[nextAlbum.slug]) ? (
            <a className="cta" onClick={() => onAlbum(nextAlbum)}>
              Read №{nextAlbum.rank} deep dive
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M13 5l7 7-7 7" /></svg>
            </a>
          ) : (
            <a className="cta" onClick={onList}>
              See all one hundred
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M13 5l7 7-7 7" /></svg>
            </a>
          )}
        </div>
      </footer>
    </div>
  );
}

function Section({ id, numeral, label, title, children }) {
  return (
    <section id={id} className="dsection">
      <aside className="dsection-label">
        <span className="chno">{numeral}</span>
        {label}
      </aside>
      <div className="dsection-body">
        <h2>{title}.</h2>
        {children}
      </div>
    </section>
  );
}

/* ---------- Scorecard, cultural momentum + collector heat ---------- */
function Scorecard({ kind, label, sub, value, badge, delta, direction, source, detail }) {
  const [open, setOpen] = useAState(false);
  const dirClass = direction === "down" ? "dir-down" : direction === "flat" ? "dir-flat" : "dir-up";
  const tipBody = kind === "heat"
    ? "A 0–100 composite of last-90-day collector activity: Discogs median sale price and price trajectory (40%), wantlist-to-have ratio (25%), Discogs marketplace listing velocity, how fast copies sell once posted (20%), and original-pressing scarcity weighting (15%). Recalculated weekly."
    : "A weighted blend of last-90-day signals: streaming velocity vs. catalog baseline (30%), critic & editorial mentions in tier-1 publications (25%), social conversation volume across TikTok, Reddit, and Twitter (25%), and sync placements in film, TV, and ads (20%). Indexed against the album's own 12-month rolling average, so the arrow shows momentum, not absolute popularity.";
  const tipTitle = kind === "heat" ? "How collector heat is calculated" : "How cultural momentum is calculated";
  return (
    <div className={"scorecard sc-" + kind + " " + dirClass}>
      <button className="sc-head" onClick={() => setOpen(!open)} aria-expanded={open}>
        <div className="sc-label">
          <span className="sc-k">
            {label}
            <span
              className="sc-info"
              role="img"
              aria-label={tipTitle}
              tabIndex={0}
              onClick={(e) => e.stopPropagation()}
              onKeyDown={(e) => e.stopPropagation()}
            >
              <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" aria-hidden="true">
                <circle cx="12" cy="12" r="10"/>
                <line x1="12" y1="11" x2="12" y2="17"/>
                <circle cx="12" cy="7.5" r="0.6" fill="currentColor" stroke="none"/>
              </svg>
              <span className="sc-tip" role="tooltip">
                <span className="sc-tip-title">{tipTitle}</span>
                <span className="sc-tip-body">{tipBody}</span>
              </span>
            </span>
          </span>
          <span className="sc-sub">{sub}</span>
        </div>
        <div className="sc-val-wrap">
          {kind === "heat" ? (
            <>
              <div className="sc-heat-meter" aria-hidden="true">
                <div className="sc-heat-fill" style={{ width: value + "%" }} />
              </div>
              <div className="sc-heat-row">
                <span className="sc-score">{value}<span className="sc-score-max">/100</span></span>
                <span className="sc-badge">{badge}</span>
                {delta && <span className="sc-delta">{delta}</span>}
              </div>
            </>
          ) : (
            <span className="sc-val">{value}</span>
          )}
        </div>
        <svg className={"sc-chev " + (open ? "is-open" : "")} width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M6 9l6 6 6-6"/></svg>
      </button>
      {open && (
        <div className="sc-drawer">
          <p className="sc-detail">{detail}</p>
          <p className="sc-source"><b>Source —</b> {source}</p>
        </div>
      )}
    </div>
  );
}

/* ---------- TrackRating, persistent 1-5 per track ---------- */
function TrackRating({ slug, trackN, seedAvg, seedCount }) {
  const key = "bbr-track-rate-" + slug + "-" + trackN;
  const [mine, setMine] = useAState(() => {
    try { const v = localStorage.getItem(key); return v ? Number(v) : 0; } catch (e) { return 0; }
  });
  const [hover, setHover] = useAState(0);

  // Simulated community average shifts slightly if the user has rated
  const count = (seedCount || 0) + (mine ? 1 : 0);
  const avg = count === 0 ? seedAvg :
    ((seedAvg * (seedCount || 0)) + (mine || 0)) / count;

  const rate = (n, e) => {
    e.stopPropagation();
    const next = mine === n ? 0 : n;
    setMine(next);
    try { localStorage.setItem(key, String(next)); } catch (err) {}
  };

  return (
    <div className="trate" onClick={(e) => e.stopPropagation()}>
      <div className="trate-stars" onMouseLeave={() => setHover(0)}>
        {[1,2,3,4,5].map(n => {
          const active = (hover || mine) >= n;
          return (
            <button
              key={n}
              className={"trate-star" + (active ? " on" : "")}
              onMouseEnter={() => setHover(n)}
              onClick={(e) => rate(n, e)}
              aria-label={"Rate " + n + " star" + (n === 1 ? "" : "s")}
            >★</button>
          );
        })}
      </div>
      <span className="trate-avg">
        <b>{avg.toFixed(1)}</b>
        <span className="trate-count">({count.toLocaleString()} {count === 1 ? "rating" : "ratings"})</span>
      </span>
    </div>
  );
}

/* ---------- AffiliateStrip, top-of-page retailer rail ---------- */
function AffiliateStrip({ album }) {
  const q = encodeURIComponent(album.artist + " " + album.title + " vinyl");
  const retailers = [
    { name: "Buy now on Amazon",      label: "New vinyl",      href: (window.BBR_buyUrl ? window.BBR_buyUrl(album.artist + " " + album.title, album.slug) : "https://www.amazon.co.uk/s?k=" + q + "&i=popular&tag=theleadin1-21") },
  ];
  return (
    <aside className="aff-strip" role="complementary" aria-label="Where to buy this record">
      <div className="aff-strip-inner">
        <div className="aff-strip-lede">
          <span className="aff-strip-eyebrow">Sponsored · Affiliate</span>
          <span className="aff-strip-title">
            Buy <em>{album.title}</em><span className="aff-strip-disclosure">, we earn a small cut, the price you pay doesn't change.</span>
          </span>
        </div>
        <div className="aff-strip-retailers">
          {retailers.map(r => (
            <a key={r.name} className="aff-chip" href={r.href} target="_blank" rel="sponsored noopener" onClick={() => window.BBR_trackClick && window.BBR_trackClick(album.slug, "strip")}>
              <span className="aff-chip-name">{r.name}</span>
              <span className="aff-chip-meta">{r.label}</span>
              <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true"><path d="M7 17L17 7M9 7h8v8"/></svg>
            </a>
          ))}
        </div>
      </div>
    </aside>
  );
}

window.AlbumPage = AlbumPage;

/* ---------- DossierNav, floating section rail on the right ---------- */
function DossierNav({ sections }) {
  const [active, setActive] = useAState(sections[0] && sections[0].id);
  const [expanded, setExpanded] = useAState(false);
  const [visible, setVisible] = useAState(false);

  useAEffect(() => {
    // Reveal once the user scrolls past the hero
    const onScroll = () => setVisible(window.scrollY > 360);
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  useAEffect(() => {
    if (!sections.length) return;
    const observer = new IntersectionObserver((entries) => {
      // pick the entry highest on screen that's intersecting
      const intersecting = entries.filter(e => e.isIntersecting);
      if (intersecting.length === 0) return;
      intersecting.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
      setActive(intersecting[0].target.id);
    }, { rootMargin: "-20% 0px -60% 0px", threshold: 0 });

    sections.forEach(s => {
      const el = document.getElementById(s.id);
      if (el) observer.observe(el);
    });
    return () => observer.disconnect();
  }, [sections]);

  const jumpTo = (e, id) => {
    e.preventDefault();
    const el = document.getElementById(id);
    if (!el) return;
    const top = el.getBoundingClientRect().top + window.scrollY - 120;
    window.scrollTo({ top, behavior: "smooth" });
  };

  return (
    <nav
      className={"dossier-nav" + (expanded ? " expanded" : "") + (visible ? " visible" : "")}
      onMouseEnter={() => setExpanded(true)}
      onMouseLeave={() => setExpanded(false)}
      aria-label="Sections of this dossier"
    >
      <div className="dn-eyebrow">Sections</div>
      {sections.map((s) => (
        <a
          key={s.id}
          href={"#" + s.id}
          className={"dn-item" + (active === s.id ? " active" : "")}
          onClick={(e) => jumpTo(e, s.id)}
        >
          <span className="dn-tick" aria-hidden="true" />
          <span className="dn-numeral">{s.numeral}</span>
          <span className="dn-label">{s.label}</span>
        </a>
      ))}
    </nav>
  );
}

window.DossierNav = DossierNav;

/* ---------- EssayReader, Web Speech API text-to-speech for the essay ---------- */
function stripForSpeech(html) {
  if (!html) return "";
  return String(html)
    .replace(/\[VERIFY[^\]]*\]/g, "")           // strip editor markers
    .replace(/<[^>]+>/g, "")                     // strip HTML tags
    .replace(/&mdash;|&ndash;/g, ", ")         // ensure pause around dashes
    .replace(/&ldquo;|&rdquo;/g, '"')
    .replace(/&lsquo;|&rsquo;/g, "'")
    .replace(/&amp;/g, "&")
    .replace(/&nbsp;/g, " ")
    .replace(/, /g, ", ")                       // pad em-dashes for natural pause
    .replace(/\.\.\./g, "… ")                   // ellipsis breath
    .replace(/\s+/g, " ")
    .trim();
}

/* Tiered voice quality, higher = more natural-sounding.
   Browsers expose a wild mix, from robotic eSpeak to modern neural TTS. */
function voiceQuality(v) {
  if (!v) return 0;
  const n = (v.name || "").toLowerCase();
  // Modern neural voices (Microsoft Edge "Online", Google Cloud, etc.)
  if (/\bneural\b|\bonline\b|wavenet/.test(n)) return 5;
  // Apple's natural-language voices
  if (/\bsiri\b|\bava\b|\bzoe\b|\bevan\b|\bnoelle\b|\bnoor\b/.test(n)) return 4;
  if (/\b(premium|enhanced|natural)\b/.test(n)) return 4;
  // Chrome's built-in Google voices (better than basic OS voices)
  if (/^google /.test(n)) return 3;
  // Microsoft's standard non-online voices
  if (/^microsoft /.test(n)) return 2;
  return 1;
}
function voiceQualityLabel(v) {
  const q = voiceQuality(v);
  return q === 5 ? "Neural" :
         q === 4 ? "Enhanced" :
         q === 3 ? "Natural" :
         q === 2 ? "Standard" :
                   "Basic";
}

function pickBestVoice(voices) {
  if (!voices.length) return null;
  // Explicit user-requested default
  const explicit = voices.find(v => /google uk english male/i.test(v.name));
  if (explicit) return explicit;
  // Filter to English first
  const en = voices.filter(v => v.lang && v.lang.startsWith("en"));
  const pool = en.length ? en : voices;
  // Sort by quality, then prefer US/GB locale, then local-service (lower latency)
  const ranked = pool.slice().sort((a, b) => {
    const qd = voiceQuality(b) - voiceQuality(a);
    if (qd !== 0) return qd;
    const localeScore = (v) => (v.lang === "en-US" ? 2 : v.lang === "en-GB" ? 1 : 0);
    const ld = localeScore(b) - localeScore(a);
    if (ld !== 0) return ld;
    return (b.localService ? 1 : 0) - (a.localService ? 1 : 0);
  });
  return ranked[0];
}

/* ---------- ElevenLabs Speech config ---------- */
const ELEVENLABS_API_KEY = "sk_630be5130ce13933ca427b47eb1eae1509637a37769c34ca";

// Curated set of ElevenLabs voices. IDs are stable across the platform.
const ELEVENLABS_VOICES = [
  // Narration-leaning male voices first, best fit for an essay reader
  { id: "JBFqnCBsd6RMkjVDRZzb", label: "George, British, warm narrator",        accent: "UK" },
  { id: "onwK4e9ZLuTAKqWW03F9", label: "Daniel, British, authoritative news",   accent: "UK" },
  { id: "iP95p4xoKVk53GoZ742B", label: "Chris, American, conversational",       accent: "US" },
  { id: "nPczCjzI2devNBz1zQrb", label: "Brian, American, deep & resonant",      accent: "US" },
  { id: "cjVigY5qzO86Huf0OWal", label: "Eric, American, friendly middle-aged",  accent: "US" },
  { id: "TX3LPaxmHKxFdv7VOQHJ", label: "Liam, American, articulate young",      accent: "US" },
  { id: "pqHfZKP75CvOlQylNhV4", label: "Bill, American, gravelly senior",       accent: "US" },
  // Female voices
  { id: "EXAVITQu4vr4xnSDxMaL", label: "Sarah, American, soft narrator",        accent: "US" },
  { id: "XrExE9yKIg1WjnnlVkGX", label: "Matilda, American, warm",               accent: "US" },
  { id: "Xb7hH8MSUJpSbSDYk0k2", label: "Alice, British, confident",             accent: "UK" },
  { id: "FGY2WhTYpPnrIDTdsKH5", label: "Laura, American, upbeat young",         accent: "US" },
  { id: "9BWtsMINqrJLrRacOk9x", label: "Aria, American, expressive",            accent: "US" },
  { id: "cgSgspJ2msm6clMCkdW9", label: "Jessica, American, conversational",     accent: "US" },
];

const ELEVENLABS_MODELS = [
  { id: "eleven_multilingual_v2", label: "Multilingual v2, highest quality" },
  { id: "eleven_turbo_v2_5",      label: "Turbo v2.5, faster, near-equal quality" },
  { id: "eleven_flash_v2_5",      label: "Flash v2.5, fastest, lower fidelity" },
];

async function elevenLabsSynthesize({ key, voice, text, model, stability, similarity, style }) {
  const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice}?output_format=mp3_44100_128`, {
    method: "POST",
    headers: {
      "xi-api-key": key,
      "Content-Type": "application/json",
      "Accept": "audio/mpeg",
    },
    body: JSON.stringify({
      text,
      model_id: model || "eleven_turbo_v2_5",
      voice_settings: {
        stability: typeof stability === "number" ? stability : 0.45,
        similarity_boost: typeof similarity === "number" ? similarity : 0.75,
        style: typeof style === "number" ? style : 0.15,
        use_speaker_boost: true,
      },
    }),
  });
  if (!res.ok) {
    let detail = "";
    try {
      const j = await res.json();
      detail = j?.detail?.message || j?.detail?.status || JSON.stringify(j).slice(0, 160);
    } catch (e) {
      detail = await res.text().catch(() => "");
    }
    throw new Error("Voice synthesis error " + res.status + (detail ? ": " + detail.slice(0, 180) : ""));
  }
  const blob = await res.blob();
  return URL.createObjectURL(blob);
}

function EssayReader({ dek, paragraphs, onSegmentChange, albumTitle, artist, album, primary, audioId = "readaloud" }) {
  const supported = typeof window !== "undefined" && "speechSynthesis" in window;
  const segments = useARef([]);
  if (segments.current.length === 0) {
    segments.current = [stripForSpeech(dek), ...paragraphs.map(stripForSpeech)].filter(Boolean);
  }

  const [state, setState] = useAState("idle"); // idle | playing | paused
  const [idx, setIdx] = useAState(0);
  const [voices, setVoices] = useAState([]);
  const [voiceName, setVoiceName] = useAState("");
  const [rate, setRate] = useAState(1.1);
  const [settingsOpen, setSettingsOpen] = useAState(false);
  const [offscreen, setOffscreen] = useAState(false);
  const rootRef = useARef(null);
  const playPauseRef = useARef(null);
  const onStopRef = useARef(null);

  // ElevenLabs Speech (premium neural voices, API key baked in)
  const [elevenEnabled, setElevenEnabled] = useAState(true);
  const [elevenKey, setElevenKey] = useAState(ELEVENLABS_API_KEY);
  const [elevenVoice, setElevenVoice] = useAState("JBFqnCBsd6RMkjVDRZzb"); // George, British narrator
  const [elevenModel, setElevenModel] = useAState("eleven_turbo_v2_5");
  const [elevenStatus, setElevenStatus] = useAState("idle"); // idle | testing | ok | error | loading
  const [elevenError, setElevenError] = useAState("");
  const elevenAudioRef = useARef(null);
  const elevenCacheRef = useARef({}); // {paragraphIdx: Promise<objectUrl>}

  // Load saved ElevenLabs config on mount
  useAEffect(() => {
    try {
      const raw = localStorage.getItem("bbr:eleven-tts");
      if (raw) {
        const cfg = JSON.parse(raw);
        if (typeof cfg.enabled === "boolean") setElevenEnabled(cfg.enabled);
        if (typeof cfg.key === "string" && cfg.key) setElevenKey(cfg.key);
        if (typeof cfg.voice === "string") setElevenVoice(cfg.voice);
        if (typeof cfg.model === "string") setElevenModel(cfg.model);
      }
      // Clean up any leftover Azure state
      localStorage.removeItem("bbr:azure-tts");
    } catch (e) {}
  }, []);

  const saveElevenCfg = (patch) => {
    try {
      const cur = JSON.parse(localStorage.getItem("bbr:eleven-tts") || "{}");
      localStorage.setItem("bbr:eleven-tts", JSON.stringify({ ...cur, ...patch }));
    } catch (e) {}
  };

  const elevenActive = elevenEnabled && !!elevenKey && !!elevenVoice;

  // Invalidate cache when voice / model / key changes (rate is applied via <audio>.playbackRate, so no resynth needed)
  useAEffect(() => {
    Object.values(elevenCacheRef.current).forEach(p => {
      if (p && typeof p.then === "function") p.then(u => { try { URL.revokeObjectURL(u); } catch (e) {} }).catch(() => {});
    });
    elevenCacheRef.current = {};
  }, [elevenVoice, elevenModel, elevenKey, elevenEnabled]);
  const idxRef = useARef(0);
  const stateRef = useARef("idle");
  const cancelledRef = useARef(false);

  useAEffect(() => { idxRef.current = idx; }, [idx]);
  useAEffect(() => { stateRef.current = state; }, [state]);
  useAEffect(() => { onSegmentChange && onSegmentChange(state === "idle" ? -1 : idx); }, [state, idx, onSegmentChange]);

  // Sticky mini-player: reveal once the reader scrolls out of view
  useAEffect(() => {
    if (!primary) return;
    const onScroll = () => {
      const el = rootRef.current;
      if (!el) return;
      setOffscreen(el.getBoundingClientRect().bottom < 64);
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener("scroll", onScroll);
  }, [primary]);

  // Hero "Listen to this essay" trigger
  useAEffect(() => {
    if (!primary) return;
    const h = () => { if (stateRef.current !== "playing" && playPauseRef.current) playPauseRef.current(); };
    window.addEventListener("bbr-essay-play", h);
    return () => window.removeEventListener("bbr-essay-play", h);
  }, [primary]);

  // Stop the read-aloud if any OTHER audio source (tracklist, cover previews)
  // starts playing. Keeps only one audio stream active across the page.
  useAEffect(() => {
    const stopHandler = (ev) => {
      const sender = ev?.detail?.id;
      if (sender && sender !== audioId && stateRef.current !== "idle" && onStopRef.current) {
        onStopRef.current();
      }
    };
    window.addEventListener("bbr-audio-play", stopHandler);
    return () => window.removeEventListener("bbr-audio-play", stopHandler);
  }, []);

  // Load available voices (async on Chrome)
  useAEffect(() => {
    if (!supported) return;
    const load = () => {
      const v = window.speechSynthesis.getVoices();
      setVoices(v);
      if (v.length && !voiceName) {
        // Restore last-used voice if it exists, else pick the best available.
        // v2 key forces a re-pick now that the default has shifted to Google UK English Male.
        let saved = null;
        try { saved = localStorage.getItem("bbr:tts-voice:v2"); } catch (e) {}
        const restored = saved && v.find(x => x.name === saved);
        const preferred = restored || pickBestVoice(v);
        if (preferred) setVoiceName(preferred.name);
      }
    };
    load();
    window.speechSynthesis.onvoiceschanged = load;
    return () => {
      cancelledRef.current = true;
      window.speechSynthesis.cancel();
      if (window.speechSynthesis.onvoiceschanged === load) {
        window.speechSynthesis.onvoiceschanged = null;
      }
    };
  }, [supported]);

  const findVoice = () => voices.find(v => v.name === voiceName) || null;

  /* ---- ElevenLabs playback ---- */
  const ensureElevenAudio = () => {
    if (!elevenAudioRef.current) {
      const a = new Audio();
      a.preload = "auto";
      elevenAudioRef.current = a;
      a.addEventListener("ended", () => {
        if (cancelledRef.current) return;
        if (stateRef.current !== "playing") return;
        speakElevenSegment(idxRef.current + 1);
      });
      a.addEventListener("error", () => {
        setElevenStatus("error");
        setElevenError("Audio playback failed");
      });
      a.addEventListener("playing", () => { setElevenStatus("idle"); });
    }
    // Always apply current rate (browser supports 0.25× – 4× without pitch shifting via preservesPitch)
    elevenAudioRef.current.playbackRate = rate;
    elevenAudioRef.current.preservesPitch = true;
    return elevenAudioRef.current;
  };

  const getOrSynth = (i) => {
    if (elevenCacheRef.current[i]) return elevenCacheRef.current[i];
    const p = elevenLabsSynthesize({
      key: elevenKey,
      voice: elevenVoice,
      text: segments.current[i],
      model: elevenModel,
    });
    elevenCacheRef.current[i] = p;
    return p;
  };

  const speakElevenSegment = async (i) => {
    if (i >= segments.current.length) {
      setState("idle"); setIdx(0); setElevenStatus("idle");
      return;
    }
    cancelledRef.current = false;
    setIdx(i);
    setElevenStatus("loading");
    setElevenError("");
    const audio = ensureElevenAudio();
    try {
      const url = await getOrSynth(i);
      if (cancelledRef.current) return;
      audio.src = url;
      audio.currentTime = 0;
      audio.playbackRate = rate;
      await audio.play();
      // Pre-fetch the next paragraph in background
      if (i + 1 < segments.current.length) {
        getOrSynth(i + 1).catch(() => {});
      }
    } catch (e) {
      // Drop cache for this paragraph so the next attempt re-tries
      delete elevenCacheRef.current[i];
      setElevenStatus("error");
      setElevenError(String(e.message || e));
      setState("idle"); setIdx(0);
    }
  };

  const testEleven = async () => {
    setElevenStatus("testing");
    setElevenError("");
    try {
      const voiceLabel = (ELEVENLABS_VOICES.find(v => v.id === elevenVoice)?.label.split(", ")[0]) || "your voice";
      const url = await elevenLabsSynthesize({
        key: elevenKey, voice: elevenVoice, model: elevenModel,
        text: "This is " + voiceLabel + ", narrating The Lead-In.",
      });
      const a = new Audio(url);
      a.playbackRate = rate;
      a.preservesPitch = true;
      await a.play();
      setElevenStatus("ok");
    } catch (e) {
      setElevenStatus("error");
      setElevenError(String(e.message || e));
    }
  };

  const speakSegment = (i) => {
    if (!supported) return;
    if (i >= segments.current.length) {
      setState("idle"); setIdx(0);
      return;
    }
    cancelledRef.current = false;
    setIdx(i);
    const u = new SpeechSynthesisUtterance(segments.current[i]);
    const v = findVoice();
    if (v) u.voice = v;
    u.rate = rate;
    u.pitch = 1;
    u.onend = () => {
      if (cancelledRef.current) return;
      if (stateRef.current !== "playing") return;
      speakSegment(i + 1);
    };
    u.onerror = (e) => {
      if (e && e.error === "canceled") return; // user-initiated, ignore
      setState("idle"); setIdx(0);
    };
    try { window.speechSynthesis.speak(u); } catch (err) {}
  };

  const onPlayPause = () => {
    if (elevenActive) {
      const audio = ensureElevenAudio();
      if (state === "playing") {
        audio.pause();
        setState("paused");
      } else if (state === "paused") {
        window.dispatchEvent(new CustomEvent("bbr-audio-play", { detail: { id: audioId } }));
        audio.play().catch(() => {});
        setState("playing");
      } else {
        cancelledRef.current = true;
        audio.pause();
        // Reset and start
        window.dispatchEvent(new CustomEvent("bbr-audio-play", { detail: { id: audioId } }));
        setTimeout(() => {
          cancelledRef.current = false;
          setState("playing");
          speakElevenSegment(0);
        }, 40);
      }
      return;
    }
    if (!supported) return;
    if (state === "playing") {
      window.speechSynthesis.pause();
      setState("paused");
    } else if (state === "paused") {
      window.dispatchEvent(new CustomEvent("bbr-audio-play", { detail: { id: audioId } }));
      window.speechSynthesis.resume();
      setState("playing");
    } else {
      cancelledRef.current = true;
      window.speechSynthesis.cancel();
      window.dispatchEvent(new CustomEvent("bbr-audio-play", { detail: { id: audioId } }));
      setTimeout(() => {
        cancelledRef.current = false;
        setState("playing");
        speakSegment(0);
      }, 60);
    }
  };

  playPauseRef.current = onPlayPause;

  const onStop = () => {
    cancelledRef.current = true;
    if (elevenActive) {
      const audio = elevenAudioRef.current;
      if (audio) { audio.pause(); audio.currentTime = 0; }
    } else {
      window.speechSynthesis.cancel();
    }
    setState("idle"); setIdx(0); setElevenStatus("idle");
  };
  onStopRef.current = onStop;

  const skip = (delta) => {
    const next = Math.max(0, Math.min(segments.current.length - 1, idxRef.current + delta));
    cancelledRef.current = true;
    if (elevenActive) {
      const audio = elevenAudioRef.current;
      if (audio) audio.pause();
      setTimeout(() => {
        cancelledRef.current = false;
        setState("playing");
        speakElevenSegment(next);
      }, 40);
      return;
    }
    window.speechSynthesis.cancel();
    setTimeout(() => {
      cancelledRef.current = false;
      setState("playing");
      speakSegment(next);
    }, 60);
  };

  // Jump straight to a given paragraph (used by the scrubber).
  const jumpTo = (targetIdx) => {
    const next = Math.max(0, Math.min(segments.current.length - 1, targetIdx));
    if (next === idxRef.current && state !== "idle") return;
    cancelledRef.current = true;
    if (elevenActive) {
      const audio = elevenAudioRef.current;
      if (audio) audio.pause();
      setTimeout(() => {
        cancelledRef.current = false;
        setState("playing");
        speakElevenSegment(next);
      }, 40);
      return;
    }
    window.speechSynthesis.cancel();
    setTimeout(() => {
      cancelledRef.current = false;
      setState("playing");
      speakSegment(next);
    }, 60);
  };

  const onVoiceChange = (name) => {
    setVoiceName(name);
    try { localStorage.setItem("bbr:tts-voice:v2", name); } catch (e) {}
    if (state === "playing" || state === "paused") {
      // Restart current paragraph with new voice
      cancelledRef.current = true;
      window.speechSynthesis.cancel();
      const cur = idxRef.current;
      setTimeout(() => {
        cancelledRef.current = false;
        setState("playing");
        // Use the new voice on the next speak call
        const u = new SpeechSynthesisUtterance(segments.current[cur]);
        const v = voices.find(x => x.name === name);
        if (v) u.voice = v;
        u.rate = rate;
        u.onend = () => {
          if (cancelledRef.current) return;
          if (stateRef.current !== "playing") return;
          speakSegment(cur + 1);
        };
        try { window.speechSynthesis.speak(u); } catch (err) {}
      }, 80);
    }
  };

  const onRateChange = (r) => {
    setRate(r);
    // ElevenLabs: just update the audio element's playbackRate live
    if (elevenActive) {
      const a = elevenAudioRef.current;
      if (a) { a.playbackRate = r; a.preservesPitch = true; }
      return;
    }
    if (state === "playing" || state === "paused") {
      cancelledRef.current = true;
      window.speechSynthesis.cancel();
      const cur = idxRef.current;
      setTimeout(() => {
        cancelledRef.current = false;
        setState("playing");
        const u = new SpeechSynthesisUtterance(segments.current[cur]);
        const v = findVoice();
        if (v) u.voice = v;
        u.rate = r;
        u.onend = () => {
          if (cancelledRef.current) return;
          if (stateRef.current !== "playing") return;
          speakSegment(cur + 1);
        };
        try { window.speechSynthesis.speak(u); } catch (err) {}
      }, 80);
    }
  };

  // Estimated minutes total
  const words = segments.current.reduce((n, s) => n + s.split(/\s+/).length, 0);
  const mins = Math.max(1, Math.round(words / (155 * rate)));
  const total = segments.current.length;
  const pct = state === "idle" ? 0 : ((idx + (state === "playing" || state === "paused" ? 0.5 : 0)) / total) * 100;
  const enVoices = voices.filter(v => v.lang.startsWith("en"));
  // Group voices by quality tier for the dropdown
  const voicesByTier = (() => {
    const tiers = { 5: [], 4: [], 3: [], 2: [], 1: [] };
    enVoices.forEach(v => { tiers[voiceQuality(v)].push(v); });
    Object.values(tiers).forEach(arr => arr.sort((a, b) => a.name.localeCompare(b.name)));
    return tiers;
  })();
  const tierLabels = { 5: "Neural, most natural", 4: "Enhanced", 3: "Natural", 2: "Standard", 1: "Basic" };
  const currentVoice = findVoice();
  const currentQuality = voiceQualityLabel(currentVoice);

  if (!supported) {
    return (
      <div className="essay-reader unsupported">
        <div className="er-main">
          <div className="er-icon" aria-hidden="true">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zM16 7.5v9a4 4 0 0 0 0-9z"/></svg>
          </div>
          <div className="er-info">
            <div className="er-label">Listen to this essay</div>
            <div className="er-meta">Your browser doesn't support text-to-speech. Try Chrome, Edge, or Safari.</div>
          </div>
        </div>
      </div>
    );
  }

  return (
    <>
    <div ref={rootRef} className={"essay-reader" + (state !== "idle" ? " is-active" : "")}>
      <div className="er-main">
        <button
          className={"er-play" + (state === "playing" ? " on" : "")}
          onClick={onPlayPause}
          aria-label={state === "playing" ? "Pause essay" : "Listen to this essay"}
        >
          {state === "playing" ? (
            <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg>
          ) : (
            <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M7 5v14l12-7z"/></svg>
          )}
        </button>
        <div className="er-info">
          <div className="er-label">
            {state === "idle" ? "Listen to this essay" : state === "paused" ? "Paused" : "Reading aloud"}
          </div>
          <div className="er-meta">
            {elevenActive ? (
              <>
                {state === "idle" ? (
                  <>{mins} min · narrated by <b>{(ELEVENLABS_VOICES.find(v => v.id === elevenVoice) || {}).label || elevenVoice}</b> <span className="er-q q-neural">Neural voice</span></>
                ) : elevenStatus === "loading" ? (
                  <>Synthesizing paragraph {idx + 1} of {total}… <span className="er-q q-neural">Neural voice</span></>
                ) : (
                  <>Paragraph {idx + 1} of {total} · <b>{((ELEVENLABS_VOICES.find(v => v.id === elevenVoice) || {}).label || elevenVoice).split(", ")[0]}</b> <span className="er-q q-neural">Neural voice</span></>
                )}
              </>
            ) : state === "idle" ? (
              <>{mins} min · narrated by {currentVoice ? <><b>{currentVoice.name}</b> <span className={"er-q q-" + currentQuality.toLowerCase()}>{currentQuality}</span></> : "your browser"}</>
            ) : (
              <>Paragraph {idx + 1} of {total} · <b>{currentVoice ? currentVoice.name : "system voice"}</b> <span className={"er-q q-" + currentQuality.toLowerCase()}>{currentQuality}</span></>
            )}
          </div>
        </div>
        <div className="er-controls">
          <button className="er-ctl" onClick={() => skip(-1)} disabled={state === "idle"} aria-label="Previous paragraph" title="Previous paragraph">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zM9.5 12l8.5 6V6z"/></svg>
          </button>
          <button className="er-ctl" onClick={() => skip(1)} disabled={state === "idle"} aria-label="Next paragraph" title="Next paragraph">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6l8.5 6L6 18V6zM16 6h2v12h-2z"/></svg>
          </button>
          <button className="er-ctl" onClick={onStop} disabled={state === "idle"} aria-label="Stop" title="Stop">
            <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12"/></svg>
          </button>
          <button className={"er-ctl er-cog" + (settingsOpen ? " on" : "")} onClick={() => setSettingsOpen(!settingsOpen)} aria-label="Voice settings" title="Voice & speed">
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
              <circle cx="12" cy="12" r="3"/>
              <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
            </svg>
          </button>
        </div>
      </div>
      <div
        className="er-scrub"
        role="slider"
        tabIndex={0}
        aria-label="Scrub to paragraph"
        aria-valuemin={1}
        aria-valuemax={total}
        aria-valuenow={Math.min(total, idx + 1)}
        onPointerDown={(e) => {
          if (!total) return;
          const bar = e.currentTarget;
          bar.setPointerCapture(e.pointerId);
          const seek = (clientX) => {
            const rect = bar.getBoundingClientRect();
            const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
            jumpTo(Math.floor(ratio * total));
          };
          seek(e.clientX);
          const move = (ev) => seek(ev.clientX);
          const up = (ev) => {
            try { bar.releasePointerCapture(e.pointerId); } catch (err) {}
            bar.removeEventListener("pointermove", move);
            bar.removeEventListener("pointerup", up);
            bar.removeEventListener("pointercancel", up);
          };
          bar.addEventListener("pointermove", move);
          bar.addEventListener("pointerup", up);
          bar.addEventListener("pointercancel", up);
        }}
        onKeyDown={(e) => {
          if (e.key === "ArrowLeft") { e.preventDefault(); jumpTo(idxRef.current - 1); }
          else if (e.key === "ArrowRight") { e.preventDefault(); jumpTo(idxRef.current + 1); }
          else if (e.key === "Home") { e.preventDefault(); jumpTo(0); }
          else if (e.key === "End") { e.preventDefault(); jumpTo(total - 1); }
        }}
      >
        <div className="er-scrub-track" aria-hidden="true">
          <span className="er-scrub-fill" style={{ width: pct + "%" }} />
          <span className="er-scrub-thumb" style={{ left: pct + "%" }} />
        </div>
        <div className="er-scrub-meta" aria-hidden="true">
          <span>Paragraph {Math.min(total, idx + 1)} of {total}</span>
          <span>{Math.round(pct)}%</span>
        </div>
      </div>
      {settingsOpen && (
        <div className="er-settings">
          <label className="er-field">
            <span className="er-field-label">Voice</span>
            <select
              value={elevenVoice}
              onChange={(e) => { setElevenVoice(e.target.value); saveElevenCfg({ voice: e.target.value }); }}
            >
              <optgroup label="British">
                {ELEVENLABS_VOICES.filter(v => v.accent === "UK").map(v => <option key={v.id} value={v.id}>{v.label}</option>)}
              </optgroup>
              <optgroup label="American">
                {ELEVENLABS_VOICES.filter(v => v.accent === "US").map(v => <option key={v.id} value={v.id}>{v.label}</option>)}
              </optgroup>
            </select>
          </label>
          <label className="er-field">
            <span className="er-field-label">Speed <b>{rate.toFixed(2)}×</b></span>
            <input
              type="range"
              min="0.7"
              max="1.6"
              step="0.05"
              value={rate}
              onChange={(e) => onRateChange(Number(e.target.value))}
            />
          </label>
        </div>
      )}
    </div>
    {primary && state !== "idle" && offscreen && (
      <div className="essay-mini" role="complementary" aria-label="Essay narration player">
        <div className="em-cover">{album && <Sleeve album={album} size={40} />}</div>
        <button className="em-play" onClick={onPlayPause} aria-label={state === "playing" ? "Pause" : "Play"}>
          {state === "playing" ? (
            <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg>
          ) : (
            <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M7 5v14l12-7z"/></svg>
          )}
        </button>
        <div className="em-mid">
          <div className="em-title">Narrating · <b>{albumTitle}</b></div>
          <div
            className="em-scrub"
            role="slider"
            aria-label="Scrub narration"
            aria-valuemin={1}
            aria-valuemax={total}
            aria-valuenow={Math.min(total, idx + 1)}
            onPointerDown={(e) => {
              const bar = e.currentTarget;
              const rect = bar.getBoundingClientRect();
              const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
              jumpTo(Math.floor(ratio * total));
            }}
          >
            <span className="em-fill" style={{ width: pct + "%" }} />
            <span className="em-thumb" style={{ left: pct + "%" }} />
          </div>
        </div>
        <span className="em-count">¶ {Math.min(total, idx + 1)}/{total}</span>
        <button className="em-x" onClick={onStop} aria-label="Stop narration">×</button>
      </div>
    )}
    </>
  );
}

window.EssayReader = EssayReader;

/* ---------- AlbumCrumbs, back navigation at the top of every album page ---------- */
function AlbumCrumbs({ album, onList, onHome, all, onAlbum }) {
  const prev = all && onAlbum ? all.find(a => a.rank === album.rank + 1) : null;
  const next = all && onAlbum ? all.find(a => a.rank === album.rank - 1) : null;
  const pad = (r) => String(r).padStart(3, "0");
  return (
    <nav className="album-crumbs" aria-label="Where you are">
      <button className="crumb-back" onClick={onList}>
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M19 12H5M12 19l-7-7 7-7" />
        </svg>
        <span className="crumb-back-text">
          <span className="crumb-back-eyebrow">Return to</span>
          <span className="crumb-back-title">The Hundred greatest records</span>
        </span>
      </button>
      <span className="crumb-current">
        <span className="crumb-rank">№ {String(album.rank).padStart(3, "0")}</span>
        <span className="crumb-sep" aria-hidden="true">·</span>
        <em className="crumb-title">{album.title}</em>
      </span>
      {(prev || next) && (
        <span className="crumb-seq">
          <button className="crumb-arrow" disabled={!prev} onClick={() => prev && onAlbum(prev)} title={prev ? "№ " + pad(prev.rank) + ", " + prev.title : "No earlier entry"} aria-label="Previous entry">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>
            {prev ? pad(prev.rank) : "—"}
          </button>
          <button className="crumb-arrow" disabled={!next} onClick={() => next && onAlbum(next)} title={next ? "№ " + pad(next.rank) + ", " + next.title : "No later entry"} aria-label="Next entry">
            {next ? pad(next.rank) : "—"}
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>
          </button>
        </span>
      )}
      <button className="crumb-home" onClick={onHome} aria-label="The Lead-In, home">
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M3 12l9-9 9 9M5 10v10h14V10" />
        </svg>
        <span>Home</span>
      </button>
    </nav>
  );
}

window.AlbumCrumbs = AlbumCrumbs;
