// ===========================================================================
// RecordPage — /record/<key>: the page every record gets (17 Oct 2026).
//
// Phase 1 of the "mini deep dive" Ric asked for: when a collector adds a
// record we have no deep dive for, somewhere to send them with the basics.
// When it first came out, the pressing they have, the tracklist, who made it,
// who else owns it, and the Wikipedia lead where the match is verified.
// Built server-side by lib/record_page.js; read in one call, record_page()
// (supabase/migrations/record_pages.sql), owners by record_page_owners().
//
// <key> is the page's own ('m<master>' / 'r<release>') or 'r<release id>' of
// any pressing on a shelf, which is the link a shelf record carries. A page
// not built yet is built for its owner on the spot (store.claimRecordPage);
// anyone else is told there is no page yet.
//
// Served noindex (vercel.json x-robots-tag and bbrApplyRouteMeta): thousands
// of generated pages are a thin-content risk the deep dives cannot afford.
// ===========================================================================
const { useState: useRpState, useEffect: useRpEffect, useMemo: useRpMemo } = React;

const RP_MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
// "1990-08-06" -> "on 6 August 1990"; "1990-08" -> "in August 1990"; "1990" -> "in 1990".
// Discogs writes an unknown day or month as 00 ("1988-04-00"): that part is left out.
function rpWhen(date, year) {
  const m = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(String(date || ""));
  const mo = m && m[2] && Number(m[2]) >= 1 && Number(m[2]) <= 12 ? Number(m[2]) : null;
  const dy = mo && m[3] && Number(m[3]) >= 1 ? Number(m[3]) : null;
  if (m && dy) return "on " + dy + " " + RP_MONTHS[mo - 1] + " " + m[1];
  if (m && mo) return "in " + RP_MONTHS[mo - 1] + " " + m[1];
  if (m) return "in " + m[1];
  return year ? "in " + year : "";
}
function rpYearOf(p) {
  const m = /^(\d{4})/.exec(String(p.first_release_date || ""));
  return m ? Number(m[1]) : (p.year || null);
}
const rpVarious = (a) => /^various( artists)?$/i.test(String(a || "").trim());
const rpLabel = (l) => (l && !/^not on label/i.test(l) ? l : null);
// The source's own markup in release notes ([a=Name], [url=...]text[/url]) is
// reduced to its words. A line pointing at another entry by number ([r=1569889],
// [m25702], [l123]) means nothing here, and a line that still names the source is
// dropped too: no reader-facing text names it.
function rpPlain(s) {
  return String(s || "").split(/\n/)
    .filter((l) => !/\[(?:a|l|m|r)=?\d+\]/i.test(l))
    .map((l) => l
      .replace(/\[url=[^\]]*\]([^[]*)\[\/url\]/gi, "$1")
      .replace(/\[(?:a|l|m|r)=([^\]]+)\]/gi, "$1")
      .replace(/\[\/?[biu]\]/gi, ""))
    .filter((l) => !/discogs/i.test(l))
    .join("\n").trim();
}

// One sentence in our own words from the CC0 facts, so every page opens with
// something readable whether or not Wikipedia has an article.
function rpIntro(p) {
  const raw = String(p.kind || "").toLowerCase();
  const kind = raw === "ep" ? "EP" : (raw || "record");
  const various = rpVarious(p.artist);
  const noun = (various ? "various-artists " : "") + kind;
  const article = /^[aeiou]/i.test(noun) || noun === "EP" ? "An" : "A";
  let s = article + " " + noun + (various ? "" : " by " + p.artist);
  const when = rpWhen(p.first_release_date, p.year);
  const label = rpLabel(p.label);
  if (when || label) s += ", first released" + (when ? " " + when : "") + (label ? " on the " + label + " label" : "");
  s += ".";
  const nv = Number(p.vinyl_versions) || 0;
  if (nv === 1) s += " There is one known vinyl pressing of it.";
  else if (nv > 1) s += " There are " + nv.toLocaleString("en-GB") + " known vinyl pressings of it.";
  else if (p.versions > 1) s += " There are " + Number(p.versions).toLocaleString("en-GB") + " known versions of it.";
  return s;
}
function rpFormat(formats) {
  const f = (formats || [])[0];
  if (!f) return "";
  const parts = [(f.qty && Number(f.qty) > 1 ? f.qty + "× " : "") + (f.name || "")].concat(f.descriptions || []);
  return parts.filter(Boolean).join(", ");
}
function rpPressingLine(r) {
  const y = /^(\d{4})/.exec(String(r.released || ""));
  return [[rpLabel(r.label), r.catno].filter(Boolean).join(" "), r.country, y && y[1]].filter(Boolean).join(" · ");
}
const rpGrade = (c) => (c ? String(c).replace("_PLUS", "+") : null);
// A row of Discogs' vinyl versions list, in the shape of a fetched release, for a
// copy whose own release has not been fetched yet.
const rpFromVersion = (v) => (v ? { release_id: v.release_id, label: v.label, catno: v.catno, country: v.country,
  released: v.released, formats: [{ name: "Vinyl", descriptions: String(v.format || "").split(/,\s*/).filter(Boolean) }] } : null);
const rpRunouts = (r) => ((r && r.identifiers) || []).filter((i) => /matrix|runout/i.test(i.type || "")).slice(0, 24);
// ---- runout etchings ------------------------------------------------------------
// The catalogue lists each etching on its own, with a description collectors type
// by hand: "Runout side A, variant 1", "Side B runout, variant 2", "A-side runout",
// "Label side A", "Runout side B, stamped variant 3". Listed as they come, a
// pressing with four sides in four variants was sixteen lines of codes and words
// (Ric, 18 Oct 2026: "messy, particularly where there's multiple variants"). So
// each is read for its side, its variant, whether it is on the label, and what is
// left of the words ("Stamped", "Etched", "Stamped / etched"), and they are laid
// out as a collector checks a record: side by side, variants across. Measured on
// the 3,046 stored etchings: 99.5% give a side, 72% a variant.
function rpParseEtch(i) {
  const raw = String((i && i.description) || "").replace(/\s+/g, " ").trim();
  let d = " " + raw + " ";
  let side = null, variant = null, onLabel = false;
  const take = (re, f) => { const m = re.exec(d); if (m) { f(m); d = d.replace(m[0], " "); } return !!m; };
  take(/\bvari(?:ant|ation)s?\s*#?\s*(\d{1,2})\b/i, (m) => { variant = Number(m[1]); });
  take(/\bv\.?\s*(\d{1,2})\b/i, (m) => { if (variant == null) variant = Number(m[1]); });
  if (!take(/\bside\s*[:#]?\s*([a-h]|\d{1,2})\b/i, (m) => { side = m[1].toUpperCase(); }))
    if (!take(/\b(?:runout|run-out|matrix|label)\s*,?\s*([a-h]|\d{1,2})\b(?=[\s,:;)]|$)/i, (m) => { side = m[1].toUpperCase(); }))
      if (!take(/\b([a-h])[\s-]?side\b/i, (m) => { side = m[1].toUpperCase(); }))
        take(/^\s*([a-h])\b(?=[\s,:;)]|$)/i, (m) => { side = m[1].toUpperCase(); });
  if (/\blabels?\b/i.test(d)) { onLabel = true; d = d.replace(/\b(cent(?:er|re)\s+)?labels?\b/gi, " "); }
  d = d.replace(/\b(matrix|runouts?|run-?outs?|dead ?wax|grooves?)\b/gi, " ");
  let note = d.replace(/[()\[\]]/g, " ")
    .replace(/\s*\/\s*/g, " / ")
    .replace(/\s*[,;:]\s*(?=[,;:]|$)/g, " ")
    .replace(/^[\s,;:/.-]+|[\s,;:/.-]+$/g, "")
    .replace(/\s{2,}/g, " ").replace(/\s+,/g, ",")
    .replace(/^(and|&)\s+/i, "");
  if (note) note = note.charAt(0).toUpperCase() + note.slice(1).replace(/\b(Etched|Stamped|Hand|Laser)\b/g, (w) => w.toLowerCase());
  // No side in the words: the code may carry one ("ILPS 9347 A-2", "DGC-24425-B2").
  if (!side) {
    const ms = String((i && i.value) || "").match(/(?:^|[\s-])\d?([A-D])[\s-]?\d{0,2}(?=$|[\s-])/g);
    if (ms && ms.length === 1) { const m = /([A-D])/.exec(ms[0]); if (m) side = m[1]; }
  }
  return { value: String((i && i.value) || "").trim(), side, variant, onLabel, note };
}
// Sides in order (A-H, then 1, 2...), and anything without one last.
const rpSideRank = (s) => (s == null ? 999 : /^\d+$/.test(s) ? 100 + Number(s) : s.charCodeAt(0));
function rpEtchLayout(ids) {
  const seen = new Set();
  const rows = (ids || []).map(rpParseEtch).filter((r) => {
    if (!r.value) return false;
    const k = [r.side, r.variant, r.onLabel, r.value].join("|");
    if (seen.has(k)) return false;
    seen.add(k); return true;
  });
  // Etchings with no variant number beside numbered ones are usually the main
  // matrix the variants differ from: their column comes first, as "Main".
  const variants = [...new Set(rows.map((r) => r.variant))].sort((a, b) => (b == null) - (a == null) || a - b);
  const sides = [...new Set(rows.map((r) => r.side))].sort((a, b) => rpSideRank(a) - rpSideRank(b));
  const notes = new Set(rows.map((r) => r.note || ""));
  // A note every etching shares ("Stamped") is said once, not on every line.
  const common = notes.size === 1 ? [...notes][0] : "";
  return { rows, variants, sides, common };
}
const rpVariantName = (v) => (v == null ? "Main" : "Variant " + v);
function RpEtchCell({ rows, common }) {
  return rows.map((r, n) => (
    <div className="rp-etch-c" key={n}>
      <span className="rp-etch-v">{r.value}</span>
      {(r.onLabel || (r.note && !common)) && (
        <span className="rp-etch-d">{[r.onLabel ? "On the label" : "", !common ? r.note : ""].filter(Boolean).join(" · ")}</span>
      )}
    </div>
  ));
}
// Up to three variants sit side by side as columns; more would leave each column
// too narrow for its codes (Wish You Were Here: a main set and eleven variants), so
// they stack by side as on a phone. Past four, the rest wait behind a button.
const RP_ETCH_COLS = 3, RP_ETCH_VARIANTS = 4;
function RpRunouts({ ids }) {
  const [allV, setAllV] = useRpState(false);
  const L = rpEtchLayout(ids);
  if (!L.rows.length) return null;
  const multi = L.variants.length > 1;
  const vs = multi && !allV ? L.variants.slice(0, RP_ETCH_VARIANTS) : L.variants;
  const stacked = vs.length > RP_ETCH_COLS;
  const at = (side, v) => L.rows.filter((r) => r.side === side && (!multi || r.variant === v));
  const sides = L.sides.filter((s) => !multi || vs.some((v) => at(s, v).length));
  const sideName = (s) => (s == null ? "—" : "Side " + s);
  return (
    <div className="rp-etch">
      {(multi || L.common) && (
        <p className="rp-etch-sum">{[multi ? L.variants.length + " variants" : "", L.common].filter(Boolean).join(" · ")}</p>
      )}
      <table className={"rp-etch-t" + (multi ? " is-multi" : "") + (stacked ? " is-stacked" : "")}>
        {multi && (
          <thead>
            <tr><th scope="col"><span className="rp-vh">Side</span></th>{vs.map((v) => <th scope="col" key={String(v)}>{rpVariantName(v)}</th>)}</tr>
          </thead>
        )}
        <tbody>
          {sides.map((s) => (
            <tr key={String(s)}>
              <th scope="row">{sideName(s)}</th>
              {multi
                ? vs.map((v) => <td key={String(v)} data-label={rpVariantName(v)}><RpEtchCell rows={at(s, v)} common={L.common} /></td>)
                : <td><RpEtchCell rows={at(s)} common={L.common} /></td>}
            </tr>
          ))}
        </tbody>
      </table>
      {multi && L.variants.length > RP_ETCH_VARIANTS && (
        <button type="button" className="rp-link rp-etch-all" onClick={() => setAllV((x) => !x)}>
          {allV ? "Fewer variants" : "Show all " + L.variants.length + " variants"}
        </button>
      )}
    </div>
  );
}
// A pressing named in two parts, for the copy card: who and which (label and
// catalogue number), then where and when.
const rpPressingName = (r) => [rpLabel(r.label), r.catno].filter(Boolean).join(" ");
const rpPressingWhere = (r) => { const y = /^(\d{4})/.exec(String(r.released || "")); return [r.country, y && y[1]].filter(Boolean).join(" · "); };
// A release's format as tags: "2×", "Vinyl" dropped (every pressing here is), and
// each description once (Discogs repeats "LP" per disc).
function rpFormatTags(r) {
  const fs = r.formats && r.formats.length ? r.formats
    : r.format ? [{ name: "Vinyl", descriptions: String(r.format).split(/,\s*/) }] : [];
  const out = [];
  fs.forEach((f) => {
    if (f.qty && Number(f.qty) > 1) out.push(f.qty + "×");
    if (f.name && !/^vinyl$/i.test(f.name)) out.push(f.name);
    (f.descriptions || []).forEach((d) => out.push(d));
    if (f.text) out.push(f.text);
  });
  return [...new Set(out.map((x) => String(x).trim()).filter(Boolean))].slice(0, 8);
}
// One pressing in the Pressings list: a vinyl version (format as Discogs words it)
// or a fetched release. `full` is the fetched release, for its runout etchings.
function RpPressRow({ r, full, first, yours, hl, linked }) {
  const ids = rpRunouts(full);
  const line = rpPressingLine(r) || r.title || "Pressing";
  const fmt = r.format != null ? r.format : rpFormat(r.formats);
  const owners = Number(r.owners) || 0;
  return (
    <li id={"rp-pr-" + r.release_id} className={(yours ? "is-yours" : "") + (first ? " is-first" : "") + (hl ? " is-hl" : "") + (linked ? " is-linked" : "")}>
      <div className="rp-pl-row">
        <div className="rp-pl-main">
          <span className="rp-pl-line">
            {line}
            {first && <span className="rp-pl-tag rp-pl-first">First pressing</span>}
            {yours && <span className="rp-pl-tag rp-pl-yours">Yours</span>}
            {linked && <span className="rp-pl-tag rp-pl-linked">This pressing</span>}
          </span>
          {fmt && <span className="rp-pl-fmt">{fmt}</span>}
        </div>
        <div className="rp-pl-side">
          {r.tier && window.TierChip && <TierChip tier={r.tier} />}
          {Number(r.ref_nm) > 0 && <span className="rp-pl-nm">Near Mint {rpGbp(Number(r.ref_nm))}</span>}
          {owners > 0 && <span className="rp-pl-n">{owners} {owners === 1 ? "collector" : "collectors"} here</span>}
        </div>
      </div>
      {ids.length > 0 && (
        <details className="rp-pl-ids">
          <summary>Runout etchings</summary>
          <RpRunouts ids={ids} />
        </details>
      )}
    </li>
  );
}
// Vinyl pressings shown before "Show all": the oldest ten, plus the first pressing
// and every one on a shelf here, wherever they fall in the list.
const RP_PRESS_SHOWN = 10;
const rpYear = (x) => { const m = /^(\d{4})/.exec(String(x || "")); return m && Number(m[1]) > 1890 ? Number(m[1]) : null; };
const rpReduced = () => !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);

// The dated vinyl pressings between y1 and y2 (inclusive) that the kept list does
// not hold, from Discogs' count per year: where the oldest hundred meet the newest
// fifty. Undated pressings are not between anything (The Joshua Tree: 16 of them,
// and none at all pressed from 1993 to 2006). Without the counts, all unlisted.
function rpBetween(years, versions, y1, y2, total) {
  if (!years || !y1 || !y2) return Math.max(0, (Number(total) || 0) - versions.length);
  const listed = {};
  versions.forEach((v) => { const y = rpYear(v.released); if (y) listed[y] = (listed[y] || 0) + 1; });
  let n = 0;
  for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) n += Math.max(0, (Number(years[y]) || 0) - (listed[y] || 0));
  return n;
}

// ---- the tracklist, side by side -------------------------------------------------
// Vinyl positions carry the side ("A1", "B3"): grouped by it, with each side's
// running time when Discogs has every track's. A CD-style list ("1", "2") is one
// group with no heading. Discogs' own headings start a group of their own.
function rpSecs(d) {
  const parts = String(d || "").split(":").map(Number);
  if (parts.length < 2 || parts.some((n) => !Number.isFinite(n))) return null;
  return parts.reduce((acc, n) => acc * 60 + n, 0);
}
function rpClock(t) {
  const h = Math.floor(t / 3600), m = Math.floor((t % 3600) / 60), sec = t % 60;
  return (h ? h + ":" + String(m).padStart(2, "0") : m) + ":" + String(sec).padStart(2, "0");
}
// The side a position names: "A1", "A1a", "B2.b" or a bare "A" (a one-track side),
// never a disc-and-track number ("CD1-1", "2-04").
const rpSideOf = (pos) => { const m = /^([A-Z])(\d+[a-z]?(\.\w+)?)?$/i.exec(String(pos || "").trim()); return m ? m[1].toUpperCase() : ""; };
function rpSides(list) {
  const tracks = (list || []);
  const letters = new Set(tracks.filter((t) => !t.heading).map((t) => rpSideOf(t.position)).filter(Boolean));
  const bySide = letters.size >= 2;
  const out = [];
  let cur = null;
  // A track with no position of its own (a suite on a page built before builder 4)
  // goes with the side it sits on; at the very top, with the first side to come.
  let waiting = [];
  tracks.forEach((t) => {
    if (t.heading) { cur = { name: t.heading, tracks: [], fromHeading: true }; out.push(cur); return; }
    const L = bySide ? rpSideOf(t.position) : "";
    if (!cur && bySide && !L) { waiting.push(t); return; }
    if (!cur || (bySide && L && cur.letter !== L && !cur.fromHeading)) { cur = { name: L ? "Side " + L : "", letter: L, tracks: waiting }; waiting = []; out.push(cur); }
    cur.tracks.push(t);
  });
  if (waiting.length) out.push({ name: "", letter: "", tracks: waiting });
  return out.filter((g) => g.tracks.length).map((g) => {
    const secs = g.tracks.map((t) => rpSecs(t.duration));
    return Object.assign(g, { time: secs.every((x) => x != null) && secs.length ? rpClock(secs.reduce((a, b) => a + b, 0)) : null });
  });
}

// ---- at a glance ------------------------------------------------------------------
// Four figures under the intro, counted up as the cheer's figure is (App.jsx
// RacCountUp, which lands at once under reduced motion or in a hidden tab).
function RpStats({ items }) {
  const CountUp = window.RacCountUp;
  const shown = items.filter(Boolean);
  if (!shown.length) return null;
  return (
    <dl className="rp-stats">
      {shown.map((x, i) => (
        <div className="rp-stat" key={x.k} style={{ animationDelay: (80 + i * 70) + "ms" }}>
          <dt>{x.k}</dt>
          <dd>{x.n != null ? (CountUp ? <CountUp value={x.n} /> : Number(x.n).toLocaleString("en-GB")) : x.v}</dd>
        </div>
      ))}
    </dl>
  );
}

// ---- the pressings on a timeline --------------------------------------------------
// Every vinyl pressing as a dot on its year: the first pressing in the accent,
// yours ringed in green, ones on shelves here filled. Pointing at a dot says what
// it is; choosing one finds its row in the list below (onPick). `years` is
// Discogs' count per year for every vinyl pressing (record_pages.vinyl_years), so
// a year's pressings that are not in the list kept here still appear, as faint
// dots: the whole history, where the list holds the oldest 100 and newest 50.
const RP_TL_STACK = 6;
function RpTimeline({ versions, years, firstId, mineIds, onPick, narrow }) {
  const [at, setAt] = useRpState(null);
  const pts = (versions || []).map((v) => ({ v, y: rpYear(v.released) })).filter((x) => x.y);
  const counts = {};
  Object.keys(years || {}).forEach((k) => { const y = Number(k), n = Number(years[k]); if (y > 1890 && n > 0) counts[y] = n; });
  pts.forEach((x) => { counts[x.y] = Math.max(counts[x.y] || 0, pts.filter((q) => q.y === x.y).length); });
  const allYears = Object.keys(counts).map(Number);
  // A record pressed in one or two years has no history to draw (Human Racing:
  // 46 vinyl pressings, all 1984). The list says so in a line instead.
  if (allYears.length < 3) return null;
  const lo = Math.min(...allYears), hi = Math.max(...allYears);
  const span = Math.max(1, hi - lo);
  const pct = (y) => (hi === lo ? 50 : ((y - lo) / span) * 100);
  const byYear = new Map();
  allYears.forEach((y) => byYear.set(y, []));
  pts.forEach((x) => byYear.get(x.y).push(x.v));
  let ghostsAny = false;
  // Within a year, the ones that matter sit at the bottom of the stack, where the eye lands.
  const weight = (v) => (Number(v.release_id) === Number(firstId) ? 0 : mineIds.has(Number(v.release_id)) ? 1 : Number(v.owners) > 0 ? 2 : 3);
  const step = span > 40 ? 10 : span > 15 ? 5 : span > 6 ? 2 : 1;
  const ticks = [];
  // A tick too near an end year collided with it on a phone ("19751980"): kept
  // clear by a share of the width, more of it on a narrow screen.
  const clear = narrow ? 16 : 8;
  for (let t = Math.ceil(lo / step) * step; t <= hi; t += step) if (pct(t) > clear && pct(t) < 100 - clear) ticks.push(t);
  const say = (v) => [[rpLabel(v.label), v.catno].filter(Boolean).join(" "), v.country, rpYear(v.released), v.format].filter(Boolean).join(" · ");
  // How many were pressed that year, said with the pressing pointed at: on a long
  // history the "+N" over each crowded year collided (Revolver: 56 years, 154 in
  // 1966), so there it is left to the caption.
  const ofYear = (v) => { const y = rpYear(v.released), c = y ? counts[y] || 0 : 0; return c > 1 ? " — one of " + c + " vinyl pressings from " + y : ""; };
  const roomy = span <= 25;
  // Years closer together than a dot is wide (a phone, half a century): smaller dots.
  const dense = narrow && span > 30;
  const touch = !!(window.matchMedia && window.matchMedia("(pointer: coarse)").matches);
  let n = 0;
  return (
    <div className="rp-tlx">
      <div className={"rp-tlx-plot" + (dense ? " is-dense" : "")}>
        {[...byYear.entries()].sort((a, b) => a[0] - b[0]).map(([y, list]) => {
          const sorted = list.slice().sort((a, b) => weight(a) - weight(b));
          const total = Math.max(counts[y] || 0, sorted.length);
          const shownDots = sorted.slice(0, RP_TL_STACK);
          const ghosts = Math.min(total - sorted.length, RP_TL_STACK - shownDots.length);
          if (total > sorted.length) ghostsAny = true;
          const hidden = total - shownDots.length - Math.max(0, ghosts);
          return (
            <div className="rp-tlx-col" key={y} style={{ left: pct(y) + "%" }}>
              {hidden > 0 && roomy && <span className="rp-tlx-more" aria-hidden="true">+{hidden}</span>}
              {Array.from({ length: Math.max(0, ghosts) }).map((_, i) => <span key={"g" + i} className="rp-tlx-dot is-ghost" aria-hidden="true" />)}
              {shownDots.slice().reverse().map((v) => {
                const id = Number(v.release_id);
                const cls = id === Number(firstId) ? " is-first" : mineIds.has(id) ? " is-yours" : Number(v.owners) > 0 ? " is-held" : "";
                return (
                  <button key={id} type="button" className={"rp-tlx-dot" + cls} style={{ animationDelay: Math.min(600, (n++) * 12) + "ms" }}
                    aria-label={say(v) + (cls ? " (" + cls.trim().replace("is-first", "first pressing").replace("is-yours", "yours").replace("is-held", "on shelves here") + ")" : "")}
                    onMouseEnter={() => setAt(v)} onFocus={() => setAt(v)} onMouseLeave={() => setAt(null)}
                    onClick={() => onPick(id)} />
                );
              })}
            </div>
          );
        })}
      </div>
      <div className="rp-tlx-axis" aria-hidden="true">
        <span className="rp-tlx-end" style={{ left: "0%" }}>{lo}</span>
        {ticks.map((t) => <span key={t} style={{ left: pct(t) + "%" }}>{t}</span>)}
        {hi !== lo && <span className="rp-tlx-end is-hi" style={{ left: "100%" }}>{hi}</span>}
      </div>
      <p className="rp-tlx-cap" aria-live="polite">{at ? say(at) + ofYear(at) : touch ? "Tap a pressing to find it in the list." : "Point at a pressing to see it, or choose one to find it in the list."}</p>
      <ul className="rp-tlx-key" aria-hidden="true">
        <li><i className="rp-tlx-dot is-first" />First pressing</li>
        {mineIds.size > 0 && <li><i className="rp-tlx-dot is-yours" />Yours</li>}
        <li><i className="rp-tlx-dot is-held" />On shelves here</li>
        <li><i className="rp-tlx-dot" />Other pressings</li>
        {ghostsAny && <li><i className="rp-tlx-dot is-ghost" />Not in the list below</li>}
      </ul>
    </div>
  );
}
const rpGbp = (n) => "£" + Math.round(n).toLocaleString("en-GB");
const rpRid = (it) => Number(it && (it.discogs_release_id || it.resolved_release_id)) || null;

// The back link names where the page was opened from (App.jsx recordFrom, set by
// goRecord). A cold landing has nowhere of ours behind it: a collector's shelf, or
// the front page for anyone else.
function rpBackLabel(from, signedIn) {
  if (!from) return signedIn ? "Your collection" : "The Lead-In";
  const shelf = from.name ? from.name + "’s shelf"
    : from.handle && !/^anonymous-/.test(from.handle) ? from.handle + "’s shelf" : "Their shelf";
  const dive = from.route === "deep-dive" && from.slug && window.bbrDiveFor ? window.bbrDiveFor(from.slug) : null;
  return ({
    collection: "Your collection", "public-collection": shelf, community: "Community",
    "community-collectors": "Collectors", leaderboards: "Leaderboards", "deep-dives": "Deep dives",
    "deep-dive": dive ? "The " + dive.title + " deep dive" : "The deep dive", home: "Home", today: "Today",
  })[from.route] || "Back";
}

// While a collector's own record is being built (lib/record_page.js, about ten
// seconds the first time): what is being done, step by step, so the wait reads as
// work rather than as a page that has failed (Ric, 17 Oct 2026). The steps are the
// builder's own, timed to its usual pace; the last one holds until the page comes.
const RP_BUILD_STEPS = [
  ["Finding the album and when it first came out", 1400],
  ["Listing every vinyl pressing of it", 3600],
  ["Picking out the first pressing", 5200],
  ["Reading the tracklist and the credits", 6800],
  ["Looking for a write-up about it", 9200],
  ["Counting who else owns it", Infinity],
];
function RpBuilding({ title }) {
  const [t, setT] = useRpState(0);
  useRpEffect(() => {
    const t0 = Date.now();
    const iv = setInterval(() => setT(Date.now() - t0), 250);
    return () => clearInterval(iv);
  }, []);
  const at = RP_BUILD_STEPS.findIndex(([, until]) => t < until);
  const pct = Math.min(94, Math.round(100 * (1 - Math.exp(-t / 7000))));
  return (
    <div className="rp-build" aria-busy="true">
      <div className="rp-build-top">
        <span className="rp-build-disc" aria-hidden="true"><i /></span>
        <div>
          <p className="rp-build-h">Putting {title ? <i>{title}</i> : "this record"}’s page together</p>
          <p className="rp-build-sub">It takes about ten seconds the first time. Stay here: it opens by itself when it’s ready.</p>
        </div>
      </div>
      <div className="rp-build-bar" role="progressbar" aria-label="Building the page" aria-valuemin={0} aria-valuemax={100} aria-valuenow={pct}>
        <span style={{ width: pct + "%" }} />
      </div>
      <ol className="rp-build-steps">
        {RP_BUILD_STEPS.map(([label], i) => (
          <li key={i} className={i < at ? "is-done" : i === at ? "is-now" : ""}>
            <span className="rp-build-mark" aria-hidden="true">{i < at ? "✓" : ""}</span>
            {label}{i < at ? <span className="rp-vh"> (done)</span> : null}
          </li>
        ))}
      </ol>
      <p className="rp-vh" role="status">{at >= 0 ? RP_BUILD_STEPS[at][0] : ""}</p>
      {t > 18000 && (
        <p className="rp-build-late">{t > 45000
          ? "This one is taking a while. It keeps going if you leave, and will be here when you come back."
          : "Still going: a record with hundreds of pressings takes a little longer."}</p>
      )}
    </div>
  );
}

function RecordPage({ pageKey, user, onGate, from, onBack, authReady }) {
  const store = window.BBR_store;
  const [, force] = useRpState(0);
  useRpEffect(() => (store && store.subscribe ? store.subscribe(() => force((n) => n + 1)) : undefined), []);
  const [d, setD] = useRpState(null);          // record_page() result
  const [state, setState] = useRpState("loading"); // loading | ready | building | missing | error
  const [tries, setTries] = useRpState(0);
  const [cover, setCover] = useRpState(null);
  const [allPress, setAllPress] = useRpState(false);
  const [adding, setAdding] = useRpState(false);   // the add sheet, opened by "Add your copy"
  const [hl, setHl] = useRpState(null);            // the pressing a timeline dot just pointed to
  const [toast, setToast] = useRpState("");
  const [reload, setReload] = useRpState(0);       // re-read in place, after an add from this page

  const mineAll = (store && store.collectionV2) || [];
  const loaded = !!(store && store.collectionV2Loaded);
  // The viewer's copy this key names, when it is an 'r' key: what the page is
  // built from if it does not exist yet, and what it shows while it is built.
  const keyRid = /^r(\d{1,12})$/.exec(pageKey || "") ? Number(pageKey.slice(1)) : null;
  const ownForKey = keyRid ? mineAll.find((it) => rpRid(it) === keyRid) : null;
  const signedIn = !!(user || (store && store.userId));
  const authOk = authReady !== false;
  // The read this page is showing. Once it is shown, the collection or the session
  // arriving later is no reason to read it again (it flashed back to the skeleton
  // for an owner landing on their own page).
  const shownFor = React.useRef(null);

  useRpEffect(() => {
    if (!pageKey || !window.BBR_supabase) return undefined;
    if (shownFor.current === pageKey + ":" + tries) return undefined;
    let live = true;
    setState("loading");
    (async () => {
      const read = async (k) => {
        const out = await window.BBR_supabase.rpc("record_page", { p_key: k });
        if (out && out.error) throw out.error;
        return (out && out.data) || null;
      };
      let page = await read(pageKey);
      if (!live) return;
      if (page) { shownFor.current = pageKey + ":" + tries; setD(page); setState("ready"); return; }
      // Not built. Ours to build only once we know whose shelf this is: before the
      // session and the collection are in, "no page yet" may be wrong.
      if (!ownForKey) { setState(!authOk || (signedIn && !loaded) ? "loading" : "missing"); return; }
      setState("building");
      const key = await store.claimRecordPage(ownForKey);
      if (!live) return;
      page = key ? await read(key) : null;
      if (!live) return;
      if (page) { shownFor.current = pageKey + ":" + tries; setD(page); setState("ready"); } else setState("error");
    })().catch(() => { if (live) setState("error"); });
    return () => { live = false; };
  }, [pageKey, tries, !!ownForKey, loaded, authOk, signedIn]);

  // After "Add your copy": read the page again in place once the new copy's
  // pressing has been linked to it (store.claimRecordPage), with no loading state.
  useRpEffect(() => {
    if (!reload || !d || !window.BBR_supabase) return undefined;
    let live = true;
    Promise.resolve(window.BBR_supabase.rpc("record_page", { p_key: d.key })).then((out) => {
      if (live && out && !out.error && out.data) setD(out.data);
    }, () => {});
    return () => { live = false; };
  }, [reload]);
  useRpEffect(() => { if (!toast) return undefined; const t = setTimeout(() => setToast(""), 2400); return () => clearTimeout(t); }, [toast]);

  // Not the last record's title on a page that has none of its own; while an
  // owner's page is built, their copy's.
  useRpEffect(() => {
    if (state === "ready") return;
    if (ownForKey && ownForKey.title) document.title = ownForKey.title + (ownForKey.artist && !rpVarious(ownForKey.artist) ? " by " + ownForKey.artist : "") + " | The Lead-In";
    else if (state === "missing" || state === "error") document.title = "Record page | The Lead-In";
  }, [state, ownForKey && ownForKey.id]);
  const p = d && d.page;
  useRpEffect(() => {
    if (!p) return undefined;
    document.title = p.title + (rpVarious(p.artist) ? "" : " by " + p.artist) + " | The Lead-In";
    const meta = (sel, attr, v) => { const el = document.head.querySelector(sel); if (el) el.setAttribute(attr, v); };
    const url = "https://www.theleadin.com/record/" + p.key;
    meta('link[rel="canonical"]', "href", url);
    meta('meta[property="og:url"]', "content", url);
    meta('meta[name="description"]', "content", rpIntro(p));
    let live = true;
    const ask = window.lbFetchCover;
    if (ask) ask({ artist: p.artist, title: p.title, year: rpYearOf(p), discogs_release_id: p.main_release_id })
      .then((u) => { if (live) setCover(u || null); });
    return () => { live = false; };
  }, [p && p.key]);

  const releases = (d && d.releases) || [];
  const versions = (d && d.versions) || [];
  // Every pressing that belongs to the page: fetched in full, or in Discogs' vinyl list.
  const relIds = useRpMemo(() => new Set(releases.map((r) => Number(r.release_id)).concat(versions.map((v) => Number(v.release_id)))), [d]);
  const relById = useRpMemo(() => new Map(releases.map((r) => [Number(r.release_id), r])), [d]);
  const mine = mineAll.filter((it) => relIds.has(rpRid(it)));
  // The weekly dives arrive after the page can (bbrLoadDives): their count is in
  // the deps, so a cold landing still finds this record's dive and the artist's.
  const divesN = window.bbrDivesNow ? window.bbrDivesNow().length : 0;
  const dive = useRpMemo(() => {
    if (!p || !window.bbrMatchDive || !window.bbrDivesNow) return null;
    const slug = window.bbrMatchDive(window.bbrDivesNow(), p.artist, p.title);
    return slug && window.bbrDiveFor ? window.bbrDiveFor(slug) : null;
  }, [p && p.key, divesN]);
  // The artist's other records with a deep dive, so the page leads somewhere.
  const artistDives = useRpMemo(() => {
    if (!p || rpVarious(p.artist) || !window.bbrDivesNow || !window.bbrDiveMatchKey) return [];
    const a = window.bbrDiveMatchKey(p.artist);
    return window.bbrDivesNow().filter((x) => x && (!dive || x.slug !== dive.slug) && window.bbrDiveMatchKey(x.artist) === a).slice(0, 3);
  }, [p && p.key, dive && dive.slug, divesN]);

  // The sleeve draws itself to a size, so the size is chosen here, not in CSS.
  const narrow = !!(window.matchMedia && window.matchMedia("(max-width: 720px)").matches);
  const nav = window.__nav || {};
  const goDive = (slug) => (e) => {
    if (!nav.goDeepDive || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
    e.preventDefault(); nav.goDeepDive(slug);
  };
  const goShelf = (e) => {
    if (!nav.goCollection || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
    e.preventDefault(); nav.goCollection();
  };
  const back = onBack ? <button type="button" className="lb-back rp-back" onClick={onBack}>&larr; {rpBackLabel(from, signedIn)}</button> : null;

  if (state === "loading" && !ownForKey) {
    // The page's own shape, drawn while it is read: no jump when it arrives.
    return (
      <div className="rp rp-skel" aria-busy="true">
        {back}
        <p className="rp-vh" role="status">Loading the record</p>
        <div className="rp-hero">
          <div className="rp-cover"><div className="rp-sk rp-sk-cover" style={{ width: narrow ? 180 : 240, height: narrow ? 180 : 240 }} /></div>
          <div className="rp-head">
            <div className="rp-sk rp-sk-line" style={{ width: 120 }} />
            <div className="rp-sk rp-sk-title" />
            <div className="rp-sk rp-sk-line" style={{ width: 160 }} />
            <div className="rp-sk rp-sk-para" />
            <div className="rp-sk rp-sk-stats" />
          </div>
        </div>
      </div>
    );
  }
  if (state !== "ready" || !p) {
    const it = ownForKey;
    const building = state === "building" || (state === "loading" && it);
    return (
      <div className="rp rp-wait">
        {back}
        {it && (
          <div className="rp-hero">
            <div className="rp-cover">{window.Sleeve && <Sleeve album={{ artist: it.artist, title: it.title, year: it.year, rank: -1 }} size={narrow ? 160 : 200} />}</div>
            <div className="rp-head">
              <h1 className="rp-title">{it.title}</h1>
              <div className="rp-artist">{it.artist}</div>
            </div>
          </div>
        )}
        {building ? <RpBuilding title={it && it.title} /> : (
          <p className="rp-status" role="status">
            {state === "loading" ? "Loading…"
              : state === "missing" ? "We haven’t put a page together for this record yet."
              : <>We couldn’t put this page together just now. <button type="button" className="rp-link" onClick={() => setTries((n) => n + 1)}>Try again</button></>}
          </p>
        )}
      </div>
    );
  }

  const first = releases.find((r) => Number(r.release_id) === Number(p.main_release_id));
  const kicker = [p.kind, rpYearOf(p)].filter(Boolean).join(" · ");
  const firstV = versions.find((v) => Number(v.release_id) === Number(p.first_vinyl_release_id));
  const firstYear = (firstV && rpYear(firstV.released)) || rpYearOf(p);
  const mineIds = new Set(mine.map((it) => rpRid(it)));
  const nm = releases.concat(versions).map((r) => Number(r.ref_nm)).filter((n) => n > 0);
  const nmLo = nm.length ? Math.min(...nm) : 0, nmHi = nm.length ? Math.max(...nm) : 0;
  const pageUrl = "https://www.theleadin.com/record/" + p.key;
  const track = (name, props) => { try { if (window.BBR_track) window.BBR_track(name, Object.assign({ key: p.key }, props || {})); } catch (e) {} };
  // A timeline dot: find its row, opening the full list when it is past the first
  // ten, and mark it for a moment.
  const pick = (id) => {
    setAllPress(true);
    setHl(id);
    setTimeout(() => {
      const el = document.getElementById("rp-pr-" + id);
      if (el) el.scrollIntoView({ block: "center", behavior: rpReduced() ? "auto" : "smooth" });
    }, 40);
    setTimeout(() => setHl((x) => (x === id ? null : x)), 2600);
    track("record_timeline_pick", {});
  };
  const addCopy = () => {
    track("record_add_open", { owned: mine.length > 0 });
    // Signed in by the app's session or the store's (they are set together).
    if (!user && !(store && store.userId)) { if (onGate) onGate("collection"); return; }
    setAdding(true);
  };
  const share = async () => {
    track("record_share", {});
    const title = p.title + (rpVarious(p.artist) ? "" : " by " + p.artist);
    try {
      if (navigator.share) { await navigator.share({ title, text: rpIntro(p), url: pageUrl }); return; }
      await navigator.clipboard.writeText(pageUrl);
      setToast("Link copied");
    } catch (e) {
      if (e && e.name === "AbortError") return;   // the share sheet was dismissed
      setToast("Couldn’t copy the link");
    }
  };
  return (
    <div className="rp">
      {back}
      <div className="rp-hero">
        <div className="rp-cover">{window.Sleeve && <Sleeve album={{ artist: p.artist, title: p.title, year: rpYearOf(p), rank: -1, coverUrl: cover || undefined }} size={narrow ? 180 : 240} />}</div>
        <div className="rp-head">
          {kicker && <div className="rp-kicker">{kicker}</div>}
          <h1 className="rp-title">{p.title}</h1>
          <div className="rp-artist">{p.artist}</div>
          <p className="rp-intro">{rpIntro(p)}</p>
          <RpStats items={[
            firstYear && { k: "First pressed", v: String(firstYear) },
            Number(p.vinyl_versions) > 0 && { k: "Vinyl pressings", n: Number(p.vinyl_versions) },
            Number(d.owners) > 0 && { k: "Collectors", n: Number(d.owners) },
            nm.length > 0 && { k: "Near Mint", v: nmLo === nmHi ? rpGbp(nmLo) : rpGbp(nmLo) + "–" + rpGbp(nmHi) },
          ]} />
          <div className="rp-ctas">
            <button type="button" className="rp-btn rp-btn-go" onClick={addCopy}>{mine.length ? "Add another copy" : "Add your copy"}</button>
            <button type="button" className="rp-btn" onClick={share}>Share</button>
            {toast && <span className="rp-toast" role="status">{toast}</span>}
          </div>
          {mine.length > 0 && (
            <p className="rp-mine">
              <span className="rp-mine-tick" aria-hidden="true">✓</span>
              {mine.length === 1 ? "On your shelf" : mine.length + " copies on your shelf"}
              {" · "}<a href="/collection" onClick={goShelf}>Open your shelf</a>
            </p>
          )}
          {window.DiveOwners && (
            <DiveOwners title={p.title} user={user} owned={mine.length > 0}
              source={{ rpc: "record_page_owners", args: { p_key: p.key }, key: p.key }} />
          )}
        </div>
      </div>

      {dive && (
        <a className="rp-dive" href={"/deep-dive/" + encodeURIComponent(dive.slug)} onClick={goDive(dive.slug)}>
          <span className="rp-dive-k">The Lead-In deep dive</span>
          <span className="rp-dive-t">There’s a deep dive on this record. Read it &rarr;</span>
        </a>
      )}

      {p.wiki_extract && (
        <section className="rp-sec rp-about" aria-labelledby="rp-about-h">
          <h2 id="rp-about-h">About this record</h2>
          <p className="rp-extract">{p.wiki_extract}</p>
          {/* CC BY-SA 4.0 asks for the source, a link to it, and the licence, every
              time; a link to the article is the credit its authors ask for. The site
              itself is not named on the page (Ric, 17 Oct 2026). */}
          <p className="rp-attrib">
            Text from <a href={p.wiki_url} target="_blank" rel="noopener">its encyclopedia entry</a>,
            {" "}under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener license">CC BY-SA 4.0</a>.
          </p>
        </section>
      )}

      {mine.length > 0 && (
        <section className="rp-sec rp-yours" aria-labelledby="rp-yours-h">
          <h2 id="rp-yours-h">{mine.length === 1 ? "Your copy" : "Your copies"}</h2>
          {mine.map((it) => {
            const r = relById.get(rpRid(it)) || rpFromVersion(versions.find((v) => Number(v.release_id) === rpRid(it))) || {};
            const ids = rpRunouts(r);
            const made = (r.companies || []).filter((c) => /pressed by|manufactured by|made by|mastered at|lacquer cut at/i.test(c.role || "")).slice(0, 4);
            const notes = rpPlain(r.notes);
            const tags = rpFormatTags(r);
            // Where this copy sits in the record's history, in a line.
            const cy = rpYear(r.released);
            const isFirstCopy = rpRid(it) === Number(p.first_vinyl_release_id);
            const ctx = isFirstCopy ? "You have the first vinyl pressing."
              : cy && firstYear && cy > firstYear ? "Yours is a " + cy + " pressing, " + (cy - firstYear) + " year" + (cy - firstYear === 1 ? "" : "s") + " after the first."
              : cy && firstYear && cy === firstYear ? "Yours is from " + cy + ", the year of the first pressing."
              : null;
            return (
              <div className={"rp-copy" + (isFirstCopy ? " is-first" : "")} key={it.id}>
                {ctx && <p className={"rp-copy-ctx" + (isFirstCopy ? " is-first" : "")}>{ctx}</p>}
                {/* The same shape for every copy: what it is on the left, what it is
                    worth on the right (under it on a phone), then the details. */}
                <div className="rp-copy-top">
                  <div className="rp-copy-id">
                    <div className="rp-copy-line">{rpPressingName(r) || it.title || "Pressing details to come"}</div>
                    {rpPressingWhere(r) && <div className="rp-copy-where">{rpPressingWhere(r)}</div>}
                    {tags.length > 0 && <ul className="rp-copy-tags" aria-label="Format">{tags.map((t) => <li key={t}>{t}</li>)}</ul>}
                  </div>
                  <div className="rp-copy-val">
                    <div className="rp-copy-price">
                      {typeof it.est_value === "number" && it.est_value > 0 ? rpGbp(it.est_value) : <span className="rp-dim">Valuation loading</span>}
                      {r.tier && window.TierChip && <TierChip tier={r.tier} />}
                    </div>
                    {(it.media_condition || it.sleeve_condition) && (
                      <div className="rp-copy-grade">
                        {it.media_condition && <span>Record <b>{rpGrade(it.media_condition)}</b></span>}
                        {it.media_condition && it.sleeve_condition && " · "}
                        {it.sleeve_condition && <span>Sleeve <b>{rpGrade(it.sleeve_condition)}</b></span>}
                      </div>
                    )}
                  </div>
                </div>
                {ids.length > 0 && (
                  <div className="rp-copy-part">
                    <h3 className="rp-copy-h">Runout etchings</h3>
                    <RpRunouts ids={ids} />
                  </div>
                )}
                {made.length > 0 && (
                  <div className="rp-copy-part">
                    <h3 className="rp-copy-h">Made by</h3>
                    <ul className="rp-made">
                      {made.map((c, i) => <li key={i}><span>{String(c.role || "").charAt(0).toUpperCase() + String(c.role || "").slice(1).toLowerCase()}</span> {c.name}</li>)}
                    </ul>
                  </div>
                )}
                {notes && (
                  <details className="rp-notes">
                    <summary>Notes on this pressing</summary>
                    <p>{notes}</p>
                  </details>
                )}
              </div>
            );
          })}
        </section>
      )}

      <section className="rp-sec rp-press" aria-labelledby="rp-press-h">
        <h2 id="rp-press-h">Pressings</h2>
        {(() => {
          // The first real vinyl pressing (lib/record_page.js firstVinyl), else
          // the master's main release, which is usually the first release but may
          // be a CD: so "main catalogue entry", never "first pressing".
          const firstV = versions.find((v) => Number(v.release_id) === Number(p.first_vinyl_release_id));
          const when = (x) => (x ? rpWhen(x, null).replace(/^(on|in) /, "") : "");
          if (firstV) {
            const where = [firstV.country, when(firstV.released)].filter(Boolean).join(", ");
            return (
              <p className="rp-press-first">
                The first known vinyl pressing is {[rpLabel(firstV.label), firstV.catno].filter(Boolean).join(" ")}
                {where ? " (" + where + ")" : ""}{firstV.format ? ", " + firstV.format : ""}.
              </p>
            );
          }
          if (!p.label && !p.catno) return null;
          const where = [p.country, when(p.released)].filter(Boolean).join(", ");
          return (
            <p className="rp-press-first">
              Its main catalogue entry is {[rpLabel(p.label), p.catno].filter(Boolean).join(" ")}
              {where ? " (" + where + ")" : ""}{rpFormat((first && first.formats) || []) ? ", " + rpFormat(first.formats) : ""}.
            </p>
          );
        })()}
        {(() => {
          const years = [...new Set(versions.map((v) => rpYear(v.released)).filter(Boolean))];
          return years.length === 1 && versions.length > 1
            ? <p className="rp-press-one">Every known vinyl pressing is from {years[0]}.</p>
            : null;
        })()}
        <RpTimeline versions={versions} years={p.vinyl_years} firstId={p.first_vinyl_release_id} mineIds={mineIds} onPick={pick} narrow={narrow} />
        {versions.length > 0 && (() => {
          const isYours = (id) => mine.some((it) => rpRid(it) === Number(id));
          const isFirst = (id) => Number(id) === Number(p.first_vinyl_release_id);
          // The pressing a shared or shelf link named (/record/r<id>), marked for a
          // visitor too: it is the copy they followed.
          const isLinked = (id) => keyRid != null && Number(id) === keyRid && !isYours(id);
          // The first pressing heads the list: within its year the order is only
          // the catalogue's (Wish You Were Here opened with a French 1975 copy).
          const fi = versions.findIndex((v) => isFirst(v.release_id));
          const ordered = fi > 0 ? [versions[fi]].concat(versions.slice(0, fi), versions.slice(fi + 1)) : versions;
          const shown = allPress ? ordered : ordered.filter((v, i) => i < RP_PRESS_SHOWN || isFirst(v.release_id) || isYours(v.release_id) || isLinked(v.release_id) || Number(v.owners) > 0);
          const total = Number(p.vinyl_versions) || versions.length;
          return (
            <>
              <h3 className="rp-h3">Vinyl pressings, oldest first</h3>
              <ul className="rp-press-list">
                {shown.map((v, i) => {
                  // Where the oldest hundred meet the newest block: how many
                  // pressings sit between them, said in a row of its own.
                  const prev = shown[i - 1];
                  const gap = prev && Number(prev.sort) <= 1000 && Number(v.sort) > 1000;
                  const y1 = gap ? rpYear(prev.released) : null, y2 = gap ? rpYear(v.released) : null;
                  const between = gap ? rpBetween(p.vinyl_years, versions, y1, y2, total) : 0;
                  return (
                    <React.Fragment key={v.release_id}>
                      {gap && between > 0 && (
                        <li className="rp-press-gap">
                          {between.toLocaleString("en-GB")} more {between === 1 ? "pressing" : "pressings"}
                          {y1 && y2 && p.vinyl_years ? (y1 === y2 ? " from " + y1 : " from " + y1 + " to " + y2) : ""}
                          {", not listed here"}
                        </li>
                      )}
                      <RpPressRow r={v} full={relById.get(Number(v.release_id))} linked={isLinked(v.release_id)}
                        first={isFirst(v.release_id)} yours={isYours(v.release_id)} hl={hl === Number(v.release_id)} />
                    </React.Fragment>
                  );
                })}
              </ul>
              {shown.length < versions.length || allPress ? (
                <button type="button" className="rp-link rp-press-all" onClick={() => setAllPress((x) => !x)}>
                  {allPress ? "Show fewer" : "Show all " + versions.length}
                </button>
              ) : null}
              {total > versions.length && (() => {
                const newest = versions.filter((v) => Number(v.sort) > 1000).length;
                // Pressings catalogued with no year are in neither block (Wish You
                // Were Here: 167 of 493), so the note says how many.
                const dated = p.vinyl_years ? Object.values(p.vinyl_years).reduce((a, n) => a + (Number(n) || 0), 0) : 0;
                const undated = dated ? Math.max(0, total - dated - versions.filter((v) => !rpYear(v.released)).length) : 0;
                return (
                  <p className="rp-press-note">
                    {newest
                      ? "These are the oldest " + (versions.length - newest) + " and the newest " + newest + " of " + total.toLocaleString("en-GB")
                      : "These are the oldest " + versions.length + " of " + total.toLocaleString("en-GB")}
                    {undated > 0 ? "; " + undated.toLocaleString("en-GB") + " more have no year on record." : "."}
                  </p>
                );
              })()}
            </>
          );
        })()}
        {(() => {
          // Pressings on shelves here that the vinyl list does not hold: other
          // formats, or a vinyl pressing past its oldest hundred.
          const inList = new Set(versions.map((v) => Number(v.release_id)));
          // Only pressings someone here holds: a link outlives a deleted copy, and
          // our own accounts' copies are not counted.
          const others = releases.filter((r) => !inList.has(Number(r.release_id)) && (Number(r.owners) > 0 || mine.some((it) => rpRid(it) === Number(r.release_id))));
          if (!others.length) return null;
          return (
            <>
              <h3 className="rp-h3">{versions.length ? "Other pressings on shelves here" : "On collectors’ shelves here"}</h3>
              <ul className="rp-press-list">
                {others.map((r) => (
                  <RpPressRow key={r.release_id} r={r} full={r}
                    first={false} yours={mine.some((it) => rpRid(it) === Number(r.release_id))} />
                ))}
              </ul>
            </>
          );
        })()}
      </section>

      {(p.tracklist || []).length > 0 && (
        <section className="rp-sec rp-tracks" aria-labelledby="rp-tracks-h">
          <h2 id="rp-tracks-h">Tracklist</h2>
          <div className="rp-sides">
            {rpSides(p.tracklist).map((g, gi) => (
              <div className="rp-side" key={gi}>
                {g.name && (
                  <h3 className="rp-side-h">
                    {g.name}
                    <span className="rp-side-n"><span className="rp-vh">, </span>{g.tracks.length} {g.tracks.length === 1 ? "track" : "tracks"}{g.time ? " · " + g.time : ""}</span>
                  </h3>
                )}
                <ol className="rp-tl">
                  {g.tracks.map((t, i) => (
                    <li key={i}><span className="rp-tl-pos">{t.position}</span><span className="rp-tl-t">{t.title}</span><span className="rp-tl-d">{t.duration}</span></li>
                  ))}
                </ol>
              </div>
            ))}
          </div>
        </section>
      )}

      {(p.credits || []).length > 0 && (
        <section className="rp-sec rp-credits" aria-labelledby="rp-credits-h">
          <h2 id="rp-credits-h">Credits</h2>
          <dl className="rp-cr">
            {p.credits.map((c, i) => (
              <React.Fragment key={i}><dt>{c.role}</dt><dd>{c.name}</dd></React.Fragment>
            ))}
          </dl>
        </section>
      )}

      {artistDives.length > 0 && (
        <section className="rp-sec rp-more" aria-labelledby="rp-more-h">
          <h2 id="rp-more-h">More {p.artist} on The Lead-In</h2>
          <ul className="rp-more-list">
            {artistDives.map((x) => (
              <li key={x.slug}><a href={"/deep-dive/" + encodeURIComponent(x.slug)} onClick={goDive(x.slug)}>The deep dive on <i>{x.title}</i> &rarr;</a></li>
            ))}
          </ul>
        </section>
      )}

      {adding && window.MyCollectionAdd_v2 && (
        <MyCollectionAdd_v2 albums={window.BBR_ALBUMS || []} preset={{ artist: p.artist, title: p.title }} closeAfterSave
          onAdd={async (item, opts) => {
            const saved = await store.addCollectionItemV2(item, opts);
            // The store builds the new pressing's page link; read the page once it has.
            if (saved && store.claimRecordPage) store.claimRecordPage(saved).then(() => setReload((n) => n + 1));
            return saved;
          }}
          onClose={() => setAdding(false)} />
      )}

      <p className="rp-sources">
        Release facts from open music databases (CC0).
        {p.wiki_extract ? " About text CC BY-SA 4.0." : ""}
      </p>
    </div>
  );
}

// ---- links to a record's page, from wherever a record is shown -------------------
// One rule for every surface (the shelf card, the record's detail sheet, public
// shelves, the Community feed): the record's deep dive when it has one, else its
// record page. pageKeys ({ "<release id>": key }, from bbrRecordPageKeys) is for
// other collectors' records, which link only once their page exists. A
// collector's own records always link: their visit builds the page.
function rpLinkFor(it, pageKeys) {
  if (!it) return null;
  const dive = it.slug && window.bbrDiveFor ? window.bbrDiveFor(it.slug) : null;
  if (dive) return { kind: "dive", slug: dive.slug, href: "/deep-dive/" + encodeURIComponent(dive.slug), label: "Read the deep dive" };
  const rid = rpRid(it);
  if (!rid) return null;
  const key = pageKeys ? pageKeys[rid] : "r" + rid;
  if (!key) return null;
  return { kind: "record", key, href: "/record/" + key, label: "About this record" };
}
// Opens a link from rpLinkFor through the app's router; false when there is none.
function rpOpen(link) {
  const nav = window.__nav || {};
  if (link && link.kind === "dive" && nav.goDeepDive) { nav.goDeepDive(link.slug); return true; }
  if (link && link.kind === "record" && nav.goRecord) { nav.goRecord(link.key); return true; }
  return false;
}
// Which of these pressings have a page, one call per 500 (record_page_keys), the
// answers kept for the page load. A failed read is not kept, and offers nothing.
const RP_KEYS = new Map();
function rpPageKeys(rids) {
  const want = [...new Set((rids || []).map(Number).filter((n) => Number.isInteger(n) && n > 0))];
  const done = () => { const o = {}; want.forEach((r) => { const k = RP_KEYS.get(r); if (k) o[r] = k; }); return o; };
  const missing = want.filter((r) => !RP_KEYS.has(r));
  if (!missing.length || !window.BBR_supabase) return Promise.resolve(done());
  const chunks = [];
  for (let i = 0; i < missing.length; i += 500) chunks.push(missing.slice(i, i + 500));
  return Promise.all(chunks.map((c) => Promise.resolve(window.BBR_supabase.rpc("record_page_keys", { p_releases: c }))
    .then((out) => {
      const m = out && !out.error && out.data;
      if (m) c.forEach((r) => RP_KEYS.set(r, m[r] || null));
    }, () => {}))).then(done);
}

Object.assign(window, { RecordPage, bbrRecordIntro: rpIntro, bbrRecordLink: rpLinkFor, bbrOpenRecordLink: rpOpen, bbrRecordPageKeys: rpPageKeys });
