/* HomeManifesto, opinionated editorial homepage.
   8 sections: hero, premise, methodology, proof, top-10 glimpse, contract, newsletter, sign-off.
   Voice: confident, slightly combative music criticism. Real album examples throughout. */

const { useState: useHMfState, useEffect: useHMfEffect, useMemo: useHMfMemo } = React;

/* The opening salvo. */
const HERO = {
  line: "A hundred albums. Eight thousand words each. Every placement defended in writing, including the ones that will lose us friends.",
  sub: "The Lead-In publishes one definitive deep dive a week, pressing guides, runout etchings, scoring data, the works, until the canon is properly written down."
};

/* Six-dimension scoring system, with a one-line explainer per dimension. */
const SCORING_DIMENSIONS = [
{ key: "songwriting", label: "Songwriting", note: "The actual songs. Melody, lyric, chord motion, structure. Whether you can hum it after one listen and still want to hear it after a hundred." },
{ key: "production", label: "Production", note: "What the producer and engineer actually did to the air in the room. Microphone choices, tape vs. digital, where the reverb is, why the bass is louder than it should be." },
{ key: "performance", label: "Performance", note: "Who is in the room and what they did when the tape rolled. The first take vs. the eighteenth. The drum fill nobody asked for." },
{ key: "innovation", label: "Innovation", note: "Whether the record did something nobody had done before, and whether anyone could have known to ask for it." },
{ key: "influence", label: "Influence", note: "What it caused. The bands it spawned, the techniques it normalized, the records that couldn't have existed without it." },
{ key: "cohesion", label: "Cohesion", note: "Whether it works as one continuous artifact, side A through side B, or whether it's a singles compilation in a sleeve. Side breaks count." }];


/* Three featured proof essays. WGO is real (live deep dive). The other two are
   plausible openers in the same critic voice, they read as if the essays exist. */
const PROOF_ALBUMS = [
{
  slug: "nevermind",
  rank: 30,
  title: "Nevermind",
  artist: "Nirvana",
  year: 1991,
  readTime: 12,
  score: 8.9,
  sleeve: { bg: "#0b3c6b", fg: "#f5d76e", style: "bold", accent: "#f5d76e" },
  eyebrow: "The placement that starts the argument",
  pull: "Thirty is not a slight; it's an argument that Nevermind matters most for what it did to the industry, and second-most for what it did to your bedroom.",
  affiliate: "https://www.amazon.co.uk/s?k=Nevermind+vinyl&tag=theleadin1-21&i=popular",
  body: "Butch Vig spent three weeks at Sound City making a punk record sound like an FM single, compressed snare, doubled vocals, chorus pedal on every overdub, and Kurt Cobain spent the rest of his life telling people he hated the mix. He was wrong. The compromise is the record. Without it the songs are a B-side; with it they are the moment American rock radio quietly became something else for a decade. We rank it for what it sounds like, not for what it changed. What it changed is in the essay.",
  href: true
},
{
  slug: "ready-to-die",
  rank: 49,
  title: "Ready to Die",
  artist: "The Notorious B.I.G.",
  year: 1994,
  readTime: 14,
  score: 8.7,
  sleeve: { bg: "#f1ece1", fg: "#1a1a1a", style: "portrait", accent: "#b91c1c" },
  eyebrow: "The deeper-cut argument",
  pull: "Forty-nine for Ready to Die is the kind of placement that will get an email from someone's older brother. We've read the email. We stand by forty-nine.",
  affiliate: "https://www.amazon.co.uk/s?k=Notorious+BIG+Ready+to+Die+vinyl&tag=theleadin1-21",
  body: "The skits don't age. The Premier and Easy Mo Bee beats absolutely do, 'Juicy,' 'Big Poppa,' 'Unbelievable,' 'Warning,' all canon, all still playing in the cars we want to be in. But there are seven minutes of this record we never need to hear again, and a hip-hop debut from 1994 that was held together by Sean Combs's pop instincts is a slightly different artifact in 2026 than it was in 1995. We move it down a tier for the seven minutes. We hold it where we hold it for the canon, which is permanent.",
  href: true
},
{
  slug: "off-the-wall",
  rank: 33,
  title: "Off the Wall",
  artist: "Michael Jackson",
  year: 1979,
  readTime: 13,
  score: 9.0,
  sleeve: { bg: "#0e0e0e", fg: "#f5e6c8", style: "portrait", accent: "#d4a574" },
  eyebrow: "The placement we'll defend in the comments",
  pull: "Thirty-three, ahead of Thriller, ahead of every disco record we love more, ahead of half of what our colleagues at this magazine wrote up before us. We'll defend it.",
  affiliate: "https://www.amazon.co.uk/s?k=Off%20The%20Wall+vinyl&tag=theleadin1-21&i=popular",
  body: "Quincy Jones told Bruce Swedien to mic the room the way Marvin Gaye had asked his engineers to mic him for What's Going On, every drum, every horn, every overdub captured with one foot in the studio and one foot on the dance floor. The result is a Black pop record so well-engineered it survived disco's collapse, every fashion cycle since, and a thousand fan edits that all sound worse than the album mixes. Thriller is more important. Off the Wall is better, song for song, and we will fight about that in the comments for a month.",
  href: true
}];


/* The contract, five commitments, written as commitments, not features. */
const CONTRACT_ITEMS = [
{
  label: "One deep dive a week",
  body: "Six-to-eighteen-thousand words. Pressing guide, runout etchings, session reconstruction, scoring data, comments on every placement. Published Wednesday morning, Pacific time, every week of the year. We've missed two weeks in three years and we owe you essays for both."
},
{
  label: "An AI podcast for every entry",
  body: "Maya and Dev, two AI co-hosts we've trained on the entire archive, record a thirty-to-forty-minute episode for every album we publish. They argue. They disagree with us in writing. They are not a marketing channel; they are the second opinion."
},
{
  label: "Pressing guides with real money in them",
  body: "Every dossier ends with a collector's corner: every notable pressing rated, every notable pressing's NM/VG+ value tracked quarterly against market medians, runout etchings transcribed, the one to buy and the three to skip."
},
{
  label: "A scoring system that moves",
  body: "The composite isn't frozen. Cultural momentum is a real input. When something happens, a reissue, a death, a sample, a generation, the score updates and the change is logged in a public history at the bottom of the page. Nothing is hidden."
},
{
  label: "Comments where we actually show up",
  body: "Every essay has an open comment thread and the editor of the piece replies, not the social-media intern, the editor, for at least the first thirty days. If we're wrong about a runout etching or a session date, we fix it in the essay and credit you in the changelog."
}];


/* ========================================================
   Top-level
   ======================================================== */
function HomeManifesto({ albums, onAlbum, onList, user, authReady, onCollection, onLeaderboards, onAuthOpen }) {
  const top10 = useHMfMemo(() => albums.slice(0, 10), [albums]);

  // If the visitor arrived via an album page's "How we score" link, scroll to
  // the methodology section once the homepage has rendered.
  useHMfEffect(() => {
    if (window.__scrollToScore) {
      window.__scrollToScore = false;
      requestAnimationFrame(() => {
        const el = document.getElementById("how-we-score");
        if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 90, behavior: "smooth" });
      });
    }
  }, []);

  // Scroll-reveal: sections fade + rise as they enter the viewport. Added pre-
  // paint so there's no flash; falls back to fully-visible if IO is missing.
  React.useLayoutEffect(() => {
    const els = Array.prototype.slice.call(document.querySelectorAll(".manifesto-home > section"));
    if (!els.length) return;
    if (!("IntersectionObserver" in window)) { els.forEach((e) => e.classList.add("in")); return; }
    els.forEach((e) => e.classList.add("hd-reveal"));
    const io = new IntersectionObserver((entries) => {
      entries.forEach((en) => { if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); } });
    }, { rootMargin: "0px 0px -8% 0px", threshold: 0.08 });
    els.forEach((e) => io.observe(e));
    return () => io.disconnect();
  }, [user, authReady]);

  const featuredAlbums = useHMfMemo(
    () => PROOF_ALBUMS.map((p) => ({ ...p, album: albums.find((a) => a.slug === p.slug) })),
    [albums]
  );

  // This week's deep dive — manual featured slug (no publish dates in the data).
  const featured = useHMfMemo(
    () => albums?.find((a) => a.slug === (window.BBR_FEATURED_SLUG || "dark-side-of-the-moon")) || albums?.[0],
    [albums]
  );

  // Don't flash the logged-out hero to a member while the session resolves.
  if (!authReady) {
    return (
      <div className="manifesto-home home-skeleton" aria-busy="true">
        <div className="home-skel-hero" />
        <div className="home-skel-band" />
        <div className="home-skel-band" />
      </div>);
  }

  // ---- Logged-in: collection-first retention ordering -------------------
  // A personal command center (value, canon progress, genre blend) leads, then
  // the live standings (where you rank + the climb), then the buy-loop gaps,
  // then editorial. Built on the member's real collection + leaderboard data.
  if (user) {
    return (
      <div className="manifesto-home" data-screen-label="Home · member">
        <CollectionCommandCenter albums={albums} user={user} onCollection={onCollection} onList={onList} />
        <LeaderboardStandings user={user} onLeaderboards={onLeaderboards} />
        <GapsBand albums={albums} />
        <WeeklyDeepDive album={featured} onAlbum={onAlbum} />
        <ManifestoTop10 top10={top10} onAlbum={onAlbum} onList={onList} />
        <ManifestoSignoff onList={onList} />
      </div>);
  }

  // ---- Logged-out: 50/50 dual hero acquisition ordering -----------------
  return (
    <div className="manifesto-home" data-screen-label="Home · visitor">
      <DualHero albums={albums} featured={featured} onList={onList} onCollection={onCollection} />
      <WeeklyDeepDive album={featured} onAlbum={onAlbum} />
      <EditorialStats />
      <DeepDiveArchive albums={albums} onAlbum={onAlbum} />
      <EditorialSignup />
      <CollectorShowcase albums={albums} onCollection={onCollection} onLeaderboards={onLeaderboards} />
      <CommunityBand onList={onList} onCollection={onCollection} />
      <ManifestoProof items={featuredAlbums} onAlbum={onAlbum} onList={onList} />
      <ManifestoTop10 top10={top10} onAlbum={onAlbum} onList={onList} />
      <ClosingDualCTA onList={onList} onCollection={onCollection} />
    </div>);
}

/* ---- 1. Opening salvo, dynamic editorial front page -------------------- */
/* A magazine-cover spread: the argument headline as the lead, and this week's
   deep dive as a live visual feature, album sleeve with a vinyl record
   rotating out from behind it. Combines the old hero + "this week" chip. */
function ManifestoHero({ onList, album, onAlbum }) {
  const full = album && window.ESSAYS_FULL && window.ESSAYS_FULL[album.slug];
  const dek =
    (full && full.essay && full.essay.dek) ||
    (full && full.tagline) ||
    "The week's definitive deep dive, pressing guide, session reconstruction, scoring data, and an argument we'll defend in the comments.";

  // Today's date, formatted for the masthead, e.g. "Saturday, 7 June 2026".
  const today = new Date().toLocaleDateString("en-GB", {
    weekday: "long", day: "numeric", month: "long", year: "numeric",
  });

  return (
    <section className="mf-hero2" data-screen-label="Hero">
      <div className="mf-hero2-watermark" aria-hidden="true">C</div>

      <div className="mf-hero2-meta">
        <div className="mf-mast">
          <span className="mf-mast-dot" />
          The Lead-In
          <span className="mf-mast-sep" />
          The Hundred
          <span className="mf-mast-sep" />
          Established 2026
        </div>
        <div className="mf-hero2-counter">{today}</div>
      </div>

      <div className="mf-hero2-grid">
        <div className="mf-hero2-lead">
          <div className="mf-hero2-eyebrow">The one hundred greatest albums, defended in writing</div>
          <h1 className="mf-hero2-headline">
            <span className="mf-hero2-hl-lead">A hundred albums.</span>
            <span className="mf-hero2-hl-rest">Eight thousand words each. Every placement defended in writing &mdash; including the ones that will lose us friends.</span>
          </h1>
          <div className="mf-hero2-foot">
            <p className="mf-hero2-sub">{HERO.sub}</p>
            <button className="mf-hero2-cta" onClick={onList}>
              <span className="mf-hero2-cta-label">Read the ranked one hundred</span>
              <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>
            </button>
          </div>
        </div>

        {album &&
        <aside className="mf-hero2-feature">
          <button
            className="mf-feat"
            onClick={() => onAlbum && onAlbum(album)}
            aria-label={`Read this week's essay: ${album.title}`}>
            <div className="mf-feat-kicker">
              <span className="mf-feat-pulse" />
              This week&rsquo;s deep dive
            </div>

            <div className="mf-feat-disc">
              <div className="mf-feat-vinyl" aria-hidden="true" />
              <div className="mf-feat-sleeve">
                <img src={"/covers/" + album.slug + ".jpg"} alt="" loading="lazy" draggable="false" />
                <span className="mf-feat-rank">№ {String(album.rank).padStart(3, "0")}</span>
              </div>
            </div>

            <div className="mf-feat-meta">
              <h2 className="mf-feat-title">
                <span className="mf-feat-name">{album.title}</span>
              </h2>
              <div className="mf-feat-artist">{album.artist} · {album.year}</div>
              <p className="mf-feat-dek">{dek}</p>
              <span className="mf-feat-read">
                Read the essay
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                  <path d="M5 12h14M13 5l7 7-7 7" />
                </svg>
              </span>
            </div>
          </button>
        </aside>}
      </div>
    </section>);

}

/* ---- 1b. Kinetic ticker, relocated as an ambient divider ---------------- */
/* Was the first moving element above the hero; now bridges the visual cover
   wall and the editorial argument below it. Functionally ambient. */
function ManifestoTickerDivider({ albums }) {
  const marqueeSource = useHMfMemo(() => {
    if (!albums || !albums.length) return [];
    const picks = [
    ...albums.slice(0, 3),
    albums[6], albums[11], albums[18], albums[24],
    albums[31], albums[42], albums[55], albums[68],
    albums[79], albums[91]].
    filter(Boolean);
    return picks;
  }, [albums]);
  if (!marqueeSource.length) return null;
  const marqueeItems = [...marqueeSource, ...marqueeSource];

  return (
    <div className="mf-ticker-divider" aria-hidden="true">
      <div className="mf-hero-ticker">
        <div className="mf-hero-ticker-track">
          {marqueeItems.map((a, i) =>
          <span key={i} className="mf-hero-ticker-item">
              <em className="mf-hero-ticker-rank">№ {String(a.rank).padStart(3, "0")}</em>
              <span className="mf-hero-ticker-title">{a.title}</span>
              <span className="mf-hero-ticker-artist">{a.artist}</span>
              <span className="mf-hero-ticker-sep" />
            </span>
          )}
        </div>
      </div>
    </div>);

}

/* ---- 1.5 The cover wall ------------------------------------------------
   Dense mosaic of real album sleeves drawn from a diverse spread of the
   ranking. This is intentionally not text-heavy, the imagery is the
   argument. The whole grid is a clickable index into the list. */
function ManifestoCoverWall({ albums, onAlbum }) {
  // A curated, balanced spread of ranks, top of the list, mid-tier, and
  // deeper cuts. 24 tiles fits a 12×2 marquee or 8×3 grid cleanly.
  const RANKS = [
  1, 6, 9, 12, 16, 22, 27, 36, 44, 51, 58, 64,
  3, 8, 14, 19, 25, 31, 39, 46, 54, 60, 71, 83];

  const picks = useHMfMemo(() => {
    if (!albums || !albums.length) return [];
    return RANKS.map((r) => albums.find((a) => a.rank === r)).filter(Boolean);
  }, [albums]);

  const liveCount = useHMfMemo(
    () => picks.filter((a) => window.ESSAYS_FULL && window.ESSAYS_FULL[a.slug]).length,
    [picks]
  );

  return (
    <section className="mf-section mf-cover-wall" data-screen-label="Cover wall">
      <div className="mf-cw-banner">
        <div className="mf-cw-lede">
          <span className="mf-cw-eyebrow">No filler, no roundups</span>
          <h2 className="mf-cw-h2">
            One hundred sleeves. One hundred essays. <em>One ranked argument.</em>
          </h2>
        </div>
        <div className="mf-cw-stats">
          <div className="mf-cw-stat">
            <div className="mf-cw-stat-val">100</div>
            <div className="mf-cw-stat-lbl">Albums ranked</div>
          </div>
          <div className="mf-cw-stat">
            <div className="mf-cw-stat-val">1.4M</div>
            <div className="mf-cw-stat-lbl">Words published</div>
          </div>
          <div className="mf-cw-stat">
            <div className="mf-cw-stat-val">Q1 ’26</div>
            <div className="mf-cw-stat-lbl">Last revision</div>
          </div>
        </div>
      </div>

      <div className="mf-cw-grid" role="list">
        {picks.map((a, i) => {
          const isLive = !!(window.ESSAYS_FULL && window.ESSAYS_FULL[a.slug]);
          return (
          <button
            key={a.rank}
            type="button"
            role="listitem"
            className={"mf-cw-tile" + (isLive ? " is-live" : " is-coming")}
            style={{ animationDelay: i * 28 + "ms" }}
            onClick={() => onAlbum && onAlbum(a)}
            aria-label={`№ ${a.rank} ${a.title} by ${a.artist}, read the deep dive`}>
          
            <img
            src={"/covers/" + a.slug + ".jpg"}
            alt=""
            loading="lazy"
            draggable="false" />

            <span className={"mf-cw-status" + (isLive ? " is-live" : "")} aria-hidden="true">
              <span className="mf-cw-status-dot" />
              {isLive ? "Live" : "Coming"}
            </span>

            <span className="mf-cw-overlay" aria-hidden="true">
              <span className="mf-cw-tile-rank">№ {String(a.rank).padStart(3, "0")}</span>
              <span className="mf-cw-tile-title">{a.title}</span>
              <span className="mf-cw-tile-art">{a.artist}</span>
            </span>
          </button>
          );
        })}
      </div>

      <div className="mf-cw-foot">
        <span className="mf-cw-foot-text">
          Twenty-four covers, drawn from across the ranking.
          <span className="mf-cw-legend">
            <span className="mf-cw-legend-item"><span className="mf-cw-status-dot is-live" /> Essay live</span>
          </span>
          Every one of the hundred has a full deep dive and a public revision history.
        </span>
        <span className="mf-cw-foot-mono">{liveCount}/24 live</span>
      </div>
    </section>);

}

/* ---- 2. The premise ----------------------------------------------------- */
function ManifestoPremise() {
  return (
    <section className="mf-section mf-premise" data-screen-label="Premise">
      <div className="mf-section-rule" style={{ padding: "24px 0px 28px" }}>
        <span className="mf-section-num">§ 01</span>
        <span className="mf-section-label">Why this exists</span>
      </div>
      <div className="mf-premise-grid" style={{ padding: "0px" }}>
        <h2 className="mf-h2">
          Music writing got safe. The records didn't.
        </h2>
        <div className="mf-premise-body">
          <p className="mf-premise-thesis">
            What follows is one hundred essays, one ranking, and an argument we are happy to have with you in writing.
          </p>
          <p>
            Every other "greatest albums" list on the internet is one of three things: a clickbait slideshow built for ad impressions, a Rolling Stone reprint with the rankings nudged for the year, or an algorithmically generated playlist with the headline rewritten by a marketing intern. None of them treat albums like the artifacts they are. None of them care about side breaks, runout etchings, or the producer's marriage. None of them are willing to put Tago Mago at thirty-seven and stand behind it.
          </p>
          <p>
            We do this because the alternative is letting the canon be written by people whose only relationship to a record is the streaming royalties on it. Vinyl is the format the canon was made on; it imposes length, sequence, fidelity, and stakes. A digital playlist has none of those things. A list of one hundred albums, properly written and properly defended, is a small act of resistance against a culture that wants you to forget the format ever mattered.
          </p>
        </div>
      </div>
    </section>);

}

/* ---- 3. The methodology -------------------------------------------------- */
/* Six scoring dimensions for the worked example. There is no per-dimension
   data in the dataset, so this breakdown is authored to demonstrate the grid
   on a mid-table classic, Rumours (№ 36), rather than repeat the #1 album,
   which already leads the homepage as the flagship deep dive. The numbers
   reflect critical consensus: reference-grade songwriting, production and
   cohesion; comparatively modest on innovation for a record that perfected
   rather than invented its form. */
const EXAMPLE_SCORE = {
  composite: 8.9,
  dims: [
  { key: "songwriting", label: "Songwriting", value: 9.4 },
  { key: "production", label: "Production", value: 9.5 },
  { key: "performance", label: "Performance", value: 9.0 },
  { key: "innovation", label: "Innovation", value: 7.6 },
  { key: "influence", label: "Influence", value: 8.8 },
  { key: "cohesion", label: "Cohesion", value: 9.3 }]
};

function ManifestoMethodology({ album, onAlbum }) {
  // Use the canonical scoring function (same one the deep-dive ScoreBreakdown
  // uses) so the composite + dimensions shown here exactly match the album's
  // own page. Fall back to the static example only if it hasn't loaded yet.
  const score =
    (album && window.bbrScoreFor && window.bbrScoreFor(album)) || EXAMPLE_SCORE;
  return (
    <section id="how-we-score" className="mf-section mf-method" data-screen-label="Methodology" style={{ margin: "24px 0px 2px", padding: "0px 56px 40px" }}>
      <div className="mf-section-rule" style={{ padding: "44px 0px 28px" }}>
        <span className="mf-section-num">§ 02</span>
        <span className="mf-section-label">How the ranking is made</span>
      </div>

      <h2 className="mf-h2 mf-h2-method">
        Six dimensions. One composite. Every placement defended in writing.
      </h2>

      <div className="mf-method-grid">
        {SCORING_DIMENSIONS.map((d, i) =>
        <div key={d.key} className="mf-dim">
            <div className="mf-dim-head">
              <span className="mf-dim-num">{String(i + 1).padStart(2, "0")}</span>
              <span className="mf-dim-label">{d.label}</span>
            </div>
            <p className="mf-dim-note">{d.note}</p>
          </div>
        )}
      </div>

      {/* Worked example, abstract criteria, made concrete on the #1 album */}
      <div className="mf-worked">
        <div className="mf-worked-cap">Here&rsquo;s what the scoring looks like in practice</div>
        <div className="mf-worked-card">
          <button
            className="mf-worked-head"
            onClick={() => album && onAlbum && onAlbum(album)}
            aria-label={album ? `Open the deep dive for ${album.title}` : "The #1 album"}>
            {album &&
            <span className="mf-worked-cover" aria-hidden="true">
              <img src={"/covers/" + album.slug + ".jpg"} alt="" loading="lazy" />
            </span>}
            <span className="mf-worked-id">
              <span className="mf-worked-kicker">№ {String(album ? album.rank : 36).padStart(3, "0")} · The composite, demonstrated</span>
              <span className="mf-worked-title">
                {album ? album.title : "Rumours"}
                <span className="mf-worked-artist"> — {album ? album.artist : "Fleetwood Mac"}</span>
              </span>
            </span>
            <span className="mf-worked-composite">
              <span className="mf-worked-comp-num">{score.composite.toFixed(2)}</span>
              <span className="mf-worked-comp-lbl">composite</span>
            </span>
          </button>

          <div className="mf-worked-rows">
            {score.dims.map((d) =>
            <div key={d.key} className="mf-worked-row">
                <span className="mf-worked-dim">{d.label}</span>
                <span className="mf-worked-meter" aria-hidden="true">
                  <span className="mf-worked-fill" style={{ width: d.value * 10 + "%" }} />
                </span>
                <span className="mf-worked-score">{d.value.toFixed(1)}</span>
              </div>
            )}
          </div>

          <div className="mf-worked-foot">
            Songwriting and production weighted ×1.25; cohesion the lightest input.
            <button
              type="button"
              className="mf-worked-link"
              onClick={() => album && onAlbum && onAlbum(album)}
              aria-label={album ? `Read the full scorecard for ${album.title}` : "Read the full scorecard"}>
              Read the full scorecard →
            </button>
          </div>
        </div>
      </div>

      <div className="mf-method-prose">
        <div className="mf-method-block">
          <div className="mf-block-eyebrow">The composite</div>
          <p>
            Each dimension is scored zero to ten by the editorial board after listening to a verified pressing on a calibrated system, blind-rated, then deliberated. The composite is not the average. Songwriting and production are weighted at one-and-a-quarter, because if those are weak, nothing else saves the record. Innovation and influence are tied. Cohesion is the smallest weight, but it can swing a placement by five spots in either direction.
          </p>
        </div>

        <div className="mf-method-block">
          <div className="mf-block-eyebrow">Editorial vs. data</div>
          <p>
            The score is built by humans. The data is there to keep us honest, pressing values from quarterly market medians, sample counts from WhoSampled, cultural references tracked through a small custom corpus we've built since 2021. When the score and the data disagree, the score wins, and we explain why in the essay. We are critics, not statisticians. Statisticians would have ranked Thriller higher.
          </p>
        </div>

        <div className="mf-method-block">
          <div className="mf-block-eyebrow">What goes in the calculation</div>
          <p>
            Pressing values matter because the market is also a critic, when a record's NM original sustains $300 in 2026 dollars, that is a thousand collectors voting against the streaming era. Cultural momentum matters because some records gain meaning over time and others lose it. The Velvet Underground &amp; Nico is a different record in 2026 than it was in 1996, and the score reflects it. Nothing is frozen.
          </p>
        </div>

        <div className="mf-method-block">
          <div className="mf-block-eyebrow">The defense</div>
          <p>
            Every entry on the list has a full essay attached to it, with a public revision history. Every essay closes with an open comment section where the editor responds in writing for thirty days. We have changed three placements in the last eighteen months on the strength of reader arguments. We have refused to change forty-one others. The point is not that we're right. The point is that we're accountable.
          </p>
        </div>
      </div>
    </section>);

}

/* ---- 4. The proof ------------------------------------------------------- */
function ManifestoProof({ items, onAlbum, onList }) {
  const open = (it) => {
    // All essays are live, navigate straight to the deep dive.
    if (onAlbum) onAlbum(it);
    else if (onList) onList();
  };
  return (
    <section className="mf-section mf-proof" data-screen-label="Proof">
      <div className="mf-section-rule" style={{ padding: "44px 0px 28px" }}>
        <span className="mf-section-num">§ 03</span>
        <span className="mf-section-label">Evidence the writing delivers</span>
      </div>

      <h2 className="mf-h2 mf-h2-proof" style={{ padding: "0px 0px 20px" }}>
        Three openers, picked so the range isn't an accident.
      </h2>

      <div className="mf-proof-grid">
        {items.map((it) =>
        <article key={it.slug} className={"mf-proof-card" + (it.href ? " is-live" : "")}>
            <div className="mf-proof-cover" style={{ background: "#0a0a0a" }}>
              <img
              src={"/covers/" + it.slug + ".jpg"}
              alt={it.title + " by " + it.artist + ", original cover"}
              loading="lazy"
              style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
            
              <div className="mf-proof-rank">№ {String(it.rank).padStart(3, "0")}</div>
            </div>
            <div className="mf-proof-body">
              <div className="mf-proof-eyebrow">{it.eyebrow}</div>
              <h3 className="mf-proof-title">
                <span className="mf-proof-itit">{it.title}</span>
                <span className="mf-proof-art"> — {it.artist}</span>
              </h3>
              <div className="mf-proof-meta">
                <span>{it.year}</span>
                <span className="mf-dot">·</span>
                <span>composite <b>{it.score.toFixed(1)}</b></span>
                <span className="mf-dot">·</span>
                <span>{it.readTime}-min read</span>
              </div>

              <p className="mf-proof-pull">{it.pull}</p>
              <p className="mf-proof-text">{it.body}</p>

              <div className="mf-proof-actions">
                <button className="mf-proof-cta" onClick={() => open(it)}>
                  {it.href ? "Read the full piece" : "Read the deep dive"}
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                    <path d="M5 12h14M13 5l7 7-7 7" />
                  </svg>
                </button>
                {it.affiliate &&
                <a
                  className="mf-proof-vinyl"
                  href={it.affiliate}
                  target="_blank"
                  rel="sponsored noopener noreferrer"
                  onClick={(e) => e.stopPropagation()}>
                  Own it on vinyl →
                </a>}
              </div>
            </div>
          </article>
        )}
      </div>
    </section>);

}

/* Recreate the Sleeve component's spirit at proof scale, without the dependency. */
function ProofSleeve({ sleeve, title, artist, year }) {
  const { bg, fg, accent, style } = sleeve;
  return (
    <div className="mf-sleeve" style={{ background: bg, color: fg }}>
      {style === "portrait" &&
      <>
          <div className="mfs-rain" />
          <div className="mfs-portrait">
            <div className="mfs-mast" style={{ color: fg }}>{artist.split(" ").slice(-1)[0].toUpperCase()}</div>
            <div className="mfs-script" style={{ color: accent }}>{title}</div>
            <div className="mfs-collar" style={{ background: accent }} />
          </div>
        </>
      }
      {style === "minimal" &&
      <div className="mfs-min">
          <div className="mfs-min-band" style={{ background: accent }} />
          <div className="mfs-min-title">{title}</div>
          <div className="mfs-min-artist">{artist}</div>
          <div className="mfs-min-year">{year}</div>
        </div>
      }
      {style === "bold" &&
      <div className="mfs-bold">
          <div className="mfs-bold-blot" style={{ background: accent }} />
          <div className="mfs-bold-title">{title}</div>
          <div className="mfs-bold-artist">{artist}</div>
        </div>
      }
    </div>);

}

/* ---- 5. The ranking, glimpsed ------------------------------------------- */
function ManifestoTop10({ top10, onAlbum, onList }) {
  // Shares the "heard" set with the full list page (localStorage key bbr:heard).
  const [heard, setHeard] = useHMfState(() => {
    try { return new Set(JSON.parse(localStorage.getItem("bbr:heard") || "[]")); }
    catch (e) { return new Set(); }
  });
  useHMfEffect(() => {
    try { localStorage.setItem("bbr:heard", JSON.stringify([...heard])); } catch (e) {}
  }, [heard]);
  const toggleHeard = (rank, e) => {
    e.stopPropagation();
    setHeard((h) => {
      const n = new Set(h);
      if (n.has(rank)) n.delete(rank); else n.add(rank);
      return n;
    });
  };

  return (
    <section className="mf-section mf-top10" data-screen-label="Top 10" style={{ padding: "0px 56px 25px" }}>
      <div className="mf-section-rule">
        <span className="mf-section-num">§ 04</span>
        <span className="mf-section-label">The top ten, as a teaser</span>
      </div>

      <div className="mf-top10-head">
        <h2 className="mf-h2">
          One through ten.<br />
          The other ninety are on the list.
        </h2>
        <p className="mf-top10-sub">
          A ranked one hundred without a top ten is a magazine without a cover. Here it is. Read it the way you'd read the front page of a serious paper, assume every position is an argument, and assume we'll defend it. Tap a circle to mark the ones you've heard.
        </p>
      </div>

      <ol className="mf-top10-list">
        {top10.map((a) => {
          const isHeard = heard.has(a.rank);
          const onVinyl = !!(window.ESSAYS_FULL && window.ESSAYS_FULL[a.slug]);
          return (
          <li key={a.rank} className="mf-top10-row" onClick={() => onAlbum(a)}>
            <div className="mf-t10-heard" onClick={(e) => e.stopPropagation()}>
              <button
                className={"mf-t10-heard-btn" + (isHeard ? " on" : "")}
                onClick={(e) => toggleHeard(a.rank, e)}
                aria-pressed={isHeard}
                aria-label={isHeard ? `Marked ${a.title} as heard` : `Mark ${a.title} as heard`}>
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
                  <polyline points="4 12 10 18 20 6" />
                </svg>
              </button>
            </div>
            <div className="mf-t10-rank">{String(a.rank).padStart(2, "0")}</div>
            <div className="mf-t10-cover" aria-hidden="true">
              <img src={"/covers/" + a.slug + ".jpg"} alt="" loading="lazy" />
            </div>
            <div className="mf-t10-title">
              <span className="mf-t10-tname">{a.title}</span>
              <span className="mf-t10-tartist">{a.artist}</span>
            </div>
            <div className="mf-t10-year">{a.year}</div>
            <div className="mf-t10-genre">{a.genre}</div>
            <div className="mf-t10-score">
              {(9.9 - (a.rank - 1) * 0.07).toFixed(2)}
            </div>
            <div className="mf-t10-vinyl">
              {onVinyl &&
              <span className="mf-t10-vinyl-badge" title="In print, pressing guide available">
                <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                  <circle cx="12" cy="12" r="9" />
                  <circle cx="12" cy="12" r="2.4" />
                </svg>
                On vinyl
              </span>}
            </div>
            <div className="mf-t10-arrow">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                <path d="M5 12h14M13 5l7 7-7 7" />
              </svg>
            </div>
          </li>
          );
        })}
      </ol>

      <div className="mf-top10-foot">
        <button className="mf-top10-cta" onClick={onList}>
          See all one hundred
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
            <path d="M5 12h14M13 5l7 7-7 7" />
          </svg>
        </button>
        <span className="mf-top10-note">
          Composite scores recalculated quarterly. Last revision: Q1 2026, Tago Mago up two, Sgt. Pepper down four, To Pimp a Butterfly up one.
        </span>
      </div>
    </section>);

}

/* ---- 6. The contract ---------------------------------------------------- */
function ManifestoContract() {
  return (
    <section className="mf-section mf-contract" data-screen-label="Contract">
      <div className="mf-section-rule" style={{ padding: "44px 0px 28px" }}>
        <span className="mf-section-num">§ 05</span>
        <span className="mf-section-label">What we owe you</span>
      </div>

      <h2 className="mf-h2 mf-h2-contract">
        If you stick around, here&rsquo;s what you get from us. Every week. Without fail.
      </h2>

      <div className="mf-contract-list">
        {CONTRACT_ITEMS.map((c, i) =>
        <article key={i} className="mf-contract-row">
            <div className="mf-c-num">{String(i + 1).padStart(2, "0")}</div>
            <div className="mf-c-body">
              <h3 className="mf-c-label">{c.label}</h3>
              <p className="mf-c-text">{c.body}</p>
            </div>
          </article>
        )}
      </div>

      <div className="mf-contract-pact">
        <div className="mf-pact-rule" />
        <p className="mf-pact-line">
          Five commitments. If we break one, you get to write a guest editorial about it and we publish it.
        </p>
      </div>
    </section>);

}

/* ---- 7. Newsletter ------------------------------------------------------ */
function ManifestoNewsletter() {
  const [email, setEmail] = useHMfState("");
  const [submitted, setSubmitted] = useHMfState(false);

  const onSubmit = (e) => {
    e.preventDefault();
    if (!email) return;
    setSubmitted(true);
  };

  return (
    <section className="mf-section mf-newsletter" data-screen-label="Newsletter">
      <div className="mf-section-rule">
        <span className="mf-section-num">§ 05</span>
        <span className="mf-section-label">The list, in your inbox</span>
      </div>

      <div className="mf-newsletter-card">
        <h2 className="mf-h2 mf-h2-newsletter">
          We&rsquo;ll tell you when a grail pressing comes back in print &mdash; and send the next essay before anyone else.
        </h2>

        <div className="mf-nl-promises">
          <div className="mf-nl-promise">
            <span className="mf-nl-promise-tag">The editorial promise</span>
            <span className="mf-nl-promise-text">One legendary album, written up in full, every Wednesday. No filler, no roundups, no sponsored picks.</span>
          </div>
          <div className="mf-nl-promise">
            <span className="mf-nl-promise-tag">The commerce promise</span>
            <span className="mf-nl-promise-text">Restock and back-in-print alerts when a grail pressing returns, with price movement on the originals we track.</span>
          </div>
        </div>

        {!submitted ?
        <form className="mf-newsletter-form" onSubmit={onSubmit}>
            <input
            className="mf-newsletter-input"
            type="email"
            placeholder="your@email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            aria-label="Email address" />
          
            <button className="mf-newsletter-submit" type="submit">
              Subscribe
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                <path d="M5 12h14M13 5l7 7-7 7" />
              </svg>
            </button>
          </form> :

        <div className="mf-newsletter-thanks">
            <span className="mf-thanks-tick">✓</span>
            <span>You&rsquo;ll get the next deep dive Wednesday morning, plus the next restock alert. Reply to it; the editor reads everything.</span>
          </div>
        }

        <div className="mf-newsletter-foot">
          <span>One email a week, no exceptions · 18,000 readers · One unsubscribe link, no dark patterns.</span>
        </div>
      </div>
    </section>);

}

/* ---- 8. Sign-off -------------------------------------------------------- */
function ManifestoSignoff({ onList }) {
  return (
    <section className="mf-section mf-signoff" data-screen-label="Sign-off">
      <div className="mf-signoff-rule" />
      <p className="mf-signoff-line">
        Albums are the longest-form thing popular music has ever asked anyone to sit down with.
        We think that's worth something. We think it's worth writing about properly.
        We think one of these will be your favorite essay you read this year.
      </p>

      <div className="mf-signoff-cta-wrap">
        <button className="mf-signoff-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>
        </button>
      </div>

      <div className="mf-signoff-byline">
        <div className="mf-by-block">
          <div className="mf-by-label">Edited by</div>
          <div className="mf-by-name">R Iannucci-Dawson</div>
        </div>
        <div className="mf-by-block">
          <div className="mf-by-label">Written by</div>
          <div className="mf-by-name">A small group of people who care too much</div>
        </div>
        <div className="mf-by-block">
          <div className="mf-by-label">Edition</div>
          <div className="mf-by-name">Volume IV · 2026</div>
        </div>
      </div>
    </section>);

}

window.HomeManifesto = HomeManifesto;
/* ========================================================
   Dual-identity homepage sections (editorial + collector)
   Appended; orchestrated by HomeManifesto above.
   ======================================================== */

function HdArrow() {
  return (
    <svg className="hd-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
      <path d="M5 12h14M13 5l7 7-7 7" />
    </svg>);
}

function ValueSparkDemo() {
  // Static upward demo line for the logged-out showcase.
  const pts = [4, 10, 9, 16, 22, 20, 28, 34, 40, 46, 52, 60];
  const W = 220, H = 56, max = 64;
  const d = pts.map((v, i) => (i ? "L" : "M") + (i / (pts.length - 1) * W).toFixed(1) + "," + (H - v / max * H).toFixed(1)).join(" ");
  return (
    <svg className="hd-spark" viewBox={"0 0 " + W + " " + H} preserveAspectRatio="none" aria-hidden="true">
      <path d={d + " L" + W + "," + H + " L0," + H + " Z"} fill="var(--accent)" opacity="0.12" />
      <path d={d} fill="none" stroke="var(--accent)" strokeWidth="2" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
    </svg>);
}

/* The 50/50 acquisition hero. Source order is Collection, connective, Canon so
   mobile stacks Collection-on-top with the connective between; desktop uses a
   grid (Canon left, Collection right, connective spanning below). */
function DualHero({ albums, featured, onList, onCollection }) {
  const wall = useHMfMemo(
    () => [1, 2, 5, 9, 15, 23].map((r) => albums.find((a) => a.rank === r)).filter(Boolean),
    [albums]
  );
  return (
    <section className="hd-hero" data-screen-label="Dual hero">
      <div className="hd-half hd-half-collection">
        <div className="hd-eyebrow">Your collection</div>
        <h1 className="hd-h">Know what your records are worth.</h1>
        <p className="hd-sub">Track, value and rank your vinyl &mdash; and climb the collector leaderboard.</p>
        <button className="hd-cta hd-cta-accent" onClick={onCollection}>Start your collection <HdArrow /></button>
        <div className="hd-dash" aria-hidden="true">
          <div className="hd-dash-top">
            <div>
              <div className="hd-dash-val"><span className="cur">£</span><HdCountUp value={1240} /></div>
              <div className="hd-dash-cap">estimated collection value</div>
            </div>
            <span className="hd-dash-delta">&#9650; 12%</span>
          </div>
          <HeroSpark />
          <div className="hd-dash-prog">
            <div className="hd-dash-prog-row"><span>Canon owned</span><span>23 / 100</span></div>
            <div className="hd-dash-bar"><i style={{ "--w": "23%" }} /></div>
          </div>
          <div className="hd-gbars">
            {[["Rock", "88%"], ["Soul / R&B", "60%"], ["Jazz", "40%"], ["Hip-Hop", "28%"]].map((row, i) => (
              <div className="hd-gbar" key={row[0]}>
                <span className="hd-gbar-l">{row[0]}</span>
                <span className="hd-gbar-t"><i style={{ "--w": row[1], transitionDelay: (i * 80) + "ms" }} /></span>
              </div>))}
          </div>
        </div>
      </div>

      <div className="hd-connect">Read the canon. Build your collection. Compete over it.</div>

      <div className="hd-half hd-half-canon">
        <div className="hd-eyebrow">The canon</div>
        <h1 className="hd-h">The 100 greatest albums, settled.</h1>
        <p className="hd-sub">One definitive list, every placement defended in writing. Argue with it.</p>
        <button className="hd-cta" onClick={onList}>Explore the 100 <HdArrow /></button>
        <div className="hd-wall" aria-hidden="true">
          {wall.map((a) => <img key={a.slug} src={"/covers/" + a.slug + ".jpg"} alt="" loading="lazy" draggable="false" />)}
        </div>
      </div>
    </section>);
}

/* This week's deep dive — the featured essay. */
function WeeklyDeepDive({ album, onAlbum }) {
  if (!album) return null;
  const full = window.ESSAYS_FULL && window.ESSAYS_FULL[album.slug];
  const dek =
    (full && full.essay && full.essay.dek) ||
    (full && full.tagline) ||
    "This week's definitive deep dive — pressing guide, session reconstruction, scoring data, and an argument we'll defend in the comments.";
  return (
    <section className="hd-deepdive" data-screen-label="This week's deep dive">
      <button className="hd-dd-card" onClick={() => onAlbum && onAlbum(album)} aria-label={"Read this week's essay: " + album.title}>
        <div className="hd-dd-art">
          <span className="hd-dd-vinyl" aria-hidden="true" />
          <img src={"/covers/" + album.slug + ".jpg"} alt="" loading="lazy" draggable="false" />
          <span className="hd-dd-rank">&#8470; {String(album.rank).padStart(3, "0")}</span>
        </div>
        <div className="hd-dd-body">
          <div className="hd-eyebrow"><span className="hd-pulse" /> This week's deep dive</div>
          <h2 className="hd-dd-title">{album.title}</h2>
          <div className="hd-dd-artist">{album.artist} &middot; {album.year}</div>
          <p className="hd-dd-dek">{dek}</p>
          <span className="hd-dd-read">Read the deep dive <HdArrow /></span>
        </div>
      </button>
    </section>);
}

/* Collector-tool showcase: the 100-grid, value-over-time, genre blend, and a
   prominent LIVE leaderboard — the competitive hook, styled like the real board. */
function CollectorShowcase({ albums, onCollection, onLeaderboards }) {
  const [top, setTop] = useHMfState(null);
  useHMfEffect(() => {
    let c = false;
    if (window.BBR_supabase) {
      window.BBR_supabase.rpc("leaderboard_totals", { genre_filter: null })
        .then(({ data }) => { if (!c) setTop((data || []).slice(0, 5)); })
        .catch(() => { if (!c) setTop([]); });
    } else { setTop([]); }
    return () => { c = true; };
  }, []);
  return (
    <section className="hd-showcase" data-screen-label="Collector tools">
      <div className="hd-section-head">
        <span className="hd-eyebrow">The collector tools</span>
        <h2 className="hd-section-h">Your shelf, measured &mdash; and ranked.</h2>
        <p className="hd-section-sub">Scan a record, get its market value, watch your total climb &mdash; then see exactly where you stand against every other collector on the leaderboard.</p>
      </div>
      <div className="hd-showcase-grid hd-showcase-grid-3">
        <div className="hd-show-card hd-show-center">
          <div className="hd-show-k">The 100, tracked</div>
          <ProgressRing value={23} max={100} sub="of the canon owned" />
        </div>
        <div className="hd-show-card">
          <div className="hd-show-k">Collection value</div>
          <div className="hd-show-val"><span className="cur">£</span><HdCountUp value={1240} /></div>
          <ValueChartRich />
        </div>
        <div className="hd-show-card hd-show-center">
          <div className="hd-show-k">Genre blend</div>
          <GenreDonut />
        </div>
      </div>
      <LeaderboardFeature top={top} onLeaderboards={onLeaderboards} />
      <button className="hd-cta hd-cta-accent hd-cta-center" onClick={onCollection}>Start your collection <HdArrow /></button>
    </section>);
}

/* Live leaderboard feature — dark panel echoing the real Leaderboards page
   (medals, gradient value bars, count-up), so the competitive hook is obvious. */
function LeaderboardFeature({ top, onLeaderboards }) {
  const rows = (top || []).slice(0, 5);
  const max = rows.length ? (Number(rows[0].total_value) || 1) : 1;
  return (
    <div className="hd-lbfeat">
      <div className="hd-lbfeat-head">
        <div>
          <div className="hd-lbfeat-eyebrow"><span className="hd-pulse" /> Live leaderboard</div>
          <h3 className="hd-lbfeat-h">The collectors out in front</h3>
        </div>
        <button className="hd-lbfeat-cta" onClick={onLeaderboards}>See the full leaderboard <HdArrow /></button>
      </div>
      {top === null ? (
        <div className="hd-muted">Loading the board&hellip;</div>
      ) : rows.length === 0 ? (
        <div className="hd-muted">Be the first on the board.</div>
      ) : (
        <div className="hd-lbfeat-rows">
          {rows.map((r, i) => (
            <button className="hd-lbfeat-row" key={r.user_id} onClick={onLeaderboards}>
              <span className={"hd-lbfeat-rank r" + i}>{i + 1}</span>
              <span className="hd-lbfeat-name">{r.display_name || "Collector"}</span>
              <span className="hd-lbfeat-bar"><i style={{ width: Math.max(6, (Number(r.total_value) || 0) / max * 100) + "%" }} /></span>
              <span className="hd-lbfeat-val">&pound;<HdCountUp value={Math.round(Number(r.total_value) || 0)} /></span>
            </button>))}
        </div>
      )}
    </div>);
}

/* Living community band. */
function CommunityBand({ onList, onCollection }) {
  return (
    <section className="hd-community" data-screen-label="Community">
      <div className="hd-section-head">
        <span className="hd-eyebrow">A living argument</span>
        <h2 className="hd-section-h">The canon is up for debate.</h2>
        <p className="hd-section-sub">Comment on every placement, nominate what we missed, and climb the board &mdash; the list argues back.</p>
      </div>
      <div className="hd-comm-grid">
        <div className="hd-comm-card hd-proof">
          <div className="hd-proof-tag">Comments</div>
          <div className="hd-cmt">
            <span className="hd-cmt-av">JM</span>
            <div className="hd-cmt-main">
              <div className="hd-cmt-name">Joel M.</div>
              <p className="hd-cmt-txt">"&#8470;33 for Off the Wall, over Thriller? Unhinged. And I think you're right."</p>
            </div>
          </div>
          <div className="hd-cmt-foot"><span>&#9829; 24</span><span className="hd-cmt-reply">the editor replied</span></div>
        </div>

        <div className="hd-comm-card hd-proof">
          <div className="hd-proof-tag">Nominations</div>
          <div className="hd-nom"><span className="hd-nom-t">Spiderland &mdash; Slint</span><span className="hd-nom-v">+312</span></div>
          <div className="hd-nom-bar"><span style={{ width: "82%" }} /></div>
          <div className="hd-nom"><span className="hd-nom-t">Madvillainy &mdash; Madvillain</span><span className="hd-nom-v">+241</span></div>
          <div className="hd-nom-bar"><span style={{ width: "63%" }} /></div>
          <div className="hd-nom"><span className="hd-nom-t">Dummy &mdash; Portishead</span><span className="hd-nom-v">+188</span></div>
          <div className="hd-nom-bar"><span style={{ width: "49%" }} /></div>
        </div>

        <div className="hd-comm-card hd-proof">
          <div className="hd-proof-tag">Leaderboard</div>
          <div className="hd-mini-lb">
            <div className="hd-mini-row"><span className="hd-mini-m m1">1</span><span className="hd-mini-n">Ric D.</span><b>&pound;3,480</b></div>
            <div className="hd-mini-row"><span className="hd-mini-m m2">2</span><span className="hd-mini-n">Sarah K.</span><b>&pound;2,910</b></div>
            <div className="hd-mini-row"><span className="hd-mini-m m3">3</span><span className="hd-mini-n">Tom&aacute;s R.</span><b>&pound;2,140</b></div>
          </div>
        </div>
      </div>
      <div className="hd-comm-ctas">
        <button className="hd-cta" onClick={onList}>Into the list &amp; the argument <HdArrow /></button>
        <button className="hd-cta hd-cta-accent" onClick={onCollection}>Start your collection <HdArrow /></button>
      </div>
    </section>);
}

/* ======================================================================
   Logged-in collector dashboard — a personal command center.
   Everything here is the member's REAL data: live collection value, the
   cumulative value curve drawn from when records were actually added, canon
   progress, and the genre blend of what's on their shelf.
   ====================================================================== */

const CM_GENRE_COLORS = ["var(--accent)", "#caa53d", "#5b7a99", "#6b8f71", "#9a6b9a", "#b0763f"];

/* Real cumulative collection value, in the order records were added. Each point
   is the running total after that record joined the shelf — so the curve is a
   true history of how the collection's value was built, not a demo trend. */
function cmCumulativeSeries(items) {
  const rows = items
    .map((r) => ({ t: r.date_added ? Date.parse(r.date_added) : 0, v: Number(r.est_value) || 0 }))
    .sort((a, b) => a.t - b.t);
  let acc = 0;
  return rows.map((r) => (acc += r.v));
}

/* Value added in the trailing 30 days (real, from date_added). 0 ⇒ hidden. */
function cmRecentGain(items) {
  const cutoff = Date.now() - 30 * 864e5;
  return items.reduce((s, r) => {
    const t = r.date_added ? Date.parse(r.date_added) : 0;
    return t >= cutoff ? s + (Number(r.est_value) || 0) : s;
  }, 0);
}

/* Top genres on the shelf by record count, tail collapsed into "Other". */
function cmGenreBlend(items) {
  const map = new Map();
  items.forEach((r) => { const g = r.genre || "Other"; map.set(g, (map.get(g) || 0) + 1); });
  const arr = [...map.entries()].map(([g, v]) => ({ g, v })).sort((a, b) => b.v - a.v);
  const top = arr.slice(0, 4);
  const rest = arr.slice(4).reduce((s, x) => s + x.v, 0);
  if (rest) top.push({ g: "Other", v: rest });
  return top.map((s, i) => ({ ...s, c: CM_GENRE_COLORS[i % CM_GENRE_COLORS.length] }));
}

/* Value curve, tuned for the dark dashboard card. Wipes in on reveal. */
function CmSpark({ pts }) {
  if (!pts || pts.length < 2) return <div className="cm-spark-flat" aria-hidden="true" />;
  const W = 260, H = 60;
  const max = Math.max(...pts), min = Math.min(...pts, 0), span = max - min || 1;
  const x = (i) => (i / (pts.length - 1)) * W;
  const y = (v) => H - ((v - min) / span) * H;
  const line = pts.map((v, i) => (i ? "L" : "M") + x(i).toFixed(1) + "," + y(v).toFixed(1)).join(" ");
  const lx = x(pts.length - 1), ly = y(pts[pts.length - 1]);
  return (
    <div className="cm-spark">
      <svg viewBox={"0 0 " + W + " " + H} preserveAspectRatio="none" aria-hidden="true">
        <defs>
          <linearGradient id="cmSparkFill" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.45" />
            <stop offset="100%" stopColor="var(--accent)" stopOpacity="0" />
          </linearGradient>
        </defs>
        <path d={line + " L" + W + "," + H + " L0," + H + " Z"} fill="url(#cmSparkFill)" />
        <path d={line} fill="none" stroke="var(--accent)" strokeWidth="2.4" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
      </svg>
      <span className="cm-spark-dot" style={{ left: (lx / W * 100) + "%", top: (ly / H * 100) + "%" }} />
    </div>);
}

/* Data-driven genre donut for the dark card. */
function CmDonut({ segs }) {
  const total = segs.reduce((s, x) => s + x.v, 0) || 1;
  const r = 46, c = 2 * Math.PI * r;
  let acc = 0;
  return (
    <div className="cm-donut-wrap">
      <svg className="cm-donut" viewBox="0 0 120 120">
        <circle cx="60" cy="60" r={r} className="cm-donut-track" />
        {segs.map((s) => {
          const len = (s.v / total) * c;
          const el = (
            <circle key={s.g} cx="60" cy="60" r={r} className="cm-donut-seg"
              stroke={s.c} strokeDasharray={len + " " + (c - len)} strokeDashoffset={-acc}
              transform="rotate(-90 60 60)" />);
          acc += len;
          return el;
        })}
        <text x="60" y="57" className="cm-donut-mid">{segs.length}</text>
        <text x="60" y="74" className="cm-donut-sub">genres</text>
      </svg>
      <div className="cm-donut-legend">
        {segs.map((s) => (
          <span key={s.g}><i style={{ background: s.c }} />{s.g}<b>{s.v}</b></span>
        ))}
      </div>
    </div>);
}

function CollectionCommandCenter({ albums, user, onCollection, onList }) {
  const store = window.BBR_store;
  const [, force] = useHMfState(0);
  useHMfEffect(() => (store ? store.subscribe(() => force((n) => n + 1)) : undefined), []);
  const items = (store && store.collectionV2) || [];

  const total = items.reduce((s, r) => s + (Number(r.est_value) || 0), 0);
  const canon = new Set(items.filter((r) => r.slug).map((r) => r.slug)).size;
  const series = useHMfMemo(() => cmCumulativeSeries(items), [items]);
  const recentGain = useHMfMemo(() => cmRecentGain(items), [items]);
  const blend = useHMfMemo(() => cmGenreBlend(items), [items]);
  const jewel = useHMfMemo(
    () => items.reduce((best, r) => ((Number(r.est_value) || 0) > (Number(best && best.est_value) || 0) ? r : best), null),
    [items]
  );

  const firstName = (user && user.name) ? user.name.split(" ")[0] : "collector";
  const today = new Date().toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long" });

  // Empty shelf → a confident onboarding panel instead of a wall of zeros.
  if (!items.length) {
    return (
      <section className="cm-hero cm-hero-empty" data-screen-label="Your collection">
        <div className="cm-glow" aria-hidden="true" />
        <div className="cm-empty-inner">
          <div className="cm-eyebrow">Your collection · {today}</div>
          <h1 className="cm-hello">Welcome, {firstName}. Your shelf is empty.</h1>
          <p className="cm-empty-sub">Add your first record &mdash; scan a barcode or search the catalogue &mdash; and we&rsquo;ll value it at the market median, chart how your collection grows, and put you on the collector leaderboard.</p>
          <div className="cm-actions">
            <button className="cm-btn cm-btn-accent" onClick={onCollection}>+ Add your first record</button>
            <button className="cm-btn cm-btn-ghost" onClick={onList}>Browse the 100 first <HdArrow /></button>
          </div>
          <div className="cm-empty-steps">
            <div className="cm-step"><span className="cm-step-n">1</span><div><b>Scan or search</b><span>Barcode, Discogs, or straight from the canon.</span></div></div>
            <div className="cm-step"><span className="cm-step-n">2</span><div><b>Get a live value</b><span>Market median for your pressing and condition.</span></div></div>
            <div className="cm-step"><span className="cm-step-n">3</span><div><b>Climb the board</b><span>Ranked live against every other collector.</span></div></div>
          </div>
        </div>
      </section>);
  }

  return (
    <section className="cm-hero" data-screen-label="Your collection">
      <div className="cm-glow" aria-hidden="true" />
      <header className="cm-hero-top">
        <div className="cm-greet">
          <div className="cm-eyebrow">Your collection · {today}</div>
          <h1 className="cm-hello">Welcome back, {firstName}.</h1>
        </div>
        <div className="cm-actions">
          <button className="cm-btn cm-btn-accent" onClick={onCollection}>+ Add a record</button>
          <button className="cm-btn cm-btn-ghost" onClick={onCollection}>Open collection <HdArrow /></button>
        </div>
      </header>

      <div className="cm-grid">
        <div className="cm-card cm-card-value">
          <div className="cm-card-k">Estimated value</div>
          <div className="cm-value-row">
            <div className="cm-value"><span className="cur">£</span><HdCountUp value={Math.round(total)} /></div>
            {recentGain > 0 &&
              <span className="cm-delta">&#9650; +&pound;{Math.round(recentGain).toLocaleString("en-GB")} <small>30d</small></span>}
          </div>
          <CmSpark pts={series} />
          <div className="cm-value-foot">Across {items.length} record{items.length === 1 ? "" : "s"} · valued at market median</div>
        </div>

        <div className="cm-card cm-card-ring">
          <div className="cm-card-k">The canon, owned</div>
          <ProgressRing value={canon} max={100} sub="on your shelf" />
        </div>

        <div className="cm-card cm-card-donut">
          <div className="cm-card-k">What&rsquo;s on the shelf</div>
          <CmDonut segs={blend} />
        </div>
      </div>

      <div className="cm-highlights">
        <div className="cm-hi"><div className="cm-hi-k">Records logged</div><div className="cm-hi-v">{items.length}</div></div>
        <div className="cm-hi"><div className="cm-hi-k">Top genre</div><div className="cm-hi-v cm-hi-v-sm">{blend.length ? blend[0].g : "—"}</div></div>
        {jewel
          ? <div className="cm-hi cm-hi-jewel">
              <div className="cm-hi-k">Crown jewel</div>
              <div className="cm-hi-v cm-hi-v-sm">{jewel.title}</div>
              <div className="cm-hi-meta">{jewel.artist} · &pound;{Math.round(Number(jewel.est_value) || 0).toLocaleString("en-GB")}</div>
            </div>
          : <div className="cm-hi"><div className="cm-hi-k">Crown jewel</div><div className="cm-hi-v cm-hi-v-sm">—</div></div>}
        <div className="cm-hi"><div className="cm-hi-k">Canon left</div><div className="cm-hi-v">{Math.max(0, 100 - canon)}</div></div>
      </div>
    </section>);
}

/* Logged-in: a live standings board — your rank, the climb, and the collectors
   either side of you. Real leaderboard data, framed as a competition. */
function CmBoardBody({ state, onLeaderboards }) {
  if (state === null) return <div className="cm-muted">Loading the board&hellip;</div>;
  const { rows, idx, total } = state;
  if (idx < 0) {
    return (
      <div className="cm-board-empty">
        <p>Add a record to enter the leaderboard and start climbing.</p>
        <button className="cm-btn cm-btn-ghost" onClick={onLeaderboards}>See who&rsquo;s out in front <HdArrow /></button>
      </div>);
  }
  const rank = idx + 1;
  const above = idx > 0 ? rows[idx - 1] : null;
  const gap = above ? Number(above.total_value) - Number(rows[idx].total_value) : 0;
  // A five-row window centred on the member (clamped at the ends of the board).
  const end = Math.min(rows.length, Math.max(idx + 3, 5));
  const start = Math.max(0, end - 5);
  const view = rows.slice(start, end).map((r, i) => ({ r, pos: start + i }));
  const max = Number(rows[0].total_value) || 1;
  return (
    <>
      <div className="cm-board-stat">
        <div className="cm-rank-chip"><span className="cm-rank-hash">#</span><span className="cm-rank-n">{rank}</span></div>
        <div className="cm-rank-txt">
          <div className="cm-rank-of">of {total} collector{total === 1 ? "" : "s"}</div>
          {above
            ? <div className="cm-rank-climb">&pound;{Math.round(gap).toLocaleString("en-GB")} to overtake <b>{above.display_name || "the collector above"}</b></div>
            : <div className="cm-rank-climb cm-rank-top">Top of the board — now defend it.</div>}
        </div>
      </div>
      <div className="cm-board-rows">
        {view.map(({ r, pos }) => {
          const isMe = pos === idx;
          return (
            <button key={r.user_id} className={"cm-row" + (isMe ? " is-me" : "")} onClick={onLeaderboards}>
              <span className={"cm-row-rank" + (pos < 3 ? " m" + pos : "")}>{pos + 1}</span>
              <span className="cm-row-name">{isMe ? "You" : (r.display_name || "Collector")}</span>
              <span className="cm-row-bar"><i style={{ width: Math.max(5, (Number(r.total_value) || 0) / max * 100) + "%" }} /></span>
              <span className="cm-row-val">&pound;{Math.round(Number(r.total_value) || 0).toLocaleString("en-GB")}</span>
            </button>);
        })}
      </div>
    </>);
}

function LeaderboardStandings({ user, onLeaderboards }) {
  const [state, setState] = useHMfState(null); // null = loading
  useHMfEffect(() => {
    let c = false;
    if (window.BBR_supabase) {
      window.BBR_supabase.rpc("leaderboard_totals", { genre_filter: null })
        .then(({ data }) => {
          if (c) return;
          const rows = (data || []).slice().sort((a, b) => Number(b.total_value) - Number(a.total_value));
          const idx = rows.findIndex((r) => r.user_id === (user && user.id));
          setState({ rows, idx, total: rows.length });
        })
        .catch(() => { if (!c) setState({ rows: [], idx: -1, total: 0 }); });
    } else { setState({ rows: [], idx: -1, total: 0 }); }
    return () => { c = true; };
  }, [user]);
  return (
    <section className="cm-board" data-screen-label="Leaderboard">
      <div className="cm-glow" aria-hidden="true" />
      <div className="cm-board-head">
        <div>
          <div className="cm-eyebrow cm-eyebrow-live"><span className="cm-pulse" /> Live leaderboard</div>
          <h2 className="cm-board-h">Where you stand</h2>
        </div>
        <button className="cm-btn cm-btn-accent" onClick={onLeaderboards}>Full leaderboard <HdArrow /></button>
      </div>
      <CmBoardBody state={state} onLeaderboards={onLeaderboards} />
    </section>);
}

/* Logged-in: gaps in the 100 with affiliate buy links (closes the loop). */
function GapsBand({ albums }) {
  const store = window.BBR_store;
  const [, force] = useHMfState(0);
  useHMfEffect(() => (store ? store.subscribe(() => force((n) => n + 1)) : undefined), []);
  const owned = new Set(((store && store.collectionV2) || []).filter((r) => r.slug).map((r) => r.slug));
  const gaps = albums.filter((a) => !owned.has(a.slug)).sort((a, b) => a.rank - b.rank).slice(0, 12);
  if (!gaps.length) return null;
  return (
    <section className="hd-gaps" data-screen-label="Gaps in the 100">
      <div className="hd-eyebrow">Close the gaps</div>
      <h2 className="hd-section-h">The canon you don't own yet</h2>
      <div className="hd-gaps-strip">
        {gaps.map((a) => (
          <a key={a.slug} className="hd-gap" href={(window.BBR_buyUrl && window.BBR_buyUrl(a.title + " " + a.artist, a.slug)) || "#"}
            target="_blank" rel="noopener"
            onClick={() => window.BBR_trackClick && window.BBR_trackClick(a.slug, "home-gap")}
            title={a.title + " — " + a.artist}>
            <img src={"/covers/" + a.slug + ".jpg"} alt="" loading="lazy" draggable="false" />
            <span className="hd-gap-rank">&#8470; {String(a.rank).padStart(3, "0")}</span>
          </a>))}
      </div>
    </section>);
}

/* Repeat both paths for anyone who scrolled the whole page. */
function ClosingDualCTA({ onList, onCollection }) {
  return (
    <section className="hd-closing" data-screen-label="Closing">
      <div className="hd-connect hd-connect-closing">Read the canon. Build your collection. Compete over it.</div>
      <div className="hd-closing-ctas">
        <button className="hd-cta" onClick={onList}>Explore the 100 <HdArrow /></button>
        <button className="hd-cta hd-cta-accent" onClick={onCollection}>Start your collection <HdArrow /></button>
      </div>
    </section>);
}

/* Count-up number that animates from 0 the first time it scrolls into view. */
function HdCountUp({ value }) {
  const [n, setN] = useHMfState(0);
  const ref = React.useRef(null);
  useHMfEffect(() => {
    const el = ref.current;
    let raf = 0, started = false;
    const run = () => {
      const t0 = performance.now(), dur = 950;
      const tick = (t) => {
        const p = Math.min(1, (t - t0) / dur), e = 1 - Math.pow(1 - p, 3);
        setN(Math.round(value * e));
        if (p < 1) raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
    };
    if (el && "IntersectionObserver" in window) {
      const io = new IntersectionObserver((es) => {
        es.forEach((en) => { if (en.isIntersecting && !started) { started = true; run(); io.disconnect(); } });
      }, { threshold: 0.3 });
      io.observe(el);
      return () => { io.disconnect(); cancelAnimationFrame(raf); };
    }
    setN(value);
  }, [value]);
  return <span ref={ref}>{n.toLocaleString("en-GB")}</span>;
}

/* ---- richer homepage visuals (editorial bigging-up + collector showcase) ---- */

/* Animated progress ring (fills on reveal via CSS using inline --c/--target). */
function ProgressRing({ value, max, sub }) {
  const r = 54, c = 2 * Math.PI * r, pct = Math.min(1, value / max);
  return (
    <div className="hd-ring-wrap">
      <svg className="hd-ring" viewBox="0 0 130 130" style={{ "--c": c, "--target": c * (1 - pct) }}>
        <circle className="hd-ring-bg" cx="65" cy="65" r={r} />
        <circle className="hd-ring-fg" cx="65" cy="65" r={r} strokeDasharray={c} transform="rotate(-90 65 65)" />
        <text className="hd-ring-num" x="65" y="62">{value}</text>
        <text className="hd-ring-den" x="65" y="84">of {max}</text>
      </svg>
      <div className="hd-ring-sub">{sub}</div>
    </div>);
}

/* Premium value-over-time area chart with gradient, gridlines, glow + delta. */
function ValueChartRich() {
  const pts = [320, 360, 410, 540, 612, 705, 760, 812, 905, 1010, 1140, 1240];
  const W = 280, H = 96, max = 1320, min = 260;
  const x = (i) => (i / (pts.length - 1)) * W;
  const y = (v) => H - ((v - min) / (max - min)) * H;
  const line = pts.map((v, i) => (i ? "L" : "M") + x(i).toFixed(1) + "," + y(v).toFixed(1)).join(" ");
  const lx = x(pts.length - 1), ly = y(pts[pts.length - 1]);
  return (
    <div className="hd-chart">
      <svg className="hd-chart-svg" viewBox={"0 0 " + W + " " + H} preserveAspectRatio="none" aria-hidden="true">
        <defs>
          <linearGradient id="hdChartFill" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.32" />
            <stop offset="100%" stopColor="var(--accent)" stopOpacity="0" />
          </linearGradient>
        </defs>
        {[0.33, 0.66].map((f) => <line key={f} x1="0" x2={W} y1={H * f} y2={H * f} className="hd-chart-grid" />)}
        <path className="hd-chart-area" d={line + " L" + W + "," + H + " L0," + H + " Z"} fill="url(#hdChartFill)" />
        <path className="hd-chart-line" d={line} fill="none" stroke="var(--accent)" strokeWidth="2.4" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
      </svg>
      <span className="hd-chart-dot" style={{ left: (lx / W * 100) + "%", top: (ly / H * 100) + "%" }} />
      <span className="hd-chart-delta">▲ +£148 this month</span>
    </div>);
}

/* Top-3 collectors as a podium (1st centre, taller). */
function LeaderboardPodium({ top }) {
  if (top === null) return <div className="hd-muted">Loading the board&hellip;</div>;
  if (!top.length) return <div className="hd-muted">Be the first on the board.</div>;
  const order = [top[1], top[0], top[2]].filter(Boolean);
  const place = (r) => top.indexOf(r) + 1;
  return (
    <div className="hd-podium">
      {order.map((r) => {
        const p = place(r);
        return (
          <div className={"hd-pod hd-pod-" + p} key={r.user_id}>
            <div className="hd-pod-medal">{p}</div>
            <div className="hd-pod-name">{r.display_name || "Collector"}</div>
            <div className="hd-pod-val">&pound;{Math.round(Number(r.total_value) || 0).toLocaleString("en-GB")}</div>
            <div className="hd-pod-bar" />
          </div>);
      })}
    </div>);
}

/* Genre-blend donut (illustrative blend for the showcase). */
function GenreDonut() {
  const segs = [
    { g: "Rock", v: 42, c: "var(--accent)" },
    { g: "Soul / R&B", v: 24, c: "#caa53d" },
    { g: "Jazz", v: 18, c: "#5b7a99" },
    { g: "Hip-Hop", v: 16, c: "#6b8f71" },
  ];
  const total = segs.reduce((s, x) => s + x.v, 0);
  const r = 46, c = 2 * Math.PI * r;
  let acc = 0;
  return (
    <div className="hd-donut-wrap">
      <svg className="hd-donut" viewBox="0 0 120 120">
        <circle cx="60" cy="60" r={r} className="hd-donut-track" />
        {segs.map((s) => {
          const len = (s.v / total) * c;
          const el = (
            <circle key={s.g} cx="60" cy="60" r={r} className="hd-donut-seg"
              stroke={s.c} strokeDasharray={len + " " + (c - len)} strokeDashoffset={-acc}
              transform="rotate(-90 60 60)" />);
          acc += len;
          return el;
        })}
        <text x="60" y="64" className="hd-donut-mid">4</text>
      </svg>
      <div className="hd-donut-legend">
        {segs.map((s) => <span key={s.g}><i style={{ background: s.c }} />{s.g}</span>)}
      </div>
    </div>);
}

/* Editorial stat band — bigs up the canon + the weekly machine. */
function EditorialStats() {
  return (
    <section className="hd-edstats" data-screen-label="The editorial machine">
      <div className="hd-edstats-inner">
        <div className="hd-edstat"><div className="hd-edstat-n"><HdCountUp value={100} /></div><div className="hd-edstat-k">albums in the canon</div></div>
        <div className="hd-edstat"><div className="hd-edstat-n">~<HdCountUp value={8000} /></div><div className="hd-edstat-k">words an essay</div></div>
        <div className="hd-edstat"><div className="hd-edstat-n">1<span className="hd-edstat-sm">/wk</span></div><div className="hd-edstat-k">new deep dive, every week</div></div>
      </div>
      <p className="hd-edstats-line">Every placement defended in writing &mdash; pressing guides, session reconstructions, scoring data, and an argument we&rsquo;ll have in the comments.</p>
    </section>);
}

/* Deep-dive archive rail — covers of albums with live essays (editorial depth). */
function DeepDiveArchive({ albums, onAlbum }) {
  const live = useHMfMemo(
    () => albums.filter((a) => window.ESSAYS_FULL && window.ESSAYS_FULL[a.slug]).slice(0, 18),
    [albums]
  );
  if (live.length < 4) return null;
  return (
    <section className="hd-archive" data-screen-label="The deep-dive archive">
      <div className="hd-arch-head">
        <span className="hd-eyebrow">The deep-dive archive</span>
        <h2 className="hd-section-h">The canon, written down &mdash; one essay at a time.</h2>
      </div>
      <div className="hd-arch-rail">
        {live.map((a) => (
          <button className="hd-arch-cover" key={a.slug} onClick={() => onAlbum && onAlbum(a)} title={a.title + " — " + a.artist}>
            <img src={"/covers/" + a.slug + ".jpg"} alt="" loading="lazy" draggable="false" />
            <span className="hd-arch-rank">&#8470; {String(a.rank).padStart(3, "0")}</span>
            <span className="hd-arch-title">{a.title}</span>
          </button>))}
      </div>
    </section>);
}

/* Prominent weekly-essay signup band. */
function EditorialSignup() {
  return (
    <section className="hd-signup" data-screen-label="Weekly deep dive signup">
      <div className="hd-signup-inner">
        <div className="hd-signup-copy">
          <div className="hd-eyebrow"><span className="hd-pulse" /> One essay a week</div>
          <h2 className="hd-signup-h">Get the weekly deep dive in your inbox.</h2>
          <p className="hd-signup-sub">A new entry in the canon every week &mdash; the full essay, the pressing guide, the argument. No spam, no filler.</p>
        </div>
        <div className="hd-signup-form">
          {window.NewsletterSignup && <NewsletterSignup variant="banner" source="homepage-editorial" />}
        </div>
      </div>
    </section>);
}

/* Compact animated value sparkline for the Collection hero panel (dark bg). */
function HeroSpark() {
  const pts = [18, 26, 22, 34, 40, 38, 52, 60, 58, 72, 84, 96];
  const W = 240, H = 52, max = 104;
  const x = (i) => (i / (pts.length - 1)) * W, y = (v) => H - (v / max) * H;
  const line = pts.map((v, i) => (i ? "L" : "M") + x(i).toFixed(1) + "," + y(v).toFixed(1)).join(" ");
  const lx = x(pts.length - 1), ly = y(pts[pts.length - 1]);
  return (
    <div className="hd-hspark">
      <svg viewBox={"0 0 " + W + " " + H} preserveAspectRatio="none" aria-hidden="true">
        <defs>
          <linearGradient id="hdHsFill" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.5" />
            <stop offset="100%" stopColor="var(--accent)" stopOpacity="0" />
          </linearGradient>
        </defs>
        <path d={line + " L" + W + "," + H + " L0," + H + " Z"} fill="url(#hdHsFill)" />
        <path d={line} fill="none" stroke="var(--accent)" strokeWidth="2.4" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
      </svg>
      <span className="hd-hspark-dot" style={{ left: (lx / W * 100) + "%", top: (ly / H * 100) + "%" }} />
    </div>);
}
