/* ================================================================
   events-block.jsx — reusable "Events and podcasts" content
   listing block for Capital Economics. Groups by what the user
   has to DO (dated + action-required vs watch/listen now), not by
   content taxonomy. Extends the Insights hub card + access-badge
   system; inherits tokens, type ramp, container width and grid.

   Bands: A header · B lead (next online event) · C coming up ·
   D watch/listen anytime · E conversion (anonymous only).
   Driven by { items, viewer, loading } so one component serves
   every state and viewer variant.
================================================================ */
const { MAX, NS, SERIF, SiteHeader, SiteFooter } = window;
const { DesignCanvas, DCSection, DCArtboard, DCPostIt } = window;

/* Fixed "now" so urgency cues are deterministic on the canvas. */
const TODAY = new Date("2026-08-13T00:00:00");

/* ---------- icons (Lucide subset) ---------- */
const EI = {
  play: <path d="M6 3v18l15-9z" fill="currentColor" stroke="none" />,
  clock: <><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></>,
  "map-pin": <><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" /><circle cx="12" cy="10" r="3" /></>,
  calendar: <><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" /></>,
  "calendar-plus": <><path d="M21 13V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8M16 2v4M8 2v4M3 10h18M18 15v6M15 18h6" /></>,
  mic: <><rect width="6" height="13" x="9" y="2" rx="3" /><path d="M5 10a7 7 0 0 0 14 0M12 19v3" /></>,
  headphones: <><path d="M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a1 1 0 0 1-1-1v-6a9 9 0 0 1 18 0v6a1 1 0 0 1-1 1h-2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3" /></>,
  "arrow-right": <><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></>,
  "arrow-up-right": <><path d="M7 17 17 7M7 7h10v10" /></>,
};
function EIcon({ name, size = 18, stroke = 2, style }) {
  return <svg style={style} width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">{EI[name]}</svg>;
}

/* ---------- date/time helpers (British formatting) ---------- */
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
function dParts(iso) { const d = new Date(iso + "T00:00:00"); return { day: d.getDate(), mon: MONTHS[d.getMonth()], monU: MONTHS[d.getMonth()].toUpperCase() }; }
function fmtDate(iso) { const d = new Date(iso + "T00:00:00"); return d.getDate() + " " + MONTHS[d.getMonth()] + " " + d.getFullYear(); }
function daysUntil(iso) { const d = new Date(iso + "T00:00:00"); return Math.round((d - TODAY) / 86400000); }
function urgency(iso) { const n = daysUntil(iso); if (n <= 0 || n > 7) return null; return n === 1 ? "Tomorrow" : "In " + n + " days"; }

/* ================================================================
   Data — richer event/podcast records. Access is derived from
   type: online = Trial access, in person = Invitation, on demand
   and podcasts = Free.
================================================================ */
const ONLINE = [
  { id: "ev-l", kind: "event", mode: "online", title: "The September policy pivot: Fed, ECB and BoE in one hour", standfirst: "Our chief economists on where the three big central banks go next, and what it means for rates and FX.", date: "2026-08-18", time: "15:00", tz: "BST", tzOffset: "+01:00", duration: 60, topic: "Monetary policy", region: "Global", speaker: { name: "Jennifer McKeown", role: "Chief Global Economist" }, url: "/events/september-policy-pivot" },
  { id: "ev-o2", kind: "event", mode: "online", title: "China after the stimulus: what the data is really telling us", standfirst: "Reading through the headline GDP print to the parts of the economy that actually matter.", date: "2026-08-27", time: "09:00", tz: "SGT", tzOffset: "+08:00", duration: 45, topic: "Growth", region: "China", speaker: { name: "Julian Evans-Pritchard", role: "Head of China Economics" }, url: "/events/china-after-stimulus" },
  { id: "ev-o3", kind: "event", mode: "online", title: "The dollar in 2027: how far can the rally run", standfirst: "Rate differentials, safe-haven demand and the level that would make us change our call.", date: "2026-09-10", time: "14:00", tz: "EDT", tzOffset: "-04:00", duration: 45, topic: "FX", region: "US", speaker: { name: "Jonas Goltermann", role: "Deputy Chief Markets Economist" }, url: "/events/dollar-2027" },
  { id: "ev-o4", kind: "event", mode: "online", title: "Euro-zone disinflation: the last mile", standfirst: "Why the final stretch back to target is the hardest, and how the ECB is likely to respond.", date: "2026-09-24", time: "11:00", tz: "CEST", tzOffset: "+02:00", duration: 45, topic: "Inflation", region: "Europe", speaker: { name: "Andrew Kenningham", role: "Chief Europe Economist" }, url: "/events/euro-zone-disinflation" },
];
const INPERSON = [
  { id: "ev-p1", kind: "event", mode: "inperson", title: "The 2027 Outlook: rates, growth and the US election aftermath", standfirst: "Our flagship client briefing, followed by drinks and direct time with the economists.", date: "2026-09-03", time: "18:00", tz: "BST", tzOffset: "+01:00", duration: 120, topic: "Growth", region: "UK", city: "London", venue: "Cardinal Place", speaker: { name: "Neil Shearing", role: "Group Chief Economist" }, url: "/events/2027-outlook-london" },
  { id: "ev-p2", kind: "event", mode: "inperson", title: "Commodities supercycle debate: our view against consensus", standfirst: "A live, on-the-record debate on whether the supercycle thesis survives the cycle.", date: "2026-09-17", time: "12:00", tz: "SGT", tzOffset: "+08:00", duration: 90, topic: "Commodities", region: "Global", city: "Singapore", venue: "CapitaGreen", speaker: { name: "Caroline Bain", role: "Chief Commodities Economist" }, url: "/events/commodities-supercycle-singapore" },
];
const ONDEMAND = [
  { id: "od-1", kind: "ondemand", title: "Mid-year review: the higher-for-longer world", date: "2026-06-20", duration: 47, topic: "Monetary policy", region: "Global", chart: "line", url: "/events/mid-year-review" },
  { id: "od-2", kind: "ondemand", title: "UK inflation: the last mile", date: "2026-05-14", duration: 38, topic: "Inflation", region: "UK", chart: "bars", url: "/events/uk-inflation-last-mile" },
  { id: "od-3", kind: "ondemand", title: "Property markets after the reset", date: "2026-04-30", duration: 52, topic: "Property", region: "Global", chart: "bars", url: "/events/property-after-reset" },
  { id: "od-4", kind: "ondemand", title: "The China property drag, one year on", date: "2026-04-02", duration: 41, topic: "Growth", region: "China", chart: "line", url: "/events/china-property-drag" },
];
const PODS = [
  { id: "pc-1", kind: "podcast", ep: 142, title: "The Fed blinks, and what it means for the dollar", date: "2026-08-12", duration: 26, topic: "Monetary policy", region: "US", url: "/podcasts/142" },
  { id: "pc-2", kind: "podcast", ep: 141, title: "China's property drag and the global spillover", date: "2026-08-05", duration: 31, topic: "Growth", region: "China", url: "/podcasts/141" },
  { id: "pc-3", kind: "podcast", ep: 140, title: "Commodities: cycle versus supercycle", date: "2026-07-29", duration: 24, topic: "Commodities", region: "Global", url: "/podcasts/140" },
  { id: "pc-4", kind: "podcast", ep: 139, title: "The UK consumer is not out of the woods", date: "2026-07-22", duration: 28, topic: "Growth", region: "UK", url: "/podcasts/139" },
];

const FULL_SET = [...ONLINE, ...INPERSON, ...ONDEMAND, ...PODS];
const NO_UPCOMING_SET = [...ONDEMAND, ...PODS];
const ONLY_INPERSON_SET = [...INPERSON, ...ONDEMAND, ...PODS];

/* ================================================================
   Access badge — extends the Insights hub badge (same shape, size,
   position). Free = neutral grey. Trial access = brand outline.
   Invitation = quieter neutral outline so it does not compete with
   the primary conversion route. Label carries the meaning in text;
   colour never carries it alone. Hover/tap reveals the rule.
================================================================ */
function accessFor(item) {
  if (item.mode === "online") return "Trial access";
  if (item.mode === "inperson") return "Invitation";
  return "Free";
}
const BADGE_TIP = {
  Free: "Free to watch or listen. No account needed.",
  "Trial access": "Attend live with platform access, including a free trial.",
  Invitation: "Places are limited. Request an invitation and we will be in touch.",
};
function AccessBadge({ level, suppressed }) {
  if (suppressed) return null;
  const cls = level === "Free" ? "is-free" : level === "Trial access" ? "is-trial" : "is-invite";
  return (
    <span className="evb-badge-wrap">
      <span className={"evb-badge " + cls} tabIndex={0} role="button" aria-label={"Access: " + level}>{level}</span>
      <span className="evb-tip" role="tooltip">{BADGE_TIP[level]}</span>
    </span>
  );
}

/* ---------- CTA wording: item type × viewer state ---------- */
const ENTITLED = { trial: true, subscriber: true };
function ctaFor(item, viewer) {
  if (item.mode === "inperson") return "Request an invitation";
  if (item.kind === "ondemand") return "Watch now";
  if (item.kind === "podcast") return "Listen";
  return ENTITLED[viewer] ? "Register to attend" : "Start a trial to attend";
}

/* ---------- speaker avatar (initials; portraits where available) ---------- */
function Avatar({ name, size = 40 }) {
  const initials = name.split(" ").map((w) => w[0]).slice(0, 2).join("");
  return <span className="evb-avatar" style={{ width: size, height: size, fontSize: size * 0.36 }} aria-hidden="true">{initials}</span>;
}
function Speaker({ speaker, size = 40 }) {
  return (
    <span className="evb-speaker">
      <Avatar name={speaker.name} size={size} />
      <span className="evb-speaker-txt"><span className="evb-speaker-name">{speaker.name}</span><span className="evb-speaker-role">{speaker.role}</span></span>
    </span>
  );
}

/* ---------- small product motif for on-demand thumbnails ---------- */
function Motif({ kind = "line", seed = 1 }) {
  const W = 320, H = 180;
  const rnd = (i) => { const x = Math.sin(seed * 9301 + i * 49297) * 233280; return x - Math.floor(x); };
  if (kind === "bars") {
    const n = 9, bw = 22, gap = (W - 44 - n * bw) / (n - 1);
    return <svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%" preserveAspectRatio="none" aria-hidden="true">{Array.from({ length: n }).map((_, i) => { const h = 26 + rnd(i) * 118; return <rect key={i} x={22 + i * (bw + gap)} y={H - 16 - h} width={bw} height={h} rx="2" fill="#164194" fillOpacity={0.3 + (i / (n - 1)) * 0.55} />; })}</svg>;
  }
  const n = 9, pts = Array.from({ length: n }).map((_, i) => [22 + i * ((W - 44) / (n - 1)), H - 20 - rnd(i) * 122]);
  const line = pts.map((p, i) => (i ? "L" : "M") + p[0].toFixed(0) + " " + p[1].toFixed(0)).join(" ");
  return <svg viewBox={`0 0 ${W} ${H}`} width="100%" height="100%" preserveAspectRatio="none" aria-hidden="true"><defs><linearGradient id={"eg" + seed} x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#164194" stopOpacity="0.2" /><stop offset="100%" stopColor="#164194" stopOpacity="0" /></linearGradient></defs><path d={line + ` L ${W - 22} ${H - 16} L 22 ${H - 16} Z`} fill={`url(#eg${seed})`} /><path d={line} fill="none" stroke="#164194" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" /></svg>;
}

/* ================================================================
   Band B — lead item (next upcoming online event, or in-person if
   that is all there is). Full-width horizontal card: ~1/3 date and
   metadata panel, ~2/3 content. The most prominent element in the
   block. Vertical on mobile.
================================================================ */
function LeadItem({ item, viewer }) {
  const { day, mon } = dParts(item.date);
  const online = item.mode === "online";
  const urg = urgency(item.date);
  const badge = accessFor(item);
  const suppressed = online && ENTITLED[viewer];
  const formatLabel = online ? "Online briefing" : "In person · " + item.city;
  return (
    <div className="evb-lead">
      <div className="evb-lead-panel">
        <div className="evb-lead-format">{formatLabel}</div>
        <div className="evb-lead-date">
          <span className="evb-lead-day">{day}</span>
          <span className="evb-lead-mon">{mon} {new Date(item.date + "T00:00:00").getFullYear()}</span>
        </div>
        {urg && <span className="evb-urgency">{urg}</span>}
        <div className="evb-lead-when">
          <span className="evb-when-row"><EIcon name="clock" size={16} /><time dateTime={item.date + "T" + item.time + item.tzOffset}>{item.time} {item.tz}</time></span>
          <span className="evb-when-row"><EIcon name={online ? "calendar" : "map-pin"} size={16} />{online ? item.duration + " min live" : item.venue}</span>
        </div>
      </div>
      <div className="evb-lead-body">
        <div className="evb-eyebrow">{item.topic} · {item.region}</div>
        <h3 className="evb-lead-title">{item.title}</h3>
        <p className="evb-lead-standfirst">{item.standfirst}</p>
        <div className="evb-lead-foot">
          <Speaker speaker={item.speaker} size={44} />
          <AccessBadge level={badge} suppressed={suppressed} />
        </div>
        <div className="evb-lead-cta">
          <a href={item.url} className="evb-btn evb-btn-primary">{ctaFor(item, viewer)}<EIcon name="arrow-right" size={17} /></a>
          {online && !ENTITLED[viewer] && <a href="/trial" className="evb-textlink">What you get on a trial</a>}
          {online && ENTITLED[viewer] && <button type="button" className="evb-textlink evb-addcal"><EIcon name="calendar-plus" size={16} />Add to calendar</button>}
        </div>
      </div>
    </div>
  );
}

/* ================================================================
   Band C — upcoming event card. Date is the hook, no filler image.
   Date block, format + access badge, headline (2 lines), speaker,
   CTA per viewer.
================================================================ */
function ComingUpCard({ item, viewer }) {
  const { day, monU } = dParts(item.date);
  const online = item.mode === "online";
  const urg = urgency(item.date);
  const suppressed = online && ENTITLED[viewer];
  return (
    <a href={item.url} className="evb-ev-card">
      <div className="evb-ev-top">
        <div className="evb-datebox"><span className="evb-datebox-mon">{monU}</span><span className="evb-datebox-day">{day}</span></div>
        <div className="evb-ev-when">
          <span className="evb-ev-time"><time dateTime={item.date + "T" + item.time + item.tzOffset}>{item.time} {item.tz}</time></span>
          {urg && <span className="evb-urgency evb-urgency-sm">{urg}</span>}
        </div>
      </div>
      <div className="evb-ev-labels">
        <span className={"evb-format " + (online ? "is-online" : "is-inperson")}>{online ? "Online briefing" : "In person · " + item.city}</span>
        <AccessBadge level={accessFor(item)} suppressed={suppressed} />
      </div>
      <h4 className="evb-ev-title">{item.title}</h4>
      <div className="evb-ev-speaker"><Speaker speaker={item.speaker} size={34} /></div>
      <div className="evb-ev-cta"><span className="evb-btn evb-btn-outline">{ctaFor(item, viewer)}</span></div>
    </a>
  );
}

/* ================================================================
   Band D — on-demand recording / podcast card. 16:9 thumbnail with
   play affordance + duration overlay, type badge top-left, eyebrow,
   headline, meta row with Free badge. Everything here is free.
================================================================ */
function MediaCard({ item }) {
  const pod = item.kind === "podcast";
  const seed = parseInt(item.id.replace(/\D/g, ""), 10) || 1;
  return (
    <a href={item.url} className="evb-media-card">
      <div className={"evb-thumb" + (pod ? " is-pod" : "")}>
        {pod ? (
          <div className="evb-pod-art"><span className="evb-pod-mark"><EIcon name="headphones" size={26} /></span><span className="evb-pod-series">The Weekly Briefing</span><span className="evb-pod-ep">Episode {item.ep}</span></div>
        ) : <Motif kind={item.chart} seed={seed} />}
        <span className="evb-type-over">{pod ? "Podcast" : "Watch again"}</span>
        {!pod && <span className="evb-play" aria-hidden="true"><EIcon name="play" size={17} /></span>}
        <span className="evb-dur">{item.duration} min</span>
      </div>
      <div className="evb-media-body">
        <div className="evb-eyebrow">{item.topic} · {item.region}</div>
        <h4 className="evb-media-title">{item.title}</h4>
        <div className="evb-media-meta">
          <time dateTime={item.date}>{fmtDate(item.date)}</time>
          <span className="evb-dot">·</span><span>{item.duration} min</span>
          <span style={{ marginLeft: "auto" }}><AccessBadge level="Free" /></span>
        </div>
      </div>
    </a>
  );
}

/* ---------- Band D wrapper: filter chips + show-more ---------- */
function MediaBand({ media }) {
  const [tab, setTab] = React.useState("all");
  const [expanded, setExpanded] = React.useState(false);
  const filtered = media.filter((m) => tab === "all" ? true : tab === "watch" ? m.kind === "ondemand" : m.kind === "podcast");
  const LIMIT = 6;
  const shown = expanded ? filtered : filtered.slice(0, LIMIT);
  const chips = [["all", "All"], ["watch", "Watch"], ["listen", "Listen"]];
  return (
    <div className="evb-band">
      <div className="evb-band-head">
        <h3 className="evb-band-h">Watch and listen anytime</h3>
      </div>
      <div className="evb-chips" role="tablist" aria-label="Filter media">
        {chips.map(([k, label]) => (
          <button key={k} role="tab" aria-selected={tab === k} className={"evb-chip" + (tab === k ? " on" : "")} onClick={() => { setTab(k); setExpanded(false); }}>{label}</button>
        ))}
      </div>
      <div className="evb-media-grid">
        {shown.map((m) => <MediaCard key={m.id} item={m} />)}
      </div>
      {filtered.length > LIMIT && (
        <div className="evb-showmore"><button className="evb-btn evb-btn-outline" onClick={() => setExpanded((v) => !v)}>{expanded ? "Show fewer" : "Show more"}</button></div>
      )}
    </div>
  );
}

/* ================================================================
   Band E — conversion module (anonymous only). Connected to what
   the user has just seen. Trial primary; register subordinate as a
   text link. No consultancy.
================================================================ */
function ConversionModule() {
  return (
    <div className="evb-conv">
      <div className="evb-conv-txt">
        <h3 className="evb-conv-h">Trial access lets you join our live briefings and read the analysis behind them.</h3>
        <p className="evb-conv-p">Start now, no card required.</p>
      </div>
      <div className="evb-conv-actions">
        <a href="/trial" className="evb-btn evb-btn-inverse">Start free trial<EIcon name="arrow-right" size={17} /></a>
        <a href="/register" className="evb-textlink evb-textlink-inv">Register for 3 free credits</a>
      </div>
    </div>
  );
}

/* ---------- skeletons ---------- */
function LeadSkel() { return <div className="evb-lead evb-skel-lead"><div className="evb-lead-panel evb-skel-panel" /><div className="evb-lead-body"><div className="evb-skel-line" style={{ width: "30%" }} /><div className="evb-skel-line" style={{ width: "90%", height: 26 }} /><div className="evb-skel-line" style={{ width: "75%", height: 26 }} /><div className="evb-skel-line" style={{ width: "55%" }} /></div></div>; }
function CardSkel() { return <div className="evb-ev-card"><div className="evb-skel-line" style={{ width: 58, height: 58, borderRadius: 10 }} /><div className="evb-skel-line" style={{ width: "40%", marginTop: 16 }} /><div className="evb-skel-line" style={{ width: "92%", height: 20 }} /><div className="evb-skel-line" style={{ width: "70%", height: 20 }} /></div>; }

/* ---------- JSON-LD (Event + PodcastEpisode) for SSR/SEO ---------- */
function schemaFor(items) {
  const graph = items.map((it) => {
    if (it.kind === "podcast") return { "@type": "PodcastEpisode", name: it.title, datePublished: it.date, timeRequired: "PT" + it.duration + "M", url: it.url, partOfSeries: { "@type": "PodcastSeries", name: "The Weekly Briefing" } };
    const base = { "@type": "Event", name: it.title, startDate: it.date + (it.time ? "T" + it.time + it.tzOffset : ""), url: it.url, eventAttendanceMode: it.mode === "online" ? "https://schema.org/OnlineEventAttendanceMode" : "https://schema.org/OfflineEventAttendanceMode", organizer: { "@type": "Organization", name: "Capital Economics" } };
    if (it.mode === "inperson") base.location = { "@type": "Place", name: it.venue, address: it.city };
    else if (it.kind === "ondemand") base.eventStatus = "https://schema.org/EventScheduled";
    return base;
  });
  return JSON.stringify({ "@context": "https://schema.org", "@graph": graph });
}

/* ================================================================
   EventsBlock — assembles the five bands with all conditional
   logic. Bands B/C/E are conditional.
================================================================ */
const DEFAULT_HEADING = "Events and podcasts";
const DEFAULT_SUB = "Live briefings with our economists, recordings you can watch back, and our podcast.";
const NOEVENTS_SUB = "Recordings you can watch back, and our podcast. Free to everyone, no account needed.";

function EventsBlock({ heading = DEFAULT_HEADING, supporting, items = FULL_SET, viewer = "anonymous", loading = false }) {
  const upcoming = items.filter((it) => it.kind === "event").sort((a, b) => (a.date < b.date ? -1 : 1));
  const media = items.filter((it) => it.kind === "ondemand" || it.kind === "podcast").sort((a, b) => (a.date < b.date ? 1 : -1));
  const lead = upcoming[0] || null;
  const comingUp = upcoming.slice(1);
  const sub = supporting || (upcoming.length ? DEFAULT_SUB : NOEVENTS_SUB);

  return (
    <div className="evb-wrap">
      <div className="evb-inner">
        {/* schema graph — present in initial HTML */}
        {!loading && <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: schemaFor(items) }} />}

        {/* Band A — header */}
        <div className="evb-head">
          <div>
            <h2 className="evb-head-h">{heading}</h2>
            <p className="evb-head-sub">{sub}</p>
          </div>
          <a href="/events" className="evb-head-link">All events and podcasts<EIcon name="arrow-right" size={15} /></a>
        </div>

        {loading ? (
          <>
            <LeadSkel />
            <div className="evb-grid" style={{ marginTop: 24 }}>{Array.from({ length: 3 }).map((_, i) => <CardSkel key={i} />)}</div>
          </>
        ) : (
          <>
            {/* Band B — lead */}
            {lead && <LeadItem item={lead} viewer={viewer} />}

            {/* Band C — coming up */}
            {comingUp.length > 0 && (
              <div className="evb-band">
                <div className="evb-band-head"><h3 className="evb-band-h">Coming up</h3></div>
                <div className="evb-grid">{comingUp.map((it) => <ComingUpCard key={it.id} item={it} viewer={viewer} />)}</div>
              </div>
            )}

            {/* Band D — watch and listen */}
            {media.length > 0 && <MediaBand media={media} />}

            {/* Band E — conversion (anonymous only) */}
            {viewer === "anonymous" && <ConversionModule />}
          </>
        )}
      </div>
    </div>
  );
}

/* ================================================================ CSS */
(function injectEVBCSS() {
  if (typeof document === "undefined" || document.getElementById("evb-css")) return;
  const base = `
  .evb-wrap { background: var(--surface-page); padding: 8px 0; }
  .evb-inner { max-width: ${MAX}px; margin: 0 auto; padding: 48px 32px; }
  .evb-eyebrow { font-family: ${NS}; font-weight: 700; font-size: 11px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--brand); margin-bottom: 9px; }

  /* Band A */
  .evb-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin-bottom: 24px; }
  .evb-head-h { font-family: ${SERIF}; font-weight: 600; font-size: 30px; line-height: 1.1; letter-spacing: -0.018em; color: var(--text-primary); margin: 0; }
  .evb-head-sub { font-family: ${NS}; font-size: 15.5px; line-height: 1.5; color: var(--text-secondary); margin: 8px 0 0; max-width: 640px; }
  .evb-head-link { font-family: ${NS}; font-size: 14.5px; font-weight: 700; color: var(--brand); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; flex: 0 0 auto; }
  .evb-head-link:hover { text-decoration: underline; }

  /* shared buttons */
  .evb-btn { display: inline-flex; align-items: center; gap: 8px; font-family: ${NS}; font-size: 14.5px; font-weight: 700; border-radius: var(--radius-md, 5px); padding: 11px 18px; cursor: pointer; border: 1px solid transparent; transition: background 140ms cubic-bezier(0.2,0,0,1), border-color 140ms; }
  .evb-btn-primary { background: var(--brand); color: #fff; }
  .evb-lead:hover .evb-btn-primary { background: var(--brand-hover, #0f3070); }
  .evb-btn-outline { background: transparent; color: var(--brand); border-color: var(--brand); }
  .evb-btn-outline:hover { background: var(--brand-subtle); }
  .evb-btn-inverse { background: #fff; color: var(--brand); }
  .evb-btn-inverse:hover { background: rgba(255,255,255,0.9); }
  .evb-textlink { font-family: ${NS}; font-size: 14px; font-weight: 700; color: var(--text-link); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }
  .evb-textlink:hover { text-decoration: underline; }
  .evb-textlink-inv { color: #fff; text-decoration: underline; }
  .evb-addcal { color: var(--text-secondary); background: none; border: 0; padding: 0; cursor: pointer; }

  /* access badge (extends Insights hub badge) */
  .evb-badge-wrap { position: relative; display: inline-flex; }
  .evb-badge { font-family: ${NS}; font-weight: 700; font-size: 11px; letter-spacing: 0.02em; padding: 3px 10px; border-radius: 999px; cursor: help; white-space: nowrap; }
  .evb-badge.is-free { background: var(--grey-100, #eef0f4); color: var(--grey-600, #5e5e5e); }
  .evb-badge.is-trial { background: transparent; color: var(--brand); border: 1.5px solid var(--brand); }
  .evb-badge.is-invite { background: transparent; color: var(--text-secondary); border: 1.5px solid var(--border-strong, #c7ccd6); }
  .evb-tip { position: absolute; bottom: calc(100% + 8px); left: 0; width: 220px; background: #1d233f; color: #fff; font-family: ${NS}; font-size: 12px; line-height: 1.45; padding: 9px 11px; border-radius: 8px; box-shadow: 0 10px 30px rgba(20,30,60,0.28); opacity: 0; visibility: hidden; transform: translateY(4px); transition: opacity 140ms, transform 140ms, visibility 140ms; z-index: 20; pointer-events: none; }
  .evb-badge-wrap:hover .evb-tip, .evb-badge:focus + .evb-tip { opacity: 1; visibility: visible; transform: none; }

  /* speaker */
  .evb-speaker { display: inline-flex; align-items: center; gap: 10px; }
  .evb-avatar { display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; background: var(--brand-subtle); color: var(--brand); font-family: ${NS}; font-weight: 700; letter-spacing: 0.01em; flex: 0 0 auto; }
  .evb-speaker-txt { display: flex; flex-direction: column; line-height: 1.25; }
  .evb-speaker-name { font-family: ${NS}; font-size: 13.5px; font-weight: 700; color: var(--text-primary); }
  .evb-speaker-role { font-family: ${NS}; font-size: 12.5px; color: var(--text-muted); }

  /* Band B lead */
  .evb-lead { display: grid; grid-template-columns: 5fr 7fr; background: var(--surface-card); border: 1px solid var(--border-default); border-radius: var(--radius-lg); overflow: hidden; text-decoration: none; color: inherit; box-shadow: 0 8px 30px rgba(36,33,58,0.07); transition: box-shadow 150ms cubic-bezier(0.2,0,0,1), transform 150ms; }
  .evb-lead:hover { box-shadow: 0 14px 40px rgba(36,33,58,0.12); transform: translateY(-2px); }
  .evb-lead-panel { position: relative; background: linear-gradient(158deg, #1d233f 0%, #164194 130%); color: #fff; padding: 30px 32px; display: flex; flex-direction: column; }
  .evb-lead-format { font-family: ${NS}; font-weight: 700; font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ce-blue-light, #a5b5de); }
  .evb-lead-date { margin-top: 18px; display: flex; flex-direction: column; }
  .evb-lead-day { font-family: ${SERIF}; font-weight: 600; font-size: 72px; line-height: 0.9; letter-spacing: -0.02em; }
  .evb-lead-mon { font-family: ${NS}; font-weight: 700; font-size: 15px; letter-spacing: 0.04em; margin-top: 6px; color: rgba(255,255,255,0.9); }
  .evb-urgency { align-self: flex-start; margin-top: 16px; font-family: ${NS}; font-weight: 700; font-size: 12px; letter-spacing: 0.03em; background: rgba(255,255,255,0.14); border: 1px solid rgba(255,255,255,0.25); color: #fff; padding: 4px 11px; border-radius: 999px; }
  .evb-lead-when { margin-top: auto; padding-top: 22px; display: grid; gap: 9px; }
  .evb-when-row { display: inline-flex; align-items: center; gap: 9px; font-family: ${NS}; font-size: 14px; color: rgba(255,255,255,0.9); font-variant-numeric: tabular-nums; }
  .evb-lead-body { padding: 30px 34px; display: flex; flex-direction: column; }
  .evb-lead-title { font-family: ${SERIF}; font-weight: 600; font-size: 28px; line-height: 1.14; letter-spacing: -0.018em; color: var(--text-primary); margin: 0; }
  .evb-lead-standfirst { font-family: ${NS}; font-size: 15.5px; line-height: 1.55; color: var(--text-secondary); margin: 12px 0 0; }
  .evb-lead-foot { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 22px 0 20px; padding-top: 20px; border-top: 1px solid var(--border-subtle); }
  .evb-lead-cta { display: flex; align-items: center; gap: 20px; margin-top: auto; flex-wrap: wrap; }

  /* Band C */
  .evb-band { margin-top: 44px; }
  .evb-band-head { margin-bottom: 18px; }
  .evb-band-h { font-family: ${SERIF}; font-weight: 600; font-size: 22px; letter-spacing: -0.01em; color: var(--text-primary); margin: 0; }
  .evb-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; }
  .evb-ev-card { display: flex; flex-direction: column; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); padding: 22px 22px 22px; text-decoration: none; color: inherit; transition: border-color 150ms cubic-bezier(0.2,0,0,1), box-shadow 150ms, transform 150ms; }
  .evb-ev-card:hover { border-color: var(--border-default); box-shadow: var(--shadow-sm); transform: translateY(-2px); }
  .evb-ev-top { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; }
  .evb-datebox { display: flex; flex-direction: column; align-items: stretch; width: 60px; border-radius: 9px; overflow: hidden; border: 1px solid var(--border-default); flex: 0 0 auto; }
  .evb-datebox-mon { background: #1d233f; color: #fff; font-family: ${NS}; font-weight: 700; font-size: 10.5px; letter-spacing: 0.12em; text-align: center; padding: 4px 0 3px; }
  .evb-datebox-day { background: var(--surface-card); color: #1d233f; font-family: ${SERIF}; font-weight: 600; font-size: 30px; line-height: 1; letter-spacing: -0.01em; text-align: center; padding: 7px 0 9px; font-variant-numeric: tabular-nums; }
  .evb-ev-when { display: flex; flex-direction: column; gap: 6px; }
  .evb-ev-time { font-family: ${NS}; font-size: 14px; font-weight: 600; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
  .evb-urgency-sm { background: var(--brand-subtle); border-color: #cdd8f0; color: var(--brand); align-self: flex-start; margin: 0; padding: 3px 9px; font-size: 11px; }
  .evb-ev-labels { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
  .evb-format { font-family: ${NS}; font-weight: 700; font-size: 10.5px; letter-spacing: 0.05em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px; }
  .evb-format.is-online { background: var(--advance-100, #e4f1fd); color: var(--advance-500, #1e8eeb); }
  .evb-format.is-inperson { background: var(--positive-100, #e7f4ec); color: var(--positive, #3c8f5a); }
  .evb-ev-title { font-family: ${SERIF}; font-weight: 600; font-size: 18.5px; line-height: 1.24; letter-spacing: -0.01em; color: var(--text-primary); margin: 0 0 16px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 2.48em; }
  .evb-ev-speaker { margin-bottom: 18px; }
  .evb-ev-cta { margin-top: auto; }

  /* Band D */
  .evb-chips { display: flex; gap: 8px; margin-bottom: 20px; }
  .evb-chip { font-family: ${NS}; font-size: 13.5px; font-weight: 700; padding: 7px 15px; border-radius: 999px; border: 1px solid var(--border-default); background: var(--surface-card); color: var(--text-secondary); cursor: pointer; }
  .evb-chip:hover { border-color: var(--border-strong); color: var(--text-primary); }
  .evb-chip.on { background: var(--brand); border-color: var(--brand); color: #fff; }
  .evb-media-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; }
  .evb-media-card { display: flex; flex-direction: column; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); overflow: hidden; text-decoration: none; color: inherit; transition: border-color 150ms cubic-bezier(0.2,0,0,1), box-shadow 150ms, transform 150ms; }
  .evb-media-card:hover { border-color: var(--border-default); box-shadow: var(--shadow-sm); transform: translateY(-2px); }
  .evb-thumb { position: relative; aspect-ratio: 16/9; background: #edf1fb; overflow: hidden; }
  .evb-thumb svg { width: 100%; height: 100%; display: block; }
  .evb-thumb::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, rgba(29,35,63,0.16), transparent 42%); pointer-events: none; }
  .evb-thumb.is-pod { background: radial-gradient(120% 120% at 50% 0%, #2b2a4a 0%, #24213a 60%); }
  .evb-pod-art { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 20px; color: #fff; text-align: center; }
  .evb-pod-mark { display: inline-flex; align-items: center; justify-content: center; width: 56px; height: 56px; border-radius: 999px; background: rgba(165,181,222,0.16); border: 1px solid rgba(165,181,222,0.4); color: var(--ce-blue-light, #a5b5de); }
  .evb-pod-mark svg { width: 26px; height: 26px; }
  .evb-pod-series { font-family: ${NS}; font-weight: 700; font-size: 12.5px; letter-spacing: 0.1em; text-transform: uppercase; }
  .evb-pod-ep { font-family: ${NS}; font-size: 12px; color: rgba(255,255,255,0.6); margin-top: -4px; }
  .evb-type-over { position: absolute; top: 12px; left: 12px; font-family: ${NS}; font-weight: 700; font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase; color: #fff; background: rgba(29,35,63,0.9); padding: 4px 9px; border-radius: 4px; }
  .evb-play { position: absolute; bottom: 12px; left: 12px; z-index: 1; width: 40px; height: 40px; border-radius: 999px; background: var(--brand); color: #fff; display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 6px 18px rgba(20,30,60,0.28); }
  .evb-play svg { width: 16px; height: 16px; margin-left: 2px; }
  .evb-media-card:hover .evb-play { background: var(--brand-hover, #0f3070); }
  .evb-dur { position: absolute; bottom: 12px; right: 12px; z-index: 1; font-family: ${NS}; font-weight: 700; font-size: 11.5px; color: #fff; background: rgba(29,35,63,0.82); padding: 3px 8px; border-radius: 4px; font-variant-numeric: tabular-nums; }
  .evb-media-body { padding: 15px 18px 18px; display: flex; flex-direction: column; flex: 1; }
  .evb-media-title { font-family: ${SERIF}; font-weight: 600; font-size: 17.5px; line-height: 1.24; letter-spacing: -0.01em; color: var(--text-primary); margin: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
  .evb-media-meta { display: flex; align-items: center; gap: 8px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border-subtle); font-family: ${NS}; font-size: 12.5px; color: var(--text-muted); }
  .evb-dot { opacity: 0.6; }
  .evb-showmore { display: flex; justify-content: center; margin-top: 28px; }

  /* Band E */
  .evb-conv { margin-top: 44px; display: flex; align-items: center; justify-content: space-between; gap: 28px; flex-wrap: wrap; background: linear-gradient(158deg, #1d233f 0%, #164194 130%); border-radius: var(--radius-lg); padding: 32px 34px; }
  .evb-conv-h { font-family: ${SERIF}; font-weight: 600; font-size: 23px; line-height: 1.2; letter-spacing: -0.012em; color: #fff; margin: 0; max-width: 640px; }
  .evb-conv-p { font-family: ${NS}; font-size: 14.5px; color: rgba(255,255,255,0.8); margin: 8px 0 0; }
  .evb-conv-actions { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }

  /* skeleton */
  .evb-skel-panel { background: var(--grey-100, #eef0f4); }
  .evb-skel-line { background: var(--grey-100, #eef0f4); height: 13px; border-radius: 6px; margin-bottom: 11px; }
  .evb-skel-lead .evb-lead-body { padding: 30px 34px; }
  @keyframes evbShimmer { 0%,100% { opacity: 0.55; } 50% { opacity: 1; } }
  .evb-skel-panel, .evb-skel-line { animation: evbShimmer 1.3s ease-in-out infinite; }

  /* tablet: two across */
  .evb-view-tablet .evb-grid, .evb-view-tablet .evb-media-grid { grid-template-columns: repeat(2, 1fr); }
  .evb-view-tablet .evb-inner { padding: 40px 28px; }
  `;
  /* mobile rules, applied via real media query AND the .evb-view-mobile class (for fixed-width canvas frames) */
  const mob = `
  @@.evb-inner { padding: 30px 18px; }
  @@.evb-head { flex-direction: column; align-items: flex-start; gap: 12px; }
  @@.evb-head-h { font-size: 25px; }
  @@.evb-lead { grid-template-columns: 1fr; }
  @@.evb-lead-panel { flex-direction: row; align-items: center; gap: 20px; padding: 22px 22px; flex-wrap: wrap; }
  @@.evb-lead-format { width: 100%; }
  @@.evb-lead-date { margin-top: 0; }
  @@.evb-lead-day { font-size: 52px; }
  @@.evb-lead-when { margin: 0 0 0 auto; padding-top: 0; }
  @@.evb-urgency { margin-top: 0; }
  @@.evb-lead-body { padding: 22px; }
  @@.evb-lead-title { font-size: 23px; }
  @@.evb-lead-cta { width: 100%; }
  @@.evb-lead-cta .evb-btn-primary { width: 100%; justify-content: center; }
  @@.evb-grid, @@.evb-media-grid { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; gap: 14px; margin: 0 -18px; padding: 4px 18px 12px; -webkit-overflow-scrolling: touch; }
  @@.evb-grid > *, @@.evb-media-grid > * { flex: 0 0 80%; scroll-snap-align: start; }
  @@.evb-chips { overflow-x: auto; flex-wrap: nowrap; margin: 0 -18px 16px; padding: 0 18px; -webkit-overflow-scrolling: touch; }
  @@.evb-chip { flex: 0 0 auto; }
  @@.evb-conv { flex-direction: column; align-items: flex-start; padding: 26px 22px; }
  @@.evb-conv-actions { width: 100%; }
  @@.evb-conv-actions .evb-btn-inverse { width: 100%; justify-content: center; }
  `;
  const el = document.createElement("style"); el.id = "evb-css";
  el.textContent = base + "\n@media (max-width: 860px){" + mob.replace(/@@/g, "") + "}\n" + mob.replace(/@@/g, ".evb-view-mobile ");
  document.head.appendChild(el);
})();

/* ================================================================
   Canvas — the block in every required state and viewer variant.
================================================================ */
function Frame({ cls, children }) {
  return <div className={cls || ""} style={{ background: "var(--surface-page)", minHeight: "100%" }}>{children}</div>;
}

function App() {
  return (
    <div style={{ minHeight: "100vh", background: "var(--surface-page)" }}>
      <EventsBlock viewer="anonymous" />
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
