/* ============================================================
   EBTR — Article reader
   Progress bar · reading time · bookmark · author w/ role badge ·
   related · premium paywall (free preview → unlock).
   ============================================================ */
const { Stat: EBStat, Badge: EBBadgeR, Button: EBButtonR, ThreeBar: EBThreeBarR, Eyebrow: EBEyebrowR } = window.EBTHubDesignSystem_ccdc9d;

/* ---- Per-user watermark layer (faint, tiled, diagonal; non-interactive) ---- */
function ebtrWatermarkURI(text) {
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="340" height="210"><text x="0" y="120" transform="rotate(-28 0 120)" font-family="Michroma, sans-serif" font-size="14" letter-spacing="2" fill="rgba(255,255,255,0.05)">${text}</text></svg>`;
  return `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`;
}
function EBTRWatermark({ viewer }) {
  const label = `Licensed to ${viewer.name} · ${viewer.company}`;
  return <div aria-hidden="true" style={{ position: "absolute", inset: 0, pointerEvents: "none", zIndex: 2, backgroundImage: ebtrWatermarkURI(label), backgroundRepeat: "repeat" }} />;
}

/* ---- Watermarked PDF export (browser print-to-PDF; reader's name on every page) ---- */
function ebtrEscape(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
function ebtrBlockHTML(b) {
  if (b.t === "p") return `<p>${ebtrEscape(b.text)}</p>`;
  if (b.t === "h2") return `<h2>${ebtrEscape(b.text)}</h2>`;
  if (b.t === "quote") return `<blockquote>${ebtrEscape(b.text)}${b.cite ? `<cite>— ${ebtrEscape(b.cite)}</cite>` : ""}</blockquote>`;
  if (b.t === "stat") return `<div class="stat"><span class="sv">${ebtrEscape(b.value)}</span><span class="sl">${ebtrEscape(b.label)}${b.source ? `<em>${ebtrEscape(b.source)}</em>` : ""}</span></div>`;
  if (b.t === "list") return `<ul>${b.items.map((it) => `<li>${ebtrEscape(it)}</li>`).join("")}</ul>`;
  if (b.t === "callout") return `<div class="callout"><span class="cl">${ebtrEscape(b.label)}</span><p>${ebtrEscape(b.text)}</p></div>`;
  return "";
}
function ebtrExportPdf(article, viewer) {
  const data = window.EBTR_DATA;
  const cat = data.categories[article.category];
  const author = ebtrResolveAuthor(article);
  const stamp = `Licensed to ${viewer.name} · ${viewer.company}`;
  const date = new Date(article.date).toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" });
  const wmSvg = `<svg xmlns='http://www.w3.org/2000/svg' width='420' height='260'><text x='0' y='150' transform='rotate(-28 0 150)' font-family='Arial' font-size='15' letter-spacing='2' fill='rgba(20,20,20,0.06)'>${ebtrEscape(stamp)}</text></svg>`;
  const wm = `url("data:image/svg+xml;utf8,${encodeURIComponent(wmSvg)}")`;
  const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${ebtrEscape(article.title)} — EBTR</title>
<style>
  @page { margin: 22mm 18mm; }
  * { box-sizing: border-box; }
  html,body { margin:0; background:#fff; color:#1b1919; -webkit-print-color-adjust:exact; print-color-adjust:exact; }
  body { font-family: Georgia, 'Times New Roman', serif; line-height:1.6; background-image:${wm}; background-repeat:repeat; }
  .head { border-bottom:2px solid #1b1919; padding-bottom:14px; margin-bottom:24px; }
  .bars { display:flex; width:54px; height:7px; margin-bottom:14px; }
  .bars i { flex:1; } .bars i:nth-child(1){background:#FF8C03} .bars i:nth-child(2){background:#5D5DBF} .bars i:nth-child(3){background:#1D38DC}
  .brand { font-family:Arial,sans-serif; font-size:11px; letter-spacing:3px; text-transform:uppercase; color:#555; }
  .kicker { font-family:Arial,sans-serif; font-size:10px; letter-spacing:2px; text-transform:uppercase; color:#1D38DC; margin:18px 0 6px; }
  h1 { font-family:Arial,sans-serif; font-size:26px; line-height:1.18; margin:0 0 10px; }
  .dek { font-size:15px; color:#444; font-style:italic; margin:0 0 12px; }
  .by { font-family:Arial,sans-serif; font-size:11px; color:#666; letter-spacing:0.5px; }
  h2 { font-family:Arial,sans-serif; font-size:17px; margin:26px 0 8px; }
  p { font-size:13.5px; margin:0 0 13px; }
  blockquote { margin:18px 0; padding-left:16px; border-left:3px solid #FF8C03; font-size:17px; font-style:italic; }
  blockquote cite { display:block; font-family:Arial,sans-serif; font-size:10px; font-style:normal; letter-spacing:1.5px; text-transform:uppercase; color:#888; margin-top:8px; }
  .stat { display:flex; gap:16px; align-items:baseline; margin:18px 0; padding:14px 16px; background:#f4f3f2; border-left:3px solid #FF8C03; }
  .stat .sv { font-family:Arial,sans-serif; font-size:34px; font-weight:700; color:#b35e07; }
  .stat .sl { font-size:13px; } .stat .sl em { display:block; font-family:Arial,sans-serif; font-style:normal; font-size:9.5px; letter-spacing:1.5px; text-transform:uppercase; color:#888; margin-top:5px; }
  ul { padding-left:20px; } li { font-size:13.5px; margin:0 0 7px; }
  .callout { margin:18px 0; padding:14px 16px; border:1px solid #1D38DC; background:#eef1fe; }
  .callout .cl { font-family:Arial,sans-serif; font-size:9.5px; letter-spacing:2px; text-transform:uppercase; color:#1D38DC; display:block; margin-bottom:6px; }
  .lic { position:fixed; bottom:6mm; left:0; right:0; text-align:center; font-family:Arial,sans-serif; font-size:9px; letter-spacing:1px; color:#999; }
  .foot { margin-top:30px; padding-top:14px; border-top:1px solid #ccc; font-family:Arial,sans-serif; font-size:10px; color:#888; line-height:1.6; }
</style></head><body>
  <div class="head"><div class="bars"><i></i><i></i><i></i></div><div class="brand">EBTHub Review</div></div>
  <div class="kicker">${ebtrEscape(cat.label)}${article.premium ? " · Premium" : ""}</div>
  <h1>${ebtrEscape(article.title)}</h1>
  <div class="dek">${ebtrEscape(article.dek)}</div>
  <div class="by">${ebtrEscape(author.name)} · ${date} · ${article.readingMins} min read</div>
  <div style="margin-top:24px">${article.body.map(ebtrBlockHTML).join("")}</div>
  <div class="foot">© 2026 EBTHub — European Business Transformation Hub · Vienna, AT.<br>This copy is licensed to ${ebtrEscape(viewer.name)} (${ebtrEscape(viewer.company)}) for personal use. Redistribution is traceable to this account.</div>
  <div class="lic">${ebtrEscape(stamp)}</div>
  <scr` + `ipt>(function(){var isMobile=/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);if(isMobile){window.addEventListener('load',function(){setTimeout(function(){try{window.focus();window.print();}catch(e){}},600);});}})();</scr` + `ipt>
</body></html>`;
  var isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || "");
  if (isMobile) {
    // Mobile Safari/Chrome silently ignore print() from a hidden iframe. Open the
    // print-ready page as a real document (Blob URL) in a new tab; its embedded
    // script auto-triggers the native Print / Save-as-PDF sheet. The user gesture
    // (the button tap) keeps the tab from being popup-blocked.
    var blob = new Blob([html], { type: "text/html" });
    var burl = URL.createObjectURL(blob);
    var w = window.open(burl, "_blank");
    if (!w) {
      // popup blocked → navigate the click through an anchor as a fallback
      var aEl = document.createElement("a"); aEl.href = burl; aEl.target = "_blank"; aEl.rel = "noopener";
      document.body.appendChild(aEl); aEl.click(); aEl.remove();
    }
    setTimeout(function () { URL.revokeObjectURL(burl); }, 60000);
    return;
  }
  // Desktop: hidden iframe (no popup → not blocked), parent triggers print.
  var old = document.getElementById("ebtr-print-frame");
  if (old) old.remove();
  var f = document.createElement("iframe");
  f.id = "ebtr-print-frame";
  f.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;visibility:hidden;";
  document.body.appendChild(f);
  var d = f.contentWindow.document;
  d.open(); d.write(html); d.close();
  var go = function () {
    try { f.contentWindow.focus(); f.contentWindow.print(); }
    catch (e) { /* ignore */ }
  };
  // give the iframe a moment to lay out, then print
  if (f.contentWindow.document.readyState === "complete") setTimeout(go, 350);
  else f.onload = function () { setTimeout(go, 350); };
}

/* ---- Body block renderer ---- */
function EBTRBlock({ block }) {
  const b = block;
  const ebtrInline = function (text) {
    const s = String(text == null ? "" : text);
    if (s.indexOf("**") < 0) return s;
    return s.split(/(\*\*[^*]+\*\*)/g).map(function (p, i) {
      const m = /^\*\*([^*]+)\*\*$/.exec(p);
      return m ? React.createElement("strong", { key: i, style: { color: "var(--white)", fontWeight: 700 } }, m[1]) : p;
    });
  };
  if (b.t === "p") {
    const raw = String(b.text || "");
    if (!/\n|^\s*[-*•]\s|^\s*#{1,3}\s/.test(raw)) {
      return <p style={{ fontFamily: "var(--font-body)", fontSize: 19, lineHeight: 1.7, color: "var(--neutral-200)", margin: "0 0 26px", textWrap: "pretty" }}>{ebtrInline(raw)}</p>;
    }
    // Walk lines: ## → heading, - → bullet (following lines become its description), else → paragraph.
    const lns = raw.split(/\n/).map((l) => l.trim());
    const out = []; let para = [], cur = null;
    const flushPara = () => { if (para.length) { out.push({ k: "p", t: para.join(" ") }); para = []; } };
    const flushList = () => { if (cur) { out.push({ k: "ul", items: cur }); cur = null; } };
    lns.forEach((ln) => {
      if (!ln) { flushList(); flushPara(); return; }
      if (/^#{1,3}\s+/.test(ln)) { flushList(); flushPara(); out.push({ k: "h2", t: ln.replace(/^#{1,3}\s+/, "") }); }
      else if (/^[-*•]\s+/.test(ln)) { flushPara(); if (!cur) cur = []; cur.push({ head: ln.replace(/^[-*•]\s+/, ""), body: [] }); }
      else if (cur) { cur[cur.length - 1].body.push(ln); }
      else { para.push(ln); }
    });
    flushList(); flushPara();
    return (
      <React.Fragment>
        {out.map((seg, i) => {
          if (seg.k === "h2") return <h2 key={i} style={{ fontFamily: "var(--font-body)", fontWeight: 700, fontSize: 27, lineHeight: 1.25, color: "var(--white)", margin: "44px 0 20px", letterSpacing: "-0.01em" }}>{ebtrInline(seg.t)}</h2>;
          if (seg.k === "ul") return (
            <ul key={i} style={{ listStyle: "none", padding: 0, margin: "10px 0 30px", display: "flex", flexDirection: "column", gap: 18 }}>
              {seg.items.map((it, j) => (
                <li key={j} style={{ display: "grid", gridTemplateColumns: "24px 1fr", gap: 12 }}>
                  <span style={{ color: "var(--ebt-orange)", fontFamily: "var(--font-ui)", fontSize: 16, lineHeight: 1.5 }}>→</span>
                  <div>
                    <div style={{ fontFamily: "var(--font-body)", fontSize: 18, lineHeight: 1.55, color: "var(--neutral-100)" }}>{ebtrInline(it.head)}</div>
                    {it.body.map((bd, k) => <p key={k} style={{ fontFamily: "var(--font-body)", fontSize: 16.5, lineHeight: 1.65, color: "var(--text-secondary)", margin: "6px 0 0" }}>{ebtrInline(bd)}</p>)}
                  </div>
                </li>
              ))}
            </ul>
          );
          return <p key={i} style={{ fontFamily: "var(--font-body)", fontSize: 19, lineHeight: 1.7, color: "var(--neutral-200)", margin: "0 0 26px", textWrap: "pretty" }}>{ebtrInline(seg.t)}</p>;
        })}
      </React.Fragment>
    );
  }
  if (b.t === "h2") return <h2 style={{ fontFamily: "var(--font-body)", fontWeight: 700, fontSize: 27, lineHeight: 1.25, color: "var(--white)", margin: "44px 0 20px", letterSpacing: "-0.01em" }}>{b.text}</h2>;
  if (b.t === "quote") return (
    <blockquote style={{ margin: "38px 0", paddingLeft: 28, borderLeft: "3px solid var(--ebt-orange)" }}>
      <p style={{ fontFamily: "var(--font-body)", fontWeight: 600, fontSize: 27, lineHeight: 1.32, color: "var(--white)", margin: 0, textWrap: "balance" }}>{b.text}</p>
      {b.cite && <cite style={{ display: "block", marginTop: 14, fontFamily: "var(--font-ui)", fontSize: 11, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--text-muted)", fontStyle: "normal" }}>— {b.cite}</cite>}
    </blockquote>
  );
  if (b.t === "stat") {
    const col = b.color === "blue" ? "var(--ebt-blue)" : b.color === "purple" ? "var(--ebt-purple)" : "var(--ebt-orange)";
    return (
      <div style={{ margin: "38px 0", padding: "28px 32px", background: "var(--surface-raised)", borderLeft: `3px solid ${col}`, display: "flex", gap: 30, alignItems: "center", flexWrap: "wrap" }}>
        <span style={{ fontFamily: "var(--font-ui)", fontSize: "clamp(40px, 13vw, 60px)", lineHeight: 0.8, letterSpacing: "-0.01em", color: col, flex: "none", alignSelf: "center", display: "block", transform: "translateY(-0.04em)" }}>{b.value}</span>
        <div style={{ flex: 1, minWidth: 220 }}>
          <div style={{ fontFamily: "var(--font-body)", fontSize: 18, lineHeight: 1.45, color: "var(--neutral-100)" }}>{b.label}</div>
          {b.source && <div style={{ fontFamily: "var(--font-body)", fontSize: 13, fontStyle: "italic", letterSpacing: "0.01em", color: "var(--text-muted)", marginTop: 8 }}>{b.source}</div>}
        </div>
      </div>
    );
  }
  if (b.t === "list") return (
    <ul style={{ listStyle: "none", padding: 0, margin: "8px 0 30px", display: "flex", flexDirection: "column", gap: 16 }}>
      {b.items.map((it, i) => (
        <li key={i} style={{ display: "grid", gridTemplateColumns: "24px 1fr", gap: 12, fontFamily: "var(--font-body)", fontSize: 18, lineHeight: 1.6, color: "var(--neutral-200)" }}>
          <span style={{ color: "var(--ebt-orange)", fontFamily: "var(--font-ui)", fontSize: 16 }}>→</span>
          <span>{ebtrInline(it)}</span>
        </li>
      ))}
    </ul>
  );
  if (b.t === "callout") return (
    <div style={{ margin: "38px 0", padding: "26px 28px", background: "rgba(29,56,220,0.08)", border: "1px solid rgba(29,56,220,0.4)" }}>
      <div style={{ fontFamily: "var(--font-ui)", fontSize: 10.5, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--blue-400)", marginBottom: 12 }}>{b.label}</div>
      <p style={{ fontFamily: "var(--font-body)", fontSize: 18, lineHeight: 1.6, color: "var(--neutral-100)", margin: 0 }}>{b.text}</p>
    </div>
  );
  if (b.t === "figure") return (
    <figure style={{ margin: "38px 0" }}>
      <div style={{ height: 280, border: "1px solid var(--border-hairline)" }}><EBTRCover cover={b.cover} accent={b.accent || "blue"} big /></div>
      {b.caption && <figcaption style={{ fontFamily: "var(--font-ui)", fontSize: 11, letterSpacing: "0.08em", color: "var(--text-muted)", marginTop: 12, textTransform: "uppercase" }}>{b.caption}</figcaption>}
    </figure>
  );
  return null;
}

/* ---- Author block ---- */
function EBTRAuthorCard({ article }) {
  const a = ebtrResolveAuthor(article);
  if (!a || !a.name) return null;
  const isStefi = !!(article.column || article.category === "digital-employee");
  return (
    <div style={{ padding: "28px 0", borderTop: "1px solid var(--border-hairline)", borderBottom: "1px solid var(--border-hairline)", margin: "48px 0", display: "flex", gap: 20, alignItems: "flex-start" }} className="ebtr-author-card">
      {a.portrait && <img src={ebtrImageSrc(a.portrait)} alt="" style={{ width: 72, height: 72, borderRadius: "50%", objectFit: "cover", flex: "none" }} />}
      <div style={{ minWidth: 0 }}>
        <div style={{ fontFamily: "var(--font-ui)", fontSize: 10.5, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--text-muted)", marginBottom: 12 }}>About the author</div>
        <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
          <span style={{ fontFamily: "var(--font-body)", fontWeight: 700, fontSize: 20, color: "var(--white)" }}>{a.name}</span>
          {isStefi && <span style={{ fontFamily: "var(--font-ui)", fontSize: 9, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--neutral-1000)", background: "var(--ebt-purple)", padding: "3px 8px" }}>AI persona</span>}
          {(a.badges && a.badges.length ? a.badges : (a.badge ? [a.badge] : [])).filter((b) => b && b.label && b.label !== "— none").map((b, i) => <EBBadgeR key={i} variant={b.variant || "outline"} shape="square" size="sm">{b.label}</EBBadgeR>)}
        </div>
        {isStefi && <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, lineHeight: 1.55, color: "var(--ebt-purple)", margin: "10px 0 0" }}>Written by an AI persona, not a human — disclosed by design.</div>}
        {a.title && <div style={{ fontFamily: "var(--font-ui)", fontSize: 11.5, letterSpacing: "0.06em", color: "var(--text-muted)", margin: "8px 0 14px", textTransform: "uppercase" }}>{a.title}</div>}
        {a.bio && <p style={{ fontFamily: "var(--font-body)", fontSize: 16.5, lineHeight: 1.6, color: "var(--text-secondary)", margin: a.title ? 0 : "12px 0 0", maxWidth: 640 }}>{a.bio}</p>}
        {a.orcid && <a href={"https://orcid.org/" + a.orcid} target="_blank" rel="noopener" style={{ display: "inline-flex", alignItems: "center", gap: 7, marginTop: 14, fontFamily: "var(--font-ui)", fontSize: 11, letterSpacing: "0.04em", color: "var(--blue-400)", textDecoration: "none" }}><svg width="13" height="13" viewBox="0 0 24 24" fill="#A6CE39"><circle cx="12" cy="12" r="12"/><path fill="#fff" d="M7.5 8.7a.9.9 0 1 1 0-1.8.9.9 0 0 1 0 1.8zM6.8 10h1.4v7H6.8v-7zm3 0h3.3c2.6 0 3.8 1.7 3.8 3.5 0 1.9-1.5 3.5-3.8 3.5h-3.3v-7zm1.4 1.3v4.4h1.8c1.6 0 2.3-1.1 2.3-2.2 0-1.2-.8-2.2-2.3-2.2h-1.8z"/></svg>ORCID {a.orcid}</a>}
      </div>
    </div>
  );
}

/* ---- References (APA, 2–5) ---- */
// Normalize a DOI value to its bare form (10.xxxx/…) so we never double-prefix
// when an author/editor pastes a full https://doi.org/… URL into the field.
function ebtrDoiBare(d) {
  return String(d == null ? "" : d).trim()
    .replace(/^\s*doi:\s*/i, "")
    .replace(/^https?:\/\/(dx\.)?doi\.org\//i, "")
    .replace(/^doi\.org\//i, "");
}
function EBTRReferences({ refs, doi }) {
  if ((!refs || refs.length === 0) && !doi) return null;
  return (
    <div style={{ margin: "48px 0 0", paddingTop: 8 }}>
      {refs && refs.length > 0 && (
        <React.Fragment>
          <div style={{ fontFamily: "var(--font-ui)", fontSize: 10.5, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--text-muted)", marginBottom: 18 }}>References</div>
          <ol style={{ listStyle: "none", padding: 0, margin: 0, counterReset: "ref", display: "flex", flexDirection: "column", gap: 14 }}>
            {refs.map((r, i) => (
              <li key={i} style={{ display: "grid", gridTemplateColumns: "26px 1fr", gap: 10, fontFamily: "var(--font-body)", fontSize: 14.5, lineHeight: 1.55, color: "var(--text-secondary)" }}>
                <span style={{ fontFamily: "var(--font-ui)", fontSize: 12, color: "var(--text-muted)" }}>{i + 1}.</span>
                <span style={{ overflowWrap: "anywhere" }}>{r.authors} ({r.year}). <span style={{ fontStyle: "italic", color: "var(--neutral-100)" }}>{r.title}.</span> {r.source}.{r.doi && <React.Fragment> <a href={"https://doi.org/" + ebtrDoiBare(r.doi)} target="_blank" rel="noopener" style={{ color: "var(--blue-400)", textDecoration: "none" }}>https://doi.org/{ebtrDoiBare(r.doi)}</a></React.Fragment>}</span>
              </li>
            ))}
          </ol>
        </React.Fragment>
      )}
      {doi && (
        <div style={{ marginTop: refs && refs.length ? 26 : 0, paddingTop: 18, borderTop: "1px solid var(--border-hairline)", fontFamily: "var(--font-ui)", fontSize: 11, letterSpacing: "0.04em", color: "var(--text-muted)", overflowWrap: "anywhere" }}>
          <a href={"https://doi.org/" + ebtrDoiBare(doi)} target="_blank" rel="noopener" style={{ color: "var(--blue-400)", textDecoration: "none" }}>https://doi.org/{ebtrDoiBare(doi)}</a>
        </div>
      )}
    </div>
  );
}

/* ---- Premium wall: the piece describes a deployable instrument + the research
   behind it. The tool itself ships separately. Lab contributors and partner
   companies read it in full — access is tied to their account, not a paywall. ---- */
function EBTRPremiumWall({ article, loggedIn, onSignIn, onRequest }) {
  return (
    <div style={{ position: "relative", marginTop: -120 }}>
      <div style={{ height: 120, background: "linear-gradient(to bottom, transparent, var(--ebt-ink))", pointerEvents: "none" }} />
      <div style={{ border: "1px solid var(--border-hairline)", borderTop: "3px solid var(--ebt-orange)", background: "var(--surface-raised)", padding: "clamp(22px,6vw,40px) clamp(18px,5vw,40px) clamp(26px,7vw,44px)", textAlign: "center" }}>
        <div style={{ display: "flex", justifyContent: "center", marginBottom: 16 }}>
          <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--ebt-orange)" strokeWidth="2"><rect x="4" y="11" width="16" height="11" /><path d="M7 11V8a5 5 0 0 1 10 0v3" /></svg>
        </div>
        <div style={{ fontFamily: "var(--font-ui)", fontSize: 10.5, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ebt-orange)", marginBottom: 12 }}>Premium</div>
        <h3 style={{ fontFamily: "var(--font-body)", fontWeight: 700, fontSize: 26, color: "var(--white)", margin: "0 0 12px", textWrap: "balance" }}>The instrument behind the piece</h3>
        <p style={{ fontFamily: "var(--font-body)", fontSize: 17, lineHeight: 1.6, color: "var(--text-secondary)", maxWidth: 520, margin: "0 auto 28px" }}>
          This paper sets out the deployable instrument and the research behind it. Lab contributors and partner companies read it in full — access is tied to your account, not a paywall. The tool itself ships separately.
        </p>
        <div style={{ display: "flex", gap: 14, justifyContent: "center", flexWrap: "wrap" }}>
          {!loggedIn && <EBButtonR variant="primary" size="md" icon="none" onClick={onSignIn}>Sign in</EBButtonR>}
          <EBButtonR variant={loggedIn ? "primary" : "ghost"} size="md" icon="none" onClick={onRequest}>Request access</EBButtonR>
        </div>
        <div style={{ marginTop: 22, fontFamily: "var(--font-ui)", fontSize: 10.5, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--text-muted)" }}>Worked in this Lab, or a partner company? Request access.</div>
      </div>
    </div>
  );
}

/* ---- LinkedIn share image (downloadable / shareable, 1200×1200 square) ----
   Members get better reach posting a native image than a link. This bakes the
   article's own cover + title + author + date + the three-bar mark into the
   LinkedIn-optimal SQUARE frame, so even non-designers post something on-brand. */
function ebtrBuildShareBlob(article, cb) {
  const W = 1200, H = 1200;
  const cv = document.createElement("canvas");
  cv.width = W; cv.height = H;
  const x = cv.getContext("2d");
  const author = ebtrResolveAuthor(article);
  const date = article.date ? new Date(article.date).toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" }) : "";
  const pad = 84;

  function paint(coverImg) {
    x.fillStyle = "#141212"; x.fillRect(0, 0, W, H);
    if (coverImg) {
      const ir = coverImg.width / coverImg.height, fr = W / H;
      let dw, dh, dx, dy;
      if (ir > fr) { dh = H; dw = H * ir; dx = (W - dw) / 2; dy = 0; }
      else { dw = W; dh = W / ir; dx = 0; dy = (H - dh) / 2; }
      try { x.drawImage(coverImg, dx, dy, dw, dh); } catch (e) {}
      const g = x.createLinearGradient(0, 0, 0, H);
      g.addColorStop(0, "rgba(15,13,13,0.30)"); g.addColorStop(0.42, "rgba(15,13,13,0.55)"); g.addColorStop(0.72, "rgba(15,13,13,0.88)"); g.addColorStop(1, "rgba(15,13,13,0.97)");
      x.fillStyle = g; x.fillRect(0, 0, W, H);
    } else {
      const g = x.createRadialGradient(W * 0.75, 180, 100, W * 0.75, 180, 900);
      g.addColorStop(0, "rgba(29,56,220,0.22)"); g.addColorStop(1, "rgba(20,18,18,0)");
      x.fillStyle = g; x.fillRect(0, 0, W, H);
    }
    // horizontal three-bar mark (like the EBTR divider) + wordmark, top-left
    const bw = 50, bh = 11, bgap = 7;
    x.fillStyle = "#FF8C03"; x.fillRect(pad, 90, bw, bh);
    x.fillStyle = "#5D5DBF"; x.fillRect(pad + bw + bgap, 90, bw, bh);
    x.fillStyle = "#1D38DC"; x.fillRect(pad + 2 * (bw + bgap), 90, bw, bh);
    x.textBaseline = "alphabetic";
    x.font = "600 23px Michroma, Arial, sans-serif";
    x.fillStyle = "#fff"; x.fillText("EBT·HUB", pad, 150);
    const wmW = x.measureText("EBT·HUB ").width;
    x.fillStyle = "#FF8C03"; x.fillText("REVIEW", pad + wmW, 150);

    // ---- bottom-anchored text block: category → (gap) → title → author ----
    const cat = ((window.EBTR_DATA.categories[article.category] || {}).label || "").toUpperCase();
    const maxW = Math.round(W * 0.74); // narrower wrap → breaks earlier
    x.font = "700 62px 'Helvetica Neue', Arial, sans-serif";
    const words = String(article.title || "").split(/\s+/);
    const lines = []; let line = "";
    for (let i = 0; i < words.length; i++) {
      const t = line ? line + " " + words[i] : words[i];
      if (x.measureText(t).width > maxW && line) { lines.push(line); line = words[i]; }
      else line = t;
    }
    if (line) lines.push(line);
    const titleLines = lines.slice(0, 5);
    const lineH = 74;
    const catH = cat ? 26 : 0;
    const gapCatTitle = cat ? 40 : 0; // more breathing room between category and title
    const titleH = titleLines.length * lineH;
    const meta = [author && author.name, date].filter(Boolean).join("   ·   ");
    const gapTitleAuthor = meta ? 30 : 0;
    const authorH = meta ? 26 : 0;
    const total = catH + gapCatTitle + titleH + gapTitleAuthor + authorH;
    let y = H - 96 - total;

    if (cat) { x.font = "600 21px Michroma, Arial, sans-serif"; x.fillStyle = "#FF8C03"; y += catH; x.fillText(cat, pad, y); }
    y += gapCatTitle;
    x.font = "700 62px 'Helvetica Neue', Arial, sans-serif"; x.fillStyle = "#ffffff";
    titleLines.forEach((ln, i) => x.fillText(ln, pad, y + lineH * 0.78 + i * lineH));
    y += titleH;
    if (meta) { y += gapTitleAuthor; x.font = "600 21px Michroma, Arial, sans-serif"; x.fillStyle = "#FF8C03"; x.fillText(meta, pad, y); }

    cv.toBlob(function (blob) { cb && cb(blob); }, "image/png");
  }

  const ready = (document.fonts && document.fonts.ready) ? document.fonts.ready : Promise.resolve();
  ready.then(function () {
    if (article.coverUrl) {
      const img = new Image();
      img.crossOrigin = "anonymous";
      img.onload = function () { paint(img); };
      img.onerror = function () { paint(null); };
      img.src = (typeof window !== "undefined" ? window.location.origin : "https://review.ebthub.com") + "/a/" + article.id + "?img=1";
    } else { paint(null); }
  });
}

/* ---- Share row (LinkedIn · WhatsApp · copy · PDF · image) ---- */
function EBTRShare({ article, onPdf }) {
  const [copied, setCopied] = React.useState(false);
  const [shared, setShared] = React.useState(false);
  const [imaging, setImaging] = React.useState(false);
  const [liOpen, setLiOpen] = React.useState(false);
  const imgBlobRef = React.useRef(null);
  React.useEffect(function () {
    imgBlobRef.current = null;
    try { ebtrBuildShareBlob(article, function (b) { imgBlobRef.current = b; }); } catch (e) {}
  }, [article.id]);
  const base = typeof window !== "undefined" ? window.location.origin : "https://review.ebthub.com";
  // Share URL: /a/ID is served per-article by the Cloud Function — it returns this
  // article's own og:image/title to crawlers (LinkedIn, etc.) and redirects humans into the app.
  const url = base + "/a/" + (article.id || "");
  // Per-article LinkedIn hook (editor-written) — the line that opens the post and stops the
  // scroll. Falls back to the dek-based template when no bespoke teaser is set.
  const hook = (article.linkedinTeaser || "").trim();
  const postBody = hook
    ? `${hook}\n\n"${article.title}" — the latest from EBTHub Review.\n\n#BusinessTransformation #AI #Leadership`
    : `${article.dek}\n\n"${article.title}" — the latest from EBTHub Review, the publication of the European Business Transformation Hub.\n\n#BusinessTransformation #AI #Leadership`;
  // Clipboard text carries NO url — the share-offsite card already provides the link
  // (image + title). Including the url here would duplicate the link in the post.
  const post = postBody;
  const ln = () => {
    // LinkedIn blocks pre-filling post text via any public URL. So we copy the editor's
    // text and show a quick 3-step modal teaching the user to paste — THEN open LinkedIn.
    // (A toast alone is invisible once the user is inside LinkedIn.)
    try { navigator.clipboard.writeText(post); } catch (e) {}
    setLiOpen(true);
  };
  const openLinkedIn = () => {
    try { navigator.clipboard.writeText(post); } catch (e) {}
    window.open("https://www.linkedin.com/sharing/share-offsite/?url=" + encodeURIComponent(url), "_blank", "noopener,width=640,height=720");
    setLiOpen(false);
  };
  const wa = () => {
    var txt = (hook || article.dek || article.title) + "\n\n" + url;
    window.open("https://wa.me/?text=" + encodeURIComponent(txt), "_blank", "noopener");
  };
  const copy = () => { try { navigator.clipboard.writeText(url); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 1800); };
  // Image: mobile → native share sheet (Save to Photos / WhatsApp Status); desktop → download.
  const deliverImage = (blob) => {
    if (!blob) return;
    var fname = (article.id || "ebtr") + "-linkedin.png";
    var isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || "");
    var file = null;
    try { file = new File([blob], fname, { type: "image/png" }); } catch (e) {}
    if (isMobile && file && navigator.canShare && navigator.canShare({ files: [file] })) {
      navigator.share({ files: [file], title: article.title }).catch(function () {});
      return;
    }
    var u = URL.createObjectURL(blob);
    if (isMobile) {
      // fallback: open the image so the user can long-press → Save to Photos
      window.open(u, "_blank");
    } else {
      var a = document.createElement("a"); a.href = u; a.download = fname;
      document.body.appendChild(a); a.click(); a.remove();
    }
    setTimeout(function () { URL.revokeObjectURL(u); }, 30000);
  };
  const onImage = () => {
    if (imgBlobRef.current) { deliverImage(imgBlobRef.current); return; }
    setImaging(true);
    ebtrBuildShareBlob(article, function (b) { imgBlobRef.current = b; setImaging(false); deliverImage(b); });
  };
  const btn = { width: 38, height: 38, borderRadius: "50%", border: "1px solid var(--border-strong)", background: "none", cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "var(--text-secondary)", transition: "border-color var(--dur-base), color var(--dur-base)" };
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10, margin: "22px 0 2px", flexWrap: "wrap" }}>
      <button className="ebtr-share" style={btn} title="Share on LinkedIn" onClick={ln}>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.63-1.85 3.36-1.85 3.6 0 4.27 2.37 4.27 5.45v6.29zM5.34 7.43a2.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13zM7.12 20.45H3.56V9h3.56v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.73C24 .77 23.2 0 22.22 0z" /></svg>
      </button>
      <button className="ebtr-share" style={btn} title="Share on WhatsApp" onClick={wa}>
        <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M.057 24l1.687-6.163a11.867 11.867 0 0 1-1.587-5.945C.16 5.335 5.495 0 12.05 0a11.817 11.817 0 0 1 8.413 3.488 11.824 11.824 0 0 1 3.48 8.414c-.003 6.557-5.338 11.892-11.893 11.892a11.9 11.9 0 0 1-5.688-1.448L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884a9.86 9.86 0 0 0 1.51 5.26l-.999 3.648 3.978-1.207zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414z" /></svg>
      </button>
      <button className="ebtr-share" style={btn} title={copied ? "Copied" : "Copy link"} onClick={copy}>
        {copied
          ? <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--blue-400)" strokeWidth="2.4"><path d="M20 6 9 17l-5-5" /></svg>
          : <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></svg>}
      </button>
      {onPdf && <button className="ebtr-share" style={btn} title="Download a watermarked PDF" onClick={onPdf}>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6" /><path d="M9 13h6M9 17h4" /></svg>
      </button>}
      <button className="ebtr-share" style={btn} title="Download a LinkedIn-ready image (post natively for best reach)" onClick={onImage}>
        {imaging
          ? <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
          : <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="18" height="18" rx="1" /><path d="m3 16 5-5 4 4 3-3 6 6" /><circle cx="8.5" cy="8.5" r="1.5" /></svg>}
      </button>
      {copied && <span style={{ fontFamily: "var(--font-ui)", fontSize: 11, letterSpacing: "0.04em", color: "var(--blue-400)" }}>Link copied</span>}
      {liOpen && <EBTRLinkedInHelp post={post} onOpen={openLinkedIn} onClose={() => setLiOpen(false)} />}
    </div>
  );
}

/* Teach-modal in Stefi's voice. Honest about copy state per platform:
   desktop auto-copies ("done for you"); mobile makes "Tap to copy" the primary
   action (auto-copy is unreliable in in-app browsers — don't claim what isn't true). */
function EBTRLinkedInHelp({ post, onOpen, onClose }) {
  const [copiedOK, setCopiedOK] = React.useState(false);
  const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test((typeof navigator !== "undefined" && navigator.userAgent) || "");
  const doCopy = () => { try { navigator.clipboard.writeText(post); } catch (e) {} setCopiedOK(true); setTimeout(() => setCopiedOK(false), 2000); };
  // Text is auto-copied when the modal opens (works ~80% of the time). So the primary
  // action is just Open LinkedIn; an explicit copy is a small fallback at the bottom.
  const lines = isMobile
    ? ["Stefi here. LinkedIn changed the rules — sharing takes one extra step now. Their wall, not ours.",
       "I've copied your post and I'll open LinkedIn. Then long-press → Paste, and post. The card builds itself."]
    : ["Stefi here. LinkedIn changed the rules — no more one-click sharing for outside pages. Not my call.",
       "I've copied your post and I'll open LinkedIn for you. Then paste (Ctrl/Cmd+V), and post. The card builds itself."];
  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 95, background: "rgba(10,9,9,0.74)", backdropFilter: "blur(3px)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 }}>
      <div onClick={(e) => e.stopPropagation()} style={{ position: "relative", width: "100%", maxWidth: 450, background: "var(--surface-raised)", border: "1px solid var(--border-hairline)", borderTop: "3px solid var(--ebt-purple)", padding: "clamp(24px,5vw,34px)", boxShadow: "0 30px 80px rgba(0,0,0,0.5)" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 18 }}>
          <span style={{ fontFamily: "var(--font-ui)", fontSize: 9, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--neutral-1000)", background: "var(--ebt-purple)", padding: "3px 8px" }}>AI</span>
          <span style={{ fontFamily: "var(--font-ui)", fontSize: 10, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--text-muted)" }}>Stefi · The Digital Employee</span>
        </div>
        <div style={{ marginBottom: 22 }}>
          {lines.map((l, i) => (
            <p key={i} style={{ fontFamily: "var(--font-body)", fontSize: i === 1 ? 15.5 : 14, fontWeight: i === 1 ? 700 : 400, lineHeight: 1.55, color: i === 1 ? "var(--white)" : "var(--text-secondary)", margin: i === lines.length - 1 ? 0 : "0 0 12px" }}>{l}</p>
          ))}
        </div>
        <button onClick={onOpen} style={{ width: "100%", background: "var(--ebt-purple)", border: "none", padding: "15px 18px", cursor: "pointer", fontFamily: "var(--font-ui)", fontSize: 12, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--white)" }}>Open LinkedIn →</button>
        <div style={{ textAlign: "center", marginTop: 12 }}>
          <button onClick={doCopy} style={{ background: "none", border: "none", padding: 4, cursor: "pointer", fontFamily: "var(--font-ui)", fontSize: 10.5, letterSpacing: "0.05em", color: copiedOK ? "var(--ebt-purple)" : "var(--text-muted)", textDecoration: "underline" }}>{copiedOK ? "Copied ✓" : "Didn't paste? Tap to copy again"}</button>
        </div>
      </div>
    </div>
  );
}

/* ---- Reader ---- */
function EBTRReader({ articleId, onOpen, onBack, onCategory, loggedIn, allAccess, viewerLabs, isUnlocked, onUnlock, onSignIn, onApply, onRequestAccess, protect, watermark, viewer, motion }) {
  const data = window.EBTR_DATA;
  const article = data.articles.find((a) => a.id === articleId);
  const scrollRef = React.useRef(null);
  const [progress, setProgress] = React.useState(0);

  React.useEffect(() => { window.scrollTo(0, 0); }, [articleId]);
  React.useEffect(() => {
    if (!protect) return;
    const block = (e) => { e.preventDefault(); return false; };
    const root = scrollRef.current;
    if (root) { root.addEventListener("copy", block); root.addEventListener("cut", block); root.addEventListener("contextmenu", block); root.addEventListener("dragstart", block); }
    return () => { if (root) { root.removeEventListener("copy", block); root.removeEventListener("cut", block); root.removeEventListener("contextmenu", block); root.removeEventListener("dragstart", block); } };
  }, [protect, articleId]);
  React.useEffect(() => {
    const onScroll = () => {
      const el = scrollRef.current; if (!el) return;
      const start = el.offsetTop, total = el.offsetHeight - window.innerHeight;
      const p = total > 0 ? (window.scrollY - start + 120) / total : 0;
      setProgress(Math.max(0, Math.min(1, p)));
    };
    onScroll(); window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, [articleId]);

  if (!article) return (
    <main style={{ maxWidth: 760, margin: "0 auto", padding: "80px var(--space-7)", textAlign: "center" }}>
      <div style={{ fontFamily: "var(--font-ui)", fontSize: 12, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--ebt-orange)", marginBottom: 14 }}>Not available</div>
      <h1 style={{ fontFamily: "var(--font-ui)", fontSize: "clamp(24px,3vw,38px)", letterSpacing: "0.02em", textTransform: "uppercase", color: "var(--white)", margin: "0 0 16px" }}>This piece isn't published</h1>
      <p style={{ fontFamily: "var(--font-body)", fontSize: 17, lineHeight: 1.6, color: "var(--text-secondary)", margin: "0 0 28px" }}>It may have been unpublished or moved. Head back to the latest from the Review.</p>
      <button onClick={onBack} style={{ background: "var(--ebt-orange)", border: "none", padding: "13px 26px", cursor: "pointer", fontFamily: "var(--font-ui)", fontSize: 12, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--neutral-1000)" }}>← Back to the Review</button>
    </main>
  );
  const cat = data.categories[article.category];
  const author = ebtrResolveAuthor(article);
  const isStefi = !!(article.column || article.category === "digital-employee");
  // Access model: free articles are open to everyone — no login wall, no meter.
  // Premium papers preview, then a teaser wall. They're unlocked for all-access
  // (editor / comped), a Lab match (the reader's labs include the article's lab),
  // or a manual grant — never a paywall purchase inside EBTR.
  const labMatch = !!(article.lab && (viewerLabs || []).indexOf(article.lab) >= 0);
  const locked = !!article.premium && !allAccess && !labMatch;
  const previewCount = article.freeUntil == null ? 3 : article.freeUntil;
  const blocks = locked ? article.body.slice(0, previewCount) : article.body;
  var related = (article.related || []).map((id) => data.articles.find((a) => a.id === id)).filter(Boolean);
  if (related.length < 4) {
    var pool = data.articles.filter((a) => a.id !== article.id && (a.status == null || a.status === "published") && related.indexOf(a) < 0);
    var flagged = pool.filter((a) => a.readNext);                       // editor-curated, same as the homepage
    var sameCat = pool.filter((a) => !a.readNext && a.category === article.category);
    var rest = pool.filter((a) => !a.readNext && a.category !== article.category);
    related = related.concat(flagged, sameCat, rest);
  }
  related = related.slice(0, 4);

  return (
    <React.Fragment>
      {/* progress */}
      <div style={{ position: "sticky", top: 72, zIndex: 40, height: 3, background: "var(--neutral-800)" }}>
        <div style={{ height: "100%", width: `${progress * 100}%`, background: "var(--ebt-orange)", transition: "width 80ms linear" }} />
      </div>

      <main ref={scrollRef}>
        <div style={{ maxWidth: 1600, margin: "0 auto", padding: "34px var(--space-7) 0" }}>
          <button onClick={onBack} style={{ display: "inline-flex", alignItems: "center", gap: 9, background: "none", border: "none", padding: 0, cursor: "pointer", fontFamily: "var(--font-ui)", fontSize: 11, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--text-secondary)", marginBottom: 26 }}>← The Review</button>

          <div className="ebtr-reader-grid" style={{ display: "grid", gridTemplateColumns: "240px minmax(0, 1fr) 330px", gap: 64, alignItems: "start" }}>

            {/* LEFT RAIL — Topics */}
            <aside className="ebtr-reader-left" style={{ position: "sticky", top: 100 }}>
              <EBEyebrowR>Topics</EBEyebrowR>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 16 }}>
                <button onClick={() => onCategory(article.category)} style={{ display: "inline-flex", alignItems: "center", gap: 7, background: "none", border: "1px solid var(--border-strong)", padding: "7px 12px", cursor: "pointer", fontFamily: "var(--font-ui)", fontSize: 10, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--white)", borderRadius: "var(--radius-sm)", whiteSpace: "nowrap" }}>
                  <span style={{ width: 8, height: 8, background: ebtrAccentVar(cat.color) }} />{cat.label}
                </button>
                {(article.tags || []).map((tg) => (
                  <span key={tg} style={{ fontFamily: "var(--font-ui)", fontSize: 10, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--text-secondary)", border: "1px solid var(--border-hairline)", padding: "7px 12px", borderRadius: "var(--radius-sm)", whiteSpace: "nowrap" }}>{tg}</span>
                ))}
              </div>
            </aside>

            {/* CENTER — article (hero + body share this width) */}
            <article style={{ minWidth: 0, maxWidth: 800, margin: "0 auto", width: "100%" }}>
              {isStefi && (
                <div style={{ marginBottom: 22, padding: "14px 18px", background: "rgba(93,93,191,0.14)", border: "1px solid rgba(93,93,191,0.5)", borderTop: "3px solid var(--ebt-purple)" }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 6, flexWrap: "wrap" }}>
                    <span style={{ fontFamily: "var(--font-ui)", fontSize: 9.5, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--neutral-1000)", background: "var(--ebt-purple)", padding: "3px 8px" }}>AI persona</span>
                    <span style={{ fontFamily: "var(--font-body)", fontWeight: 700, fontSize: 15, color: "var(--white)" }}>Stefi — EBTHub's first digital employee</span>
                  </div>
                  <div style={{ fontFamily: "var(--font-body)", fontSize: 13.5, lineHeight: 1.55, color: "var(--text-secondary)" }}>This column is written by an AI persona, not a human. Disclosed by design.</div>
                </div>
              )}
              {!isStefi && (
                <div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 18 }}>
                  <EBTRCategoryTag category={article.category} />
                  {article.premium && <EBTRPremiumTag />}
                </div>
              )}
              <h1 style={{ fontFamily: "var(--ebtr-display)", fontSize: "clamp(26px, 3vw, 40px)", lineHeight: 1.12, letterSpacing: "0", color: "var(--white)", margin: 0, textWrap: "balance", overflowWrap: "break-word", hyphens: "none" }}>{article.title}</h1>
              <p style={{ fontFamily: "var(--font-body)", fontSize: 21, lineHeight: 1.5, color: "var(--text-secondary)", margin: "22px 0 26px", textWrap: "pretty" }}>{article.dek}</p>

              {/* byline */}
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, flexWrap: "wrap", paddingBottom: 24, borderBottom: "1px solid var(--border-hairline)" }}>
                <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
                  <div style={{ lineHeight: 1.3 }}>
                    <div style={{ fontFamily: "var(--font-body)", fontWeight: 700, fontSize: 15, color: "var(--white)", display: "flex", alignItems: "center", gap: 8 }}>{author.name}{isStefi && <span style={{ fontFamily: "var(--font-ui)", fontSize: 8.5, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--neutral-1000)", background: "var(--ebt-purple)", padding: "2px 6px" }}>AI</span>}</div>
                    <div style={{ fontFamily: "var(--font-ui)", fontSize: 10.5, letterSpacing: "0.06em", color: "var(--text-muted)" }}>{new Date(article.date).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" })} · {article.readingMins} MIN READ</div>
                  </div>
                </div>
              </div>

              {/* share */}
              <EBTRShare article={article} onPdf={() => ebtrExportPdf(article, viewer || { name: "EBTR Reader", company: "EBTHub" })} />

              {/* hero — her portrait for columns, generated cover otherwise */}
              {isStefi && author.portraitUrl ? (
                <div style={{ display: "grid", gridTemplateColumns: "180px 1fr", gap: 24, alignItems: "center", margin: "28px 0 40px", padding: "22px", border: "1px solid var(--border-hairline)", borderLeft: "3px solid var(--ebt-purple)", background: "var(--surface-raised)" }} className="ebtr-col-hero">
                  <img src={author.portraitUrl} alt={author.name} style={{ width: 180, height: 220, objectFit: "cover", border: "1px solid var(--border-hairline)" }} />
                  <div>
                    <div style={{ fontFamily: "var(--font-ui)", fontSize: 10.5, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--ebt-purple)", marginBottom: 8 }}>The Digital Employee</div>
                    <div style={{ fontFamily: "var(--font-body)", fontWeight: 700, fontSize: 19, color: "var(--white)", marginBottom: 6 }}>{author.name}</div>
                    <p style={{ fontFamily: "var(--font-body)", fontSize: 14.5, lineHeight: 1.6, color: "var(--text-secondary)", margin: 0 }}>{author.bio}</p>
                  </div>
                </div>
              ) : (
                <div style={{ height: 420, border: "1px solid var(--border-hairline)", margin: "28px 0 40px" }}><EBTRCover cover={article.cover} accent={cat.color} big src={article.coverUrl} /></div>
              )}

              {/* body */}
              <div className={protect ? "ebtr-protect" : undefined} style={{ position: "relative" }}>
                {watermark && viewer && <EBTRWatermark viewer={viewer} />}
                {blocks.map((b, i) => <EBTRBlock key={i} block={b} />)}
              </div>
              {locked
                ? <EBTRPremiumWall article={article} loggedIn={loggedIn} onSignIn={onSignIn} onRequest={() => onRequestAccess && onRequestAccess(article)} />
                : <React.Fragment><EBTRReferences refs={article.references} doi={article.doi} /><EBTRAuthorCard article={article} />{typeof EBTRInlineCTA !== "undefined" && <EBTRInlineCTA campaign={article.id} />}</React.Fragment>}
            </article>

            {/* RIGHT RAIL — What to Read Next */}
            <aside className="ebtr-reader-right" style={{ position: "sticky", top: 100 }}>
              <div style={{ paddingBottom: 14, borderBottom: "2px solid var(--ebt-blue)", marginBottom: 8 }}>
                <span style={{ fontFamily: "var(--font-ui)", fontSize: 12, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--blue-400)" }}>What to Read Next</span>
              </div>
              {related.map((a, i) => {
                const rc = data.categories[a.category];
                return (
                  <button key={a.id} onClick={() => onOpen(a.id)} className="ebtr-next" style={{ display: "block", width: "100%", textAlign: "left", background: "none", border: "none", borderBottom: "1px solid var(--border-hairline)", padding: "20px 0", cursor: "pointer" }}>
                    <div style={{ fontFamily: "var(--font-ui)", fontSize: 15, color: ebtrAccentVar(rc.color), marginBottom: 8 }}>{String(i + 1).padStart(2, "0")}</div>
                    <div className="ebtr-next-t" style={{ fontFamily: "var(--font-body)", fontWeight: 700, fontSize: 17, lineHeight: 1.25, color: "var(--text-primary)", transition: "color var(--dur-base)" }}>{a.title}</div>
                    <div style={{ fontFamily: "var(--font-ui)", fontSize: 10, letterSpacing: "0.06em", color: "var(--text-muted)", marginTop: 8 }}>{a.readingMins} MIN{a.premium ? " · PREMIUM" : ""}</div>
                  </button>
                );
              })}
            </aside>
          </div>
        </div>
      </main>
    </React.Fragment>
  );
}

Object.assign(window, { EBTRReader });
