/* WGOArtwork.jsx
   ------------------------------------------------------------------
   Album-art treatment for "What's Going On":
     - <WGOCover>     CSS recreation of the iconic 1971 portrait sleeve.
                      Pure CSS/SVG, no copyrighted asset shipped.
                      Optional hotspots overlay the artwork.
     - <AboutCover>   "About the cover" editorial section with hotspots.
     - <MakingGallery> Photo-style gallery with placeholder frames
                       describing the imagery that would live there.
     - <StickyBuyRail> Amazon affiliate module that follows the reader.
   ------------------------------------------------------------------ */

const { useState: useWAState, useEffect: useWAEffect, useRef: useWARef } = React;

/* ============================================================
   1. WGOCover, typographic + portrait recreation
   ------------------------------------------------------------
   Shape language of the real cover:
     - Full-bleed close-crop portrait (warm umber, sepia/rain wash)
     - Marvin in a tan trench, collar up, head turned three-quarters
     - "MARVIN GAYE" in a large condensed serif, top
     - "What's Going On" hand-script overlapping the portrait
     - Rain streaks rendered as faint diagonal lines
   We don't ship a photograph. We render an iconographic stand-in
   that reads as the cover at a glance.
   ============================================================ */
function WGOCover({ size = 460, hotspots = false, onHotspot }) {
  const [active, setActive] = useWAState(null);
  const wrap = {
    width: size, height: size, position: "relative", overflow: "hidden",
    flexShrink: 0,
    background: "linear-gradient(165deg, #2a1a10 0%, #1a0e07 60%, #0d0703 100%)",
    boxShadow: "0 1px 0 rgba(0,0,0,.04), 0 28px 56px -28px rgba(0,0,0,.55)",
    color: "#e8c896",
    fontFamily: "'Playfair Display', serif",
  };

  return (
    <div className="wgo-cover" style={wrap}>
      {/* Atmospheric vignette, dark edges, warm centre glow */}
      <div style={{
        position: "absolute", inset: 0,
        background: "radial-gradient(ellipse at 42% 48%, rgba(217,119,87,0.42) 0%, rgba(120,52,28,0.18) 28%, rgba(0,0,0,0) 65%)",
        pointerEvents: "none"
      }} />

      {/* Portrait silhouette, Marvin three-quarter, collar up.
          Built from layered SVG ellipses + paths. Iconographic, not photo. */}
      <svg viewBox="0 0 460 460" width={size} height={size}
           style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
           preserveAspectRatio="xMidYMid slice">
        <defs>
          <radialGradient id="wgoFace" cx="0.45" cy="0.42" r="0.5">
              <stop offset="0%" stopColor="#5a3520" />
              <stop offset="55%" stopColor="#3d2113" />
              <stop offset="100%" stopColor="#1a0a05" />
          </radialGradient>
          <linearGradient id="wgoCoat" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="#7a4a2a" />
            <stop offset="100%" stopColor="#2a1608" />
          </linearGradient>
          <linearGradient id="wgoCoatHi" x1="0" y1="0" x2="1" y2="1">
            <stop offset="0%" stopColor="#a66a3c" stopOpacity="0.4" />
            <stop offset="60%" stopColor="#5a3520" stopOpacity="0" />
          </linearGradient>
          <filter id="wgoGrain">
            <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="3" />
            <feColorMatrix values="0 0 0 0 0.4   0 0 0 0 0.25  0 0 0 0 0.12  0 0 0 0.18 0" />
            <feComposite in2="SourceGraphic" operator="in" />
          </filter>
        </defs>

        {/* Coat, broad shoulder mass */}
        <path d="M 60 460 L 60 360 Q 120 290 180 286 L 280 286 Q 360 296 410 360 L 410 460 Z"
              fill="url(#wgoCoat)" />
        {/* Coat highlight, collar lapel */}
        <path d="M 175 290 L 220 360 L 200 460 L 130 460 L 110 380 Z"
              fill="url(#wgoCoatHi)" />
        {/* Turned-up collar V */}
        <path d="M 200 290 L 235 240 L 270 290 L 250 320 L 235 305 L 220 320 Z"
              fill="#0e0604" opacity="0.85" />

        {/* Neck */}
        <path d="M 210 270 Q 235 280 260 270 L 258 300 Q 235 308 212 300 Z"
              fill="#3a1f10" />

        {/* Head, three-quarter facing left */}
        <ellipse cx="232" cy="200" rx="68" ry="82" fill="url(#wgoFace)" />

        {/* Cheek/jaw shadow, defines the turn */}
        <path d="M 175 200 Q 180 245 220 270 L 210 280 Q 175 260 168 220 Z"
              fill="#0d0604" opacity="0.55" />

        {/* Hair, close-cropped afro silhouette, top edge */}
        <path d="M 162 168 Q 168 122 232 116 Q 295 122 300 172 Q 290 150 232 145 Q 178 152 162 168 Z"
              fill="#0a0402" />
        <ellipse cx="232" cy="142" rx="68" ry="22" fill="#0a0402" />

        {/* Brow shadow */}
        <path d="M 178 175 Q 232 168 286 178 L 282 188 Q 232 180 182 188 Z"
              fill="#0d0604" opacity="0.7" />

        {/* Faint moustache, Marvin trademark */}
        <path d="M 218 226 Q 232 232 246 226 L 244 232 Q 232 236 220 232 Z"
              fill="#1a0a05" opacity="0.7" />

        {/* Eye, single visible (three-quarter view), looking off-camera */}
        <ellipse cx="218" cy="195" rx="4.5" ry="2.2" fill="#0a0402" />

        {/* Rain streaks, diagonal soft lines */}
        <g stroke="#e8c896" strokeWidth="0.6" opacity="0.18">
          {Array.from({ length: 38 }).map((_, i) => {
            const x = (i * 17) % 460;
            const y = (i * 31) % 460;
            return <line key={i} x1={x} y1={y} x2={x - 14} y2={y + 30} />;
          })}
        </g>
        <g stroke="#fff1d6" strokeWidth="0.4" opacity="0.1">
          {Array.from({ length: 22 }).map((_, i) => {
            const x = (i * 41 + 8) % 460;
            const y = (i * 23 + 12) % 460;
            return <line key={i} x1={x} y1={y} x2={x - 10} y2={y + 22} />;
          })}
        </g>

        {/* Subtle film grain layer */}
        <rect width="460" height="460" filter="url(#wgoGrain)" opacity="0.55" />
      </svg>

      {/* "MARVIN GAYE" type, top, condensed sepia */}
      <div style={{
        position: "absolute", top: size * 0.06, left: size * 0.07, right: size * 0.07,
        fontFamily: "'Playfair Display', serif",
        fontWeight: 900,
        fontSize: size * 0.082,
        letterSpacing: "0.04em",
        color: "#f4d39a",
        textShadow: "0 1px 2px rgba(0,0,0,0.5)",
        zIndex: 2
      }}>MARVIN GAYE</div>

      {/* "What's Going On", script-style italic overlapping the portrait */}
      <div style={{
        position: "absolute",
        top: size * 0.32,
        left: size * 0.06,
        right: size * 0.06,
        fontFamily: "'Playfair Display', serif",
        fontStyle: "italic",
        fontWeight: 700,
        fontSize: size * 0.155,
        lineHeight: 0.92,
        color: "#fbe2b3",
        letterSpacing: "-0.01em",
        textShadow: "0 2px 6px rgba(0,0,0,0.55)",
        zIndex: 2,
        textWrap: "balance"
      }}>
        <span style={{ display: "block", transform: "rotate(-2deg)", transformOrigin: "left top" }}>What&apos;s</span>
        <span style={{ display: "block", marginLeft: size * 0.18, transform: "rotate(-1deg)", marginTop: size * 0.01 }}>Going On</span>
      </div>

      {/* Tamla logo / catalog, bottom-right, mono */}
      <div style={{
        position: "absolute", bottom: size * 0.04, right: size * 0.06,
        fontFamily: "'JetBrains Mono', monospace",
        fontSize: size * 0.028,
        letterSpacing: "0.18em",
        color: "#d9a874",
        opacity: 0.85,
        zIndex: 2
      }}>TAMLA · TS-310</div>

      {/* Bottom-left small year */}
      <div style={{
        position: "absolute", bottom: size * 0.04, left: size * 0.06,
        fontFamily: "'JetBrains Mono', monospace",
        fontSize: size * 0.028,
        letterSpacing: "0.2em",
        color: "#d9a874",
        opacity: 0.85,
        zIndex: 2
      }}>1971</div>

      {/* Hotspots overlay, only when requested */}
      {hotspots && WGO_HOTSPOTS.map((h) => (
        <button
          key={h.id}
          className={"wgo-hot " + (active === h.id ? "is-active" : "")}
          style={{ left: h.x + "%", top: h.y + "%" }}
          onClick={() => {
            const next = active === h.id ? null : h.id;
            setActive(next);
            onHotspot && onHotspot(next);
          }}
          aria-label={h.title}
        >
          <span className="wgo-hot-dot">{h.n}</span>
        </button>
      ))}

      {/* "Recreation, not original" disclosure, tiny, corner */}
      <div className="wgo-cover-disclosure" aria-hidden="true">
        Editorial recreation
      </div>
    </div>
  );
}

/* Hotspot positions are percentage offsets within the cover frame. */
const WGO_HOTSPOTS = [
  {
    id: "type",
    n: 1,
    x: 12, y: 9,
    title: "The masthead",
    body: "Curtis McNair set 'MARVIN GAYE' in a custom hand-drawn condensed serif, sized so the artist's name reads first. The hierarchy was a deliberate break from Motown's house style, where the song title usually led."
  },
  {
    id: "script",
    n: 2,
    x: 12, y: 38,
    title: "The script",
    body: "The script title is two-line and tilted, handlettered by McNair, intentionally informal so it would feel personal rather than promotional. The looping G's were redrawn nine times before final."
  },
  {
    id: "rain",
    n: 3,
    x: 78, y: 25,
    title: "The rain",
    body: "Photographer Jim Hendin shot Gaye in a real January downpour outside Marvin's home, no studio rain rig. The streaks you see in the printed sleeve are airbrushed in over Hendin's negative to amplify what the weather already gave them."
  },
  {
    id: "collar",
    n: 4,
    x: 50, y: 68,
    title: "The collar",
    body: "The turned-up trench collar is the design's emotional anchor, it tells you the man is cold, looking outward, alone with the weather. McNair cropped tight to the collar specifically; the original Hendin frames showed Gaye's full torso."
  }
];

/* ============================================================
   2. AboutCover, designer story + clickable hotspots
   ============================================================
   Renders the album's real cover image (covers/<rank>.jpg) at 420px
   square with absolutely-positioned hotspot buttons sourced from
   data.hotspots. Falls back to the WGO CSS recreation only if no
   album is passed (legacy path). */
function AboutCover({ data, album }) {
  const [active, setActive] = useWAState(null);
  // Normalise ONCE here — de-collide and renumber — so the dots on the sleeve and
  // the callout below it always show the same marker number.
  const hotspots = spreadCollisions(
    (data && data.hotspots) || (album && album.slug === "whats-going-on" ? WGO_HOTSPOTS : [])
  );
  return (
    <div className="cover-about">
      <div className="cover-about-art">
        {album ? (
          <CoverWithHotspots
            album={album}
            hotspots={hotspots}
            active={active}
            setActive={setActive}
            size={420}
          />
        ) : (
          <WGOCover size={420} hotspots={true} onHotspot={setActive} />
        )}
      </div>

      <div className="cover-about-body">
        {/* Omit a credit we do not have rather than printing an empty row with a
            label. A blank beside "Photography" reads as though the photographer is
            unknown to history, when it usually means we have not sourced it. */}
        <div className="cover-credits">
          {data.artDirector && <div><b>Art direction</b><span>{data.artDirector}</span></div>}
          {data.photographer && <div><b>Photography</b><span>{data.photographer}</span></div>}
          {data.typeface && <div><b>Type</b><span>{data.typeface}</span></div>}
          {data.format && <div><b>Format</b><span>{data.format}</span></div>}
        </div>

        {data.lede && <p className="cover-story-lede">{data.lede}</p>}
        {/* `data.paragraphs.map` used to run unguarded. Every cover block in the
            corpus happened to carry the field, so it never fired — but this is the
            same shape as the `t.listen.at` read that white-screened Ray of Light and
            Discovery in production: one absent key and React unmounts the page. */}
        {Array.isArray(data.paragraphs) && data.paragraphs.map((p, i) => (
          <p key={i} className="cover-story-p" dangerouslySetInnerHTML={{ __html: p }} />
        ))}

        <div className="cover-hint">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
            <circle cx="12" cy="12" r="10"/>
            <line x1="12" y1="8" x2="12" y2="13"/>
            <circle cx="12" cy="16" r="0.8" fill="currentColor"/>
          </svg>
          Click the numbered points on the cover to read the design notes.
        </div>

        {active && (() => {
          const h = hotspots.find(h => h.id === active);
          if (!h) return null;
          return (
            <div className="cover-callout-active">
              <span className="cover-callout-num">{h.n}</span>
              <div>
                <div className="cover-callout-title">{h.title}</div>
                <p>{h.body}</p>
              </div>
            </div>
          );
        })()}
      </div>
    </div>
  );
}

/* CoverWithHotspots, square frame holding the real album cover image
   (covers/<rank>.jpg) with absolutely-positioned hotspot buttons. The
   hotspots use the same .wgo-hot class as the legacy WGO recreation, so
   styling carries over for free. */
/* Two annotations written at the same coordinates render exactly on top of each
   other, so the lower-numbered marker is invisible and readers see the sequence
   start at 2. That was live on 11 albums. Rather than invent placements for
   sleeves an editor hasn't re-checked, fan any colliding group out around its
   shared point: every marker becomes reachable, and none moves far enough to
   point at something new. Sleeves with distinct coordinates are untouched. */
function spreadCollisions(hotspots) {
  const groups = new Map();
  for (const h of hotspots) {
    const key = `${h.x},${h.y}`;
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(h);
  }
  const out = [];
  for (const group of groups.values()) {
    if (group.length === 1) { out.push(group[0]); continue; }
    const R = 9; // percent of the sleeve — far enough to separate, near enough to stay on target
    group.forEach((h, i) => {
      const angle = (2 * Math.PI * i) / group.length - Math.PI / 2;
      out.push({
        ...h,
        x: Math.max(4, Math.min(96, h.x + R * Math.cos(angle))),
        y: Math.max(4, Math.min(96, h.y + R * Math.sin(angle))),
      });
    });
  }
  // Keep the authored order, and renumber from position rather than trusting the
  // authored `n`. Annotations are appended by a second data file that numbers
  // from the base array's length, so a gap appears whenever an entry is later
  // removed — purple-rain shipped markers 1, 2, 4, 5. Deriving the number from
  // position means the reader always sees 1..n with no gaps.
  return hotspots.map((h, i) => ({ ...(out.find((o) => o.id === h.id) || h), n: i + 1 }));
}

function CoverWithHotspots({ album, hotspots: rawHotspots, active, setActive, size = 420 }) {
  const src = "/covers/" + album.slug + ".jpg";
  // Idempotent: AboutCover already normalised these, but the legacy call sites
  // pass raw data, so run it here too. spreadCollisions on an already-spread set
  // is a no-op because the collisions are gone.
  const hotspots = spreadCollisions(rawHotspots || []);
  return (
    <div
      className="cover-hot-wrap"
      style={{ width: "100%", maxWidth: size, aspectRatio: "1 / 1", position: "relative", flexShrink: 0, background: "#0a0a0a" }}
    >
      <img
        src={src}
        alt={album.title + " by " + album.artist + ", original cover"}
        style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
      />
      {hotspots.map((h) => (
        <button
          key={h.id}
          className={"wgo-hot " + (active === h.id ? "is-active" : "")}
          style={{ left: h.x + "%", top: h.y + "%" }}
          onClick={() => {
            const next = active === h.id ? null : h.id;
            setActive(next);
            if (next) {
              // Nudge the note (which renders below the image) into view so it's
              // obvious the dot changed the copy.
              setTimeout(() => {
                const el = document.querySelector(".cover-callout-active");
                if (el && window.matchMedia("(max-width: 760px)").matches) {
                  el.scrollIntoView({ behavior: "smooth", block: "center" });
                }
              }, 60);
            }
          }}
          aria-label={h.title}
          aria-pressed={active === h.id}
        >
          <span className="wgo-hot-dot">{h.n}</span>
        </button>
      ))}
    </div>
  );
}

/* ============================================================
   3. MakingGallery, large + 2-stack layout (option 0)
   Each frame is a CSS-illustrated placeholder with caption and
   an explicit "would-be" credit so the editor knows what to drop in.
   ============================================================ */
function MakingGallery({ images }) {
  const [lightbox, setLightbox] = useWAState(null);
  // True when at least one frame has a reference image wired in.
  const anyRef = images.some((im) => im && im.imageUrl);
  // Robust to short galleries, only render frames that actually exist.
  const primary = images[0];
  const stacked = images.slice(1, 3);
  if (!primary) return null;
  return (
    <>
      <div className={"mk-gallery" + (stacked.length === 0 ? " mk-gallery--solo" : "")}>
        <button className="mk-frame mk-frame-large" onClick={() => setLightbox(0)}>
          <FrameArt image={primary} />
          <span className="mk-cap">
            <span className="mk-cap-num">01</span>
            <span className="mk-cap-title">{primary.title}</span>
            {primary.imageCredit && <span className="mk-cap-credit">{primary.imageCredit}</span>}
          </span>
        </button>
        {stacked.length > 0 && (
          <div className="mk-stack">
            {stacked.map((im, i) => (
              <button key={i} className="mk-frame" onClick={() => setLightbox(i + 1)}>
                <FrameArt image={im} />
                <span className="mk-cap">
                  <span className="mk-cap-num">{String(i + 2).padStart(2, "0")}</span>
                  <span className="mk-cap-title">{im.title}</span>
                  {im.imageCredit && <span className="mk-cap-credit">{im.imageCredit}</span>}
                </span>
              </button>
            ))}
          </div>
        )}
      </div>

      {lightbox !== null && (
        <div className="mk-lightbox" onClick={() => setLightbox(null)} role="dialog">
          <button className="mk-close" onClick={() => setLightbox(null)} aria-label="Close">×</button>
          <div className="mk-lightbox-inner" onClick={(e) => e.stopPropagation()}>
            <div className="mk-lightbox-art"><FrameArt image={images[lightbox]} large /></div>
            <div className="mk-lightbox-meta">
              <div className="mk-cap-num">{String(lightbox + 1).padStart(2, "0")}</div>
              <h4>{images[lightbox].title}</h4>
              <p>{images[lightbox].caption}</p>
              {images[lightbox].imageCredit && <p className="mk-cap-credit">{images[lightbox].imageCredit}</p>}
            </div>
          </div>
        </div>
      )}
    </>
  );
}

/* FrameArt, render the wired reference image when present, fall back to the
   editorial CSS/SVG illustration otherwise. Keeps the placeholder behaviour
   untouched for frames that haven't been wired yet. */
function FrameArt({ image, large }) {
  if (image && image.imageUrl) {
    return (
      <div className="ga-art ga-photo">
        <img
          src={image.imageUrl}
          alt={image.title || ""}
          loading="lazy"
          style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
        />
      </div>
    );
  }
  return <GalleryArt kind={image && image.kind} large={large} />;
}

/* GalleryArt, illustrated CSS/SVG placeholders per scene type */
function GalleryArt({ kind, large }) {
  // SVG gradient and filter ids live in a DOCUMENT-wide namespace, not a per-SVG
  // one, so two instances of the same scene on one page defined the same id twice
  // and every `url(#…)` in the second resolved to the FIRST instance's gradient.
  // bitches-brew has two gallery frames of kind "session" and did exactly that.
  // Here the two gradients happen to be identical so nothing looked wrong, which is
  // the bad kind of bug: invalid markup, correct-looking output, and a real
  // mis-paint the moment the two scenes differ. useId gives each instance its own
  // namespace.
  const uid = React.useId().replace(/[^a-zA-Z0-9]/g, "");
  const gid = (name) => `${name}-${uid}`;

  if (kind === "hitsville") {
    // Studio control room, warm tungsten, mixing console, headphones
    return (
      <div className="ga-art ga-hitsville">
        <svg viewBox="0 0 800 600" preserveAspectRatio="xMidYMid slice">
          <defs>
            <linearGradient id={gid("hRoom")} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor="#3a2a1c"/>
              <stop offset="100%" stopColor="#0f0905"/>
            </linearGradient>
            <radialGradient id={gid("hLamp")} cx="0.7" cy="0.25" r="0.5">
              <stop offset="0%" stopColor="#f5c46a" stopOpacity="0.7"/>
              <stop offset="100%" stopColor="#f5c46a" stopOpacity="0"/>
            </radialGradient>
          </defs>
          <rect width="800" height="600" fill={`url(#${gid("hRoom")})`}/>
          <rect width="800" height="600" fill={`url(#${gid("hLamp")})`}/>
          {/* Back wall, acoustic panels */}
          {Array.from({ length: 8 }).map((_, i) => (
            <rect key={i} x={40 + i * 92} y="80" width="76" height="180" fill="#5a3a22" opacity="0.65"/>
          ))}
          {/* Mixing console */}
          <rect x="60" y="340" width="680" height="180" fill="#1a1208" stroke="#d97757" strokeWidth="1.5"/>
          <rect x="60" y="340" width="680" height="22" fill="#2a1c10"/>
          {/* Console knobs grid */}
          {Array.from({ length: 28 }).map((_, i) => {
            const col = i % 14, row = Math.floor(i / 14);
            return <circle key={i} cx={90 + col * 47} cy={395 + row * 60} r="9" fill="#3a2614" stroke="#d97757" strokeWidth="0.8"/>;
          })}
          {/* Faders */}
          {Array.from({ length: 14 }).map((_, i) => (
            <g key={i}>
              <rect x={84 + i * 47} y="475" width="6" height="40" fill="#0a0604"/>
              <rect x={80 + i * 47} y={478 + (i % 5) * 6} width="14" height="8" fill="#e8c896"/>
            </g>
          ))}
          {/* Tape reels */}
          <circle cx="180" cy="160" r="55" fill="#1a1208" stroke="#7a5028" strokeWidth="3"/>
          <circle cx="180" cy="160" r="22" fill="#2a1c10"/>
          <circle cx="180" cy="160" r="6" fill="#d97757"/>
          <circle cx="320" cy="160" r="55" fill="#1a1208" stroke="#7a5028" strokeWidth="3"/>
          <circle cx="320" cy="160" r="22" fill="#2a1c10"/>
          <circle cx="320" cy="160" r="6" fill="#d97757"/>
          {/* Microphone silhouette */}
          <g transform="translate(580 130)">
            <rect x="-8" y="0" width="16" height="50" rx="6" fill="#d4a574"/>
            <rect x="-2" y="50" width="4" height="100" fill="#5a3a22"/>
            <rect x="-30" y="150" width="60" height="6" fill="#3a2614"/>
          </g>
          {/* Headphones on console */}
          <g transform="translate(640 320)">
            <path d="M 0 30 Q 0 -10 35 -10 Q 70 -10 70 30" fill="none" stroke="#2a1c10" strokeWidth="6"/>
            <ellipse cx="2" cy="40" rx="14" ry="18" fill="#1a1208"/>
            <ellipse cx="68" cy="40" rx="14" ry="18" fill="#1a1208"/>
          </g>
          {/* Light streak */}
          <path d="M 600 0 L 760 0 L 760 200 Z" fill="#f5c46a" opacity="0.06"/>
        </svg>
        {large && <div className="ga-glow"/>}
      </div>
    );
  }

  if (kind === "session") {
    // Marvin at the mic, three-quarter, condenser, headphones
    return (
      <div className="ga-art ga-session">
        <svg viewBox="0 0 800 600" preserveAspectRatio="xMidYMid slice">
          <defs>
            <radialGradient id={gid("sBg")} cx="0.5" cy="0.5" r="0.7">
              <stop offset="0%" stopColor="#3a2210"/>
              <stop offset="100%" stopColor="#080402"/>
            </radialGradient>
            <radialGradient id={gid("sLight")} cx="0.62" cy="0.38" r="0.35">
              <stop offset="0%" stopColor="#f5c46a" stopOpacity="0.55"/>
              <stop offset="100%" stopColor="#f5c46a" stopOpacity="0"/>
            </radialGradient>
          </defs>
          <rect width="800" height="600" fill={`url(#${gid("sBg")})`}/>
          <rect width="800" height="600" fill={`url(#${gid("sLight")})`}/>

          {/* Figure, silhouette, three-quarter, behind mic */}
          <ellipse cx="500" cy="220" rx="80" ry="98" fill="#3a2210"/>
          <ellipse cx="500" cy="160" rx="80" ry="40" fill="#0a0402"/>{/* hair */}
          <path d="M 420 240 Q 420 320 460 360 L 540 360 Q 580 320 580 240 L 580 600 L 420 600 Z" fill="#1a0e07"/>
          {/* Headphone band over silhouette */}
          <path d="M 422 165 Q 500 100 578 165" stroke="#5a3a22" strokeWidth="10" fill="none"/>
          <ellipse cx="420" cy="195" rx="22" ry="28" fill="#0a0402"/>
          <ellipse cx="580" cy="195" rx="22" ry="28" fill="#0a0402"/>

          {/* Mic stand + condenser */}
          <rect x="318" y="280" width="6" height="320" fill="#3a2614"/>
          <ellipse cx="321" cy="600" rx="80" ry="6" fill="#1a0e07"/>
          <g transform="translate(290 220)">
            <rect x="0" y="0" width="62" height="100" rx="8" fill="#d4a574" stroke="#7a5028" strokeWidth="2"/>
            {/* Mesh lines */}
            {Array.from({ length: 8 }).map((_, i) => (
              <line key={i} x1="6" y1={12 + i * 11} x2="56" y2={12 + i * 11} stroke="#5a3a22" strokeWidth="0.7"/>
            ))}
            {Array.from({ length: 5 }).map((_, i) => (
              <line key={i} x1={12 + i * 12} y1="6" x2={12 + i * 12} y2="94" stroke="#5a3a22" strokeWidth="0.7"/>
            ))}
            <rect x="20" y="100" width="22" height="14" fill="#5a3a22"/>
          </g>

          {/* Pop filter, circle */}
          <circle cx="380" cy="270" r="42" fill="none" stroke="#f5e3c0" strokeWidth="1.5" opacity="0.5"/>
          <circle cx="380" cy="270" r="42" fill="#f5e3c0" opacity="0.04"/>

          {/* Lyric sheet held in hand silhouette */}
          <g transform="translate(120 380) rotate(-8)">
            <rect width="140" height="170" fill="#e8d5a8" stroke="#7a5028" strokeWidth="1"/>
            {Array.from({ length: 9 }).map((_, i) => (
              <line key={i} x1="14" y1={20 + i * 16} x2="120" y2={20 + i * 16} stroke="#3a2210" strokeWidth="1"/>
            ))}
          </g>

          {/* Lens flare */}
          <circle cx="640" cy="120" r="4" fill="#f5c46a"/>
          <circle cx="640" cy="120" r="40" fill="#f5c46a" opacity="0.08"/>
        </svg>
      </div>
    );
  }

  if (kind === "lyrics") {
    // Lyric sheet on a desk, handwritten lines
    return (
      <div className="ga-art ga-lyrics">
        <svg viewBox="0 0 800 600" preserveAspectRatio="xMidYMid slice">
          <defs>
            <linearGradient id={gid("lDesk")} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor="#1f1208"/>
              <stop offset="100%" stopColor="#0a0604"/>
            </linearGradient>
          </defs>
          <rect width="800" height="600" fill={`url(#${gid("lDesk")})`}/>
          {/* Wood grain */}
          {Array.from({ length: 30 }).map((_, i) => (
            <line key={i} x1="0" y1={i * 22} x2="800" y2={i * 22 + 6} stroke="#3a2210" strokeWidth="0.5" opacity="0.4"/>
          ))}
          {/* Sheet */}
          <g transform="translate(120 60) rotate(-3)">
            <rect width="500" height="500" fill="#f0e0bc" stroke="#7a5028" strokeWidth="1.5"/>
            <rect x="0" y="0" width="500" height="36" fill="#7a5028" opacity="0.18"/>
            {/* Header text, hand-style */}
            <text x="20" y="60" fontFamily="'Playfair Display', serif" fontStyle="italic" fontSize="22" fill="#3a2210">
              What's Going On, Verse 1
            </text>
            {/* Lines, with strikethroughs and edits */}
            {[
              { y: 100, w: 380, edit: false },
              { y: 130, w: 320, edit: true },
              { y: 160, w: 400, edit: false },
              { y: 190, w: 280, edit: false },
              { y: 220, w: 360, edit: true },
              { y: 250, w: 300, edit: false },
              { y: 290, w: 200, edit: false },
              { y: 320, w: 350, edit: false },
              { y: 350, w: 260, edit: true },
              { y: 380, w: 320, edit: false },
              { y: 420, w: 340, edit: false },
              { y: 450, w: 280, edit: false },
            ].map((l, i) => (
              <g key={i}>
                <rect x="20" y={l.y} width={l.w} height="3" fill="#3a2210" opacity="0.7"/>
                {l.edit && <line x1="20" y1={l.y + 1} x2={20 + l.w * 0.6} y2={l.y + 1} stroke="#a83232" strokeWidth="1.2"/>}
                {l.edit && (
                  <text x={20 + l.w + 8} y={l.y + 5} fontFamily="'Playfair Display', serif" fontStyle="italic" fontSize="14" fill="#a83232">
                    rev.
                  </text>
                )}
              </g>
            ))}
            <text x="20" y="490" fontFamily="'JetBrains Mono', monospace" fontSize="11" fill="#7a5028">
              MG / Aug 1970
            </text>
          </g>
          {/* Pen */}
          <g transform="translate(560 380) rotate(40)">
            <rect width="180" height="10" rx="3" fill="#1a0e07"/>
            <rect x="170" width="14" height="10" fill="#d97757"/>
            <polygon points="184,0 200,5 184,10" fill="#3a2210"/>
          </g>
          {/* Coffee ring */}
          <circle cx="640" cy="150" r="58" fill="none" stroke="#5a3a22" strokeWidth="3" opacity="0.45"/>
          <circle cx="640" cy="150" r="58" fill="#3a2210" opacity="0.18"/>
        </svg>
      </div>
    );
  }

  return null;
}

/* ============================================================
   4. StickyBuyRail, Amazon affiliate module that follows reader
   ------------------------------------------------------------
   Mounts to the right edge of the article column.
   - Hides until the user has scrolled past the hero.
   - Auto-tucks above the footer (via IntersectionObserver of footer).
   - Dismissible; remembers dismissal in localStorage per slug.
   ============================================================ */
function StickyBuyRail({ slug, items, title, subtitle, artist, albumTitle }) {
  // The second buy rail on the site, on the artwork dossiers. Hidden while commerce
  // is off — see window.BBR_COMMERCE_ENABLED in data/collection_sample.js. Not
  // replaced with an add bar here: AlbumExtras' StickyBuyBar already follows the
  // reader down these same pages, so an add action is present either way and two
  // floating rails on one page is one too many.
  if (!window.BBR_COMMERCE_ENABLED) return null;
  // Many of the per-pressing Amazon URLs in the dataset return "No results
  // for your search query in Music" because the qualifier text is too
  // specific. We route every vinyl-format Amazon item to a clean album-vinyl
  // search instead, and collapse those items into a single "Vinyl LP, current
  // pressings" card so the same destination isn't shown under three different
  // headings. Books, biographies and documentaries (13 entries in the dataset)
  // keep their original URL and format text.
  const genericAmazon = (artist && albumTitle)
    ? "https://www.amazon.co.uk/s?k=" + encodeURIComponent(artist + " " + albumTitle + " vinyl") + "&i=popular&tag=theleadin1-21"
    : null;
  const NON_VINYL_RE = /\b(book|biograph|documentary|film|cassette|tape|cd only|hardcover|paperback)\b/i;
  const isAmazonVinyl = (it) =>
    (it.vendor || "").toLowerCase().includes("amazon") && !NON_VINYL_RE.test(it.format || "");

  // Build a working list: collapse Amazon-vinyl items into one card, keep
  // non-vinyl items as themselves.
  const safeItems = items || [];
  const amazonVinyl = safeItems.filter(isAmazonVinyl);
  const others = safeItems.filter(it => !isAmazonVinyl(it));
  let displayItems = safeItems;
  if (amazonVinyl.length && genericAmazon) {
    const nums = amazonVinyl
      .map(it => parseFloat((it.price || "").replace(/[^\d.]/g, "")))
      .filter(n => !isNaN(n));
    const minP = nums.length ? Math.min(...nums) : null;
    const collapsed = {
      format: amazonVinyl.length > 1 ? "Vinyl LP, current pressings" : "Vinyl LP",
      vendor: "Amazon UK",
      price: minP != null ? "From £" + minP.toFixed(2) : (amazonVinyl[0].price || ""),
      url: genericAmazon,
      note: "Browse the current Amazon UK listings for " + (albumTitle || "this record") + " on vinyl.",
    };
    displayItems = [collapsed, ...others];
  }
  const resolveUrl = (it) => (it && it.url) || "#";
  const [show, setShow] = useWAState(false);
  const [hide, setHide] = useWAState(false);
  const [dismissed, setDismissed] = useWAState(() => {
    try { return localStorage.getItem("bbr-buyrail-dismiss-" + slug) === "1"; } catch (e) { return false; }
  });
  const [openMore, setOpenMore] = useWAState(false);

  useWAEffect(() => {
    const onScroll = () => {
      // Show after 600px scroll (past hero); hide if footer is in viewport
      const sc = window.scrollY || document.documentElement.scrollTop;
      setShow(sc > 600);
      const footer = document.querySelector(".endnote");
      if (footer) {
        const r = footer.getBoundingClientRect();
        setHide(r.top < window.innerHeight - 60);
      }
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  const dismiss = () => {
    setDismissed(true);
    try { localStorage.setItem("bbr-buyrail-dismiss-" + slug, "1"); } catch (e) {}
  };

  if (dismissed || !items || !items.length) return null;

  const visible = show && !hide;
  const top = displayItems[0];
  const more = displayItems.slice(1);

  return (
    <aside className={"buyrail " + (visible ? "is-visible" : "")} aria-label="Get a copy">
      <button className="buyrail-close" onClick={dismiss} aria-label="Dismiss">×</button>
      <div className="buyrail-eyebrow">
        <span className="buyrail-dot" /> Get a copy
      </div>
      <div className="buyrail-title">{title}</div>
      <div className="buyrail-sub">{subtitle}</div>

      <a className="buyrail-primary" href={resolveUrl(top)} target="_blank" rel="sponsored noopener">
        <div className="buyrail-prim-row">
          <span className="buyrail-format">{top.format}</span>
          <span className="buyrail-price">{top.price}</span>
        </div>
        <div className="buyrail-vendor">
          <AmazonGlyph /> Buy on {top.vendor}
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M7 17L17 7M9 7h8v8"/></svg>
        </div>
        {top.note && <div className="buyrail-note">{top.note}</div>}
      </a>

      {more.length > 0 && (
        <>
          <button
            className="buyrail-toggle"
            onClick={() => setOpenMore(!openMore)}
            aria-expanded={openMore}
          >
            {openMore ? "Hide" : "Show"} {more.length} more format{more.length === 1 ? "" : "s"}
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{ transform: openMore ? "rotate(180deg)" : "none", transition: "transform .2s" }}><path d="M6 9l6 6 6-6"/></svg>
          </button>
          {openMore && (
            <div className="buyrail-more">
              {more.map((m, i) => (
                <a key={i} className="buyrail-row" href={resolveUrl(m)} target="_blank" rel="sponsored noopener">
                  <span className="buyrail-row-fmt">{m.format}</span>
                  <span className="buyrail-row-price">{m.price}</span>
                  <span className="buyrail-row-cta">View →</span>
                </a>
              ))}
            </div>
          )}
        </>
      )}

      <div className="buyrail-disclose">
        The Lead-In is an Amazon Associate. We earn a small commission on qualifying purchases, at no extra cost to you.
      </div>
    </aside>
  );
}

function AmazonGlyph() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M3 16c4 4 14 4 18 0" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
      <path d="M19 17l3 1-1-3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
      <text x="12" y="11" textAnchor="middle" fontFamily="'JetBrains Mono', monospace" fontSize="9" fontWeight="700" fill="currentColor">a</text>
    </svg>
  );
}

Object.assign(window, {
  WGOCover, AboutCover, CoverWithHotspots, MakingGallery, StickyBuyRail
});
