// slides-closing.jsx — Slides 15-21 (use cases 3-5, other features, love, comparison, close)

function Slide16_AI() {
  return <UseCaseSlide
    caseNo={3}
    motif={MOTIF_AI}
    titleWord="AI Training Data"
    subtitle="Clean-room datasets for security AI"
    problem={{
      applies: "DATA GEN · ML TRAINING · LABELING",
      headline: "Training data is locked to customer networks",
      cost: "COST · LEGAL + ETHICAL",
      items: [
        "Protect customer PII",
        "Can't learn from attacks you've never seen in the wild",
        "No simple way to generate adversarial data at scale",
      ],
    }}
    solution={{
      headline: "Generate clean-room data at will",
      outcome: "DATASET · ON DEMAND · CLEAN",
      items: [
        "Build large complex networks on demand",
        "100% clean data — <span style='color:#c5ae4f'>no customer signal leaks</span>",
        "Simulate any attack pattern you can script",
        "Use any security or logging stack at your disposal",
        "User simulation adds realistic background noise",
      ],
    }}
  />;
}

function Slide17_ThreatEm() {
  return <UseCaseSlide
    caseNo={4}
    motif={MOTIF_THREATEM}
    titleWord="Threat Emulation"
    subtitle="It's Friday, you need an env by EOD"
    problem={{
      applies: "PURPLE TEAM · DETECTION ENG",
      headline: "Stand up a realistic enterprise by hand — again?",
      cost: "COST · A LONG WEEKEND",
      items: [
        "Custom Windows domain",
        "ELK + Elastic Security + Fleet with detection rules",
        "Velociraptor server + clients",
        "PowerShell script-block logging · Sysmon",
        "<span style='color:#e94560'>How long would this take you?</span>",
      ],
    }}
    solution={{
      headline: "Ansible roles · CI/CD · reproducible",
      outcome: "SPINUP · MINUTES · UNATTENDED",
      items: [
        "Ansible roles configure VMs — documented, repeatable",
        "Templates managed by Ludus, not by internal knowledge",
        "Fully documented REST API — plug into CI/CD",
        "<strong>Build, deploy, destroy, repeat</strong>",
      ],
    }}
  />;
}

// FIRST-DRAFT COPY — iterate with user.
// The Model Gym use case: drop AI agents into a Ludus range and let
// them operate like a red-team adversary (run exploits, pivot,
// escalate) inside a fully sandboxed enterprise.
function Slide18_ModelGym() {
  return <UseCaseSlide
    caseNo={5}
    motif={MOTIF_GYM}
    titleWord="Model Gym"
    subtitle="A sandboxed enterprise for AI agents to operate in"
    problem={{
      applies: "AGENT R&D · SECURITY AI · EVAL",
      headline: "No safe place to let AI agents operate",
      cost: "COST · UNTESTABLE AT SCALE",
      items: [
        "Security agents need realistic networks to be evaluated",
        "Running agents on customer networks — <span style='color:#e94560'>high risk</span>",
        "Toy environments don't reveal real-world behaviour",
        "No fair baseline to compare agent performance",
      ],
    }}
    solution={{
      headline: "Drop agents into a range — let them run",
      outcome: "OUTCOME · CONTROLLED ADVERSARY SANDBOX",
      items: [
        "Plug MCP-capable agents straight into a full Ludus range",
        "Agents run exploits, pivot, escalate — fully sandboxed",
        "Reproducible networks ⇒ apples-to-apples <span style='color:#c5ae4f'>evaluation</span>",
        "Snapshot → test → revert for repeatable scoring runs",
      ],
    }}
  />;
}

function Slide19_ThreatIntel() {
  return <UseCaseSlide
    caseNo={6}
    motif={MOTIF_INTEL}
    titleWord="Threat Intelligence"
    subtitle="Get stage 2 when commodity sandboxes can't"
    problem={{
      applies: "THREAT INTEL · MALWARE ANALYSIS"
    , headline: "Adversaries won't detonate on a default sandbox",
      cost: "COST · STAGES 2+ LOST",
      items: [
        "Sandbox detection is ubiquitous and getting better",
        "Environmental keying is becoming the standard",
        "Commercial sandboxes can't fingerprint your enterprise",
        "Never see stage 2 or hands-on keyboard action",
      ],
    }}
    solution={{
      headline: "Build a \"digital twin\" of your environment",
      outcome: "REALISTIC · REPRODUCIBLE · RELIABLE",
      items: [
        "Your domain, your users, your apps — at-scale",
        "Realistic machines that defeat anti-VM checks",
        "User emulation convinces the attacker it's real",
        "Real BIOS, dates, files — defeats <span style='color:#c5ae4f'>anti-sandbox checks</span>",
      ],
    }}
  />;
}

// ─── SLIDE 20 · OTHER FEATURES (docs-style, interactive) ───────
// Custom line-art icons for each feature tile — replace the emoji
// glyphs with HUD-style SVGs that match the deck vocabulary.
// Rendered at two sizes: small (sidebar, ~20px) and large (content
// pane, ~52px). `size` prop flows to width/height, currentColor to stroke.
const FeatureIcon = ({ id, size = 20 }) => {
  const s = size;
  const common = { width: s, height: s, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round" };
  switch (id) {
    case "blueprints":
      return (
        <svg {...common}>
          <path d="M4 2.5 H13 L16.5 6 V17.5 H4 Z"/>
          <path d="M13 2.5 V6 H16.5"/>
          <path d="M6.5 14 H13.5 L6.5 8.5 Z"/>
        </svg>
      );
    case "ai":
      return (
        <svg {...common}>
          <circle cx="10" cy="10" r="3"/>
          <circle cx="4" cy="4" r="1.2" fill="currentColor"/>
          <circle cx="16" cy="4" r="1.2" fill="currentColor"/>
          <circle cx="4" cy="16" r="1.2" fill="currentColor"/>
          <circle cx="16" cy="16" r="1.2" fill="currentColor"/>
          <path d="M5 5 L8 8 M15 5 L12 8 M5 15 L8 12 M15 15 L12 12"/>
        </svg>
      );
    case "webui":
      return (
        <svg {...common}>
          <rect x="2.5" y="3.5" width="15" height="10"/>
          <path d="M7 16.5 h6 M10 13.5 v3"/>
          <rect x="4.5" y="6" width="5" height="3.5" strokeDasharray="1.5 1"/>
          <path d="M11.5 7.5 L11.5 11.5 L13 10.3 L14 12 L14.8 11.6 L13.8 10 L15.5 10 Z" fill="currentColor" stroke="none"/>
        </svg>
      );
    case "templates":
      return (
        <svg {...common}>
          <rect x="5.5" y="2.5" width="10" height="12"/>
          <rect x="3.5" y="5" width="10" height="12"/>
          <path d="M6 9 h6 M6 12 h4"/>
        </svg>
      );
    case "testing":
      return (
        <svg {...common}>
          <path d="M10 2.5 L16.5 5 V10 C16.5 14 13.5 16.5 10 17.5 C6.5 16.5 3.5 14 3.5 10 V5 Z"/>
          <path d="M7 10 L9.3 12.2 L13.3 7.8"/>
        </svg>
      );
    case "api":
      return (
        <svg {...common}>
          <path d="M7 3 Q4 3 4 6 Q4 9 2.5 10 Q4 11 4 14 Q4 17 7 17"/>
          <path d="M13 3 Q16 3 16 6 Q16 9 17.5 10 Q16 11 16 14 Q16 17 13 17"/>
          <circle cx="7.5" cy="10" r="0.7" fill="currentColor" stroke="none"/>
          <circle cx="10" cy="10" r="0.7" fill="currentColor" stroke="none"/>
          <circle cx="12.5" cy="10" r="0.7" fill="currentColor" stroke="none"/>
        </svg>
      );
    case "net":
      return (
        <svg {...common}>
          <circle cx="10" cy="10" r="7.5"/>
          <path d="M2.5 10 H17.5 M10 2.5 Q5 10 10 17.5 M10 2.5 Q15 10 10 17.5"/>
        </svg>
      );
    case "groups":
      return (
        <svg {...common}>
          <circle cx="5.5" cy="7" r="1.8"/>
          <circle cx="10" cy="5.5" r="1.8"/>
          <circle cx="14.5" cy="7" r="1.8"/>
          <path d="M2.5 15.5 Q5.5 11 10 11 Q14.5 11 17.5 15.5"/>
        </svg>
      );
    case "cluster":
      return (
        <svg {...common}>
          <rect x="2.5" y="2.5" width="4.5" height="4.5"/>
          <rect x="8" y="2.5" width="4.5" height="4.5"/>
          <rect x="13.5" y="2.5" width="4" height="4.5"/>
          <rect x="2.5" y="8" width="4.5" height="4.5"/>
          <rect x="8" y="8" width="4.5" height="4.5"/>
          <rect x="13.5" y="8" width="4" height="4.5"/>
          <rect x="2.5" y="13.5" width="4.5" height="4"/>
          <rect x="8" y="13.5" width="4.5" height="4"/>
          <rect x="13.5" y="13.5" width="4" height="4"/>
        </svg>
      );
    case "roles":
      return (
        <svg {...common}>
          <rect x="3" y="4" width="14" height="3.2" rx="0.4"/>
          <rect x="3" y="8.4" width="14" height="3.2" rx="0.4"/>
          <rect x="3" y="12.8" width="14" height="3.2" rx="0.4"/>
          <circle cx="5.2" cy="5.6" r="0.55" fill="currentColor" stroke="none"/>
          <circle cx="5.2" cy="10" r="0.55" fill="currentColor" stroke="none"/>
          <circle cx="5.2" cy="14.4" r="0.55" fill="currentColor" stroke="none"/>
        </svg>
      );
    case "cloud":
      return (
        <svg {...common}>
          <path d="M5.5 13.5 Q2.5 13.5 2.5 10.5 Q2.5 8 5 7.5 Q6 4.5 9.5 4.5 Q13 4.5 14 8 Q17.5 8 17.5 11 Q17.5 13.5 15 13.5 Z"/>
        </svg>
      );
    case "enterprise":
      return (
        <svg {...common}>
          <rect x="3" y="5" width="6" height="12"/>
          <rect x="9" y="2.5" width="8" height="14.5"/>
          <path d="M5 8 h2 M5 11 h2 M5 14 h2 M11 5.5 h1.5 M14.5 5.5 h1.5 M11 8.5 h1.5 M14.5 8.5 h1.5 M11 11.5 h1.5 M14.5 11.5 h1.5 M11 14.5 h1.5 M14.5 14.5 h1.5"/>
        </svg>
      );
    default:
      return null;
  }
};

function Slide20_OtherFeatures() {
  const features = [
    {
      id: "blueprints",
      flagship: true,
      nav: "Blueprints",
      icon: "blueprints",
      tagline: "Save, share, and re-apply range configs in one command",
      body: "A saved range config a teammate applies instantly. Onboarding, workshops, approved configs your team can self-serve — all versioned, all shareable.",
      snippet: [
        "ludus blueprint create --id ad-lab",
        "ludus blueprint share group ad-lab sec-team",
        "ludus blueprint apply ad-lab",
      ],
      path: "/docs/using-ludus/blueprints",
    },
    {
      id: "ai",
      flagship: true,
      nav: "AI Assistants",
      icon: "ai",
      tagline: "Control Ludus from Claude, Codex, Cursor — via MCP + skills",
      body: "The MCP server exposes every Ludus API action to coding assistants; skills bundle range-config, troubleshooting, CLI and environment knowledge. Talk to your range in plain English.",
      snippet: [
        "# in your coding assistant:",
        '"snapshot all my VMs, start testing mode, allow example.com"',
        "# the agent translates to the right ludus commands",
      ],
      path: "/docs/using-ludus/mcp",
    },
    {
      id: "webui",
      flagship: true,
      nav: "Web UI",
      icon: "webui",
      tagline: "Drag-and-drop topology design · SSO-ready",
      body: "Self-hostable, offline-capable Next.js GUI on the same REST API. Run it behind SSO (PocketBase). Pro and Enterprise tiers.",
      media: "assets/web-ui.gif",
      mediaAlt: "Ludus Web UI — drag-and-drop topology designer",
      host: "ludus.cloud",
      path: "/#pro",
    },
    {
      id: "templates",
      nav: "Automated templates",
      icon: "templates",
      tagline: "Packer-built VMs from checksum-verified ISOs",
      body: "13 templates shipped out of the box — Windows, Linux, Kali, FLARE-VM, macOS. Bring your own with a Packer HCL file; they build unattended on first run.",
      snippet: [
        "ludus templates list",
        "ludus templates build",
      ],
      path: "/docs/using-ludus/templates",
    },
    {
      id: "testing",
      nav: "Testing mode",
      icon: "testing",
      tagline: "Snapshot, isolate, allow live C2 by domain or IP",
      body: "Pre-snapshot every VM, block the internet, reconnect only the domains or IPs you need for live C2 — then revert to clean state. OPSEC-safe by default.",
      snippet: [
        "ludus testing start",
        "ludus testing allow -d c2.example.com",
        "ludus testing stop    # reverts all snapshots",
      ],
      path: "/docs/quick-start/testing-mode",
    },
    {
      id: "api",
      nav: "REST API",
      icon: "api",
      tagline: "Every CLI action is an OpenAPI 3.0 HTTP call",
      body: "Wire Ludus into CI/CD, dashboards, or your own tooling. Full OpenAPI 3.0 spec ships with the server — no scraping, no guessing.",
      snippet: [
        'curl -H "X-API-KEY: $KEY" \\',
        "     https://ludus:8080/range",
        "# spec at https://<ludus>:8080/swagger",
      ],
      path: "/docs",
    },
    {
      id: "net",
      nav: "VLANs + firewall",
      icon: "net",
      tagline: "Up to 254 /24 networks with arbitrary per-port rules",
      body: "Model real enterprise topologies — flat networks, segmented DMZs, pivots. Rules are declared in YAML and applied by the Ludus router VM.",
      snippet: [
        "network:",
        "  rules:",
        "    - name: allow RDP from ops",
        "      vlan_src: 10",
        "      vlan_dst: 20",
        "      protocol: tcp",
        "      ports: 3389",
        "      action: ACCEPT",
      ],
      path: "/docs/infrastructure-operations/networking",
    },
    {
      id: "groups",
      nav: "User groups",
      icon: "groups",
      tagline: "Onboard the whole team in one command",
      body: "Create a group, add members, grant it range access — a new analyst gets their lab in a single assign. Group managers run it day-to-day without admin rights.",
      snippet: [
        "ludus group create sec-team",
        "ludus group add user sec-team alice",
        "ludus group add range sec-team CLIENT-ENG",
      ],
      path: "/docs/using-ludus/sharing",
    },
    {
      id: "cluster",
      nav: "Cluster support",
      icon: "cluster",
      tagline: "Scale a range across multiple Proxmox nodes",
      body: "Auto-detects a Proxmox cluster and spreads VMs by RAM and CPU headroom. Range networks span nodes via Proxmox SDN + VXLAN, and templates built on one node clone across the whole cluster.",
      snippet: [
        "# auto-detects your Proxmox cluster",
        "defaults:",
        "  target_node: pve1",
        "ludus:",
        "  - vm_name: \"{{ range_id }}-dc01\"",
        "    target_node: pve2   # pin to a node",
      ],
      path: "/docs/infrastructure-operations/cluster",
    },
    {
      id: "roles",
      nav: "Ansible roles",
      icon: "roles",
      tagline: "Any of 35,000+ Galaxy roles, plus your own",
      body: "Install from Ansible Galaxy, a git URL, or a local directory. Any role compatible with the VM's OS works — no special Ludus wrapping required.",
      snippet: [
        "# from Ansible Galaxy",
        "ludus ansible role add badsectorlabs.ludus_adcs",
        "",
        "# or from a local directory",
        "ludus ansible role add -d ./my_custom_role",
      ],
      path: "/docs/using-ludus/roles",
    },
    {
      id: "cloud",
      nav: "Runs anywhere",
      icon: "cloud",
      tagline: "Bare metal · Proxmox · Azure · AWS · GCP · Hyper-V · Fusion",
      body: "Anywhere Debian 12/13 or Proxmox 8/9 will run — deploy on the metal in a SCIF, on an AWS .metal EC2, or on a GCP nested-virt VM. Same experience.",
      snippet: [
        "# Any of:",
        "# bare metal · Proxmox 8/9 · Azure Dv3/Ev3",
        "# AWS *.metal · GCP nested-virt",
        "# Hyper-V · VMware Fusion",
      ],
      path: "/docs/category/deployment-options",
    },
    {
      id: "enterprise",
      nav: "Enterprise roles",
      icon: "enterprise",
      tagline: "Curated catalog on top of the OSS core",
      body: "Detection, C2, access, and identity integrations maintained by Bad Sector Labs. Drop them into any range config — no glue code, no one-off installers. Pro and Enterprise tiers.",
      snippet: [
        "# DETECTION & OBSERVABILITY",
        "velociraptor · zeek · sysmon · mde · chronicle-exporter",
        "",
        "# COMMAND & CONTROL · ACCESS",
        "mythic · guacamole · sso (pocketbase)",
        "",
        "# IDENTITY & REALISM",
        "bulk-ad-content · ghosts · unconstrained-delegation",
        "",
        "# OPERATIONS",
        "outbound-wireguard · kms · anti-sandbox",
      ],
      path: "/docs/category/enterprise",
    },
  ];

  const [sel, setSel] = React.useState(0);
  const active = features[sel];
  const { slideNo } = React.useContext(DeckCtx);

  // Up/Down arrow nav within the sidebar — only when this slide is active.
  // deck-stage.js binds Left/Right/Home/End/R/0-9 but leaves Up/Down free.
  // slideNo comes from DeckCtx so the guard tracks reordering automatically.
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
      if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
      const stage = document.querySelector("deck-stage");
      if (!stage || stage.index !== slideNo - 1) return;
      e.preventDefault();
      e.stopPropagation();
      if (e.key === "ArrowDown") {
        setSel((s) => Math.min(s + 1, features.length - 1));
      } else {
        setSel((s) => Math.max(s - 1, 0));
      }
    };
    window.addEventListener("keydown", onKey, true);
    return () => window.removeEventListener("keydown", onKey, true);
  }, [features.length, slideNo]);

  return (
    <Slide section="CAPABILITIES">
      <Content>
        <NumberedEyebrow>FEATURES OF THE PLATFORM</NumberedEyebrow>
        <Title style={{marginTop: 16}}>Highlights from the toolkit</Title>
        <div style={{fontSize: TS.body, color: DK_FG_MUTED, marginTop: 14, maxWidth: 1100}}>
          Click any entry to preview.
        </div>

        <div style={{
          flex: 1, minHeight: 0, marginTop: 32,
          display: "grid",
          gridTemplateColumns: "400px 1fr",
          gap: 24,
        }}>
          {/* ── Docs-style sidebar ───────────────────────────── */}
          <div style={{
            position: "relative",
            border: `1px solid ${DK_BORDER}`,
            background: "rgba(13,17,23,0.55)",
            display: "flex", flexDirection: "column",
            overflow: "hidden",
          }}>
            <Brackets size={10} color="rgba(197,174,79,0.35)" inset={-1} thickness={1}/>
            <div style={{
              padding: "16px 22px 14px",
              borderBottom: `1px solid ${DK_BORDER}`,
              fontSize: 11, letterSpacing: "0.3em", color: DK_FG_MUTED,
              display: "flex", justifyContent: "space-between",
            }}>
              <span>DOCS · USING LUDUS</span>
              <span style={{color: GOLD}}>
                {String(sel+1).padStart(2,"0")} / {String(features.length).padStart(2,"0")}
              </span>
            </div>
            <div style={{flex: 1, overflowY: "auto", padding: "6px 0"}}>
              {features.map((f, i) => {
                const on = i === sel;
                return (
                  <button
                    key={f.id}
                    onClick={() => setSel(i)}
                    style={{
                      width: "100%", textAlign: "left",
                      background: on ? "rgba(197,174,79,0.10)" : "transparent",
                      color: on ? DK_FG : DK_FG_MUTED,
                      border: "none",
                      borderLeft: `2px solid ${on ? GOLD : "transparent"}`,
                      padding: "7px 20px",
                      display: "flex", alignItems: "center", gap: 12,
                      cursor: "pointer",
                      fontFamily: "inherit",
                      transition: "background 120ms, color 120ms",
                    }}
                  >
                    <span style={{
                      fontSize: 11, color: on ? GOLD : DK_FG_MUTED,
                      letterSpacing: "0.15em", minWidth: 22,
                    }}>{String(i+1).padStart(2,"0")}</span>
                    <span style={{
                      width: 22, height: 22,
                      display: "inline-flex", alignItems: "center", justifyContent: "center",
                      color: on ? GOLD : DK_FG_MUTED,
                    }}>
                      <FeatureIcon id={f.icon} size={20}/>
                    </span>
                    <span style={{
                      fontSize: 17, fontWeight: on ? 600 : 500,
                      flex: 1,
                    }}>{f.nav}</span>
                    {f.flagship && (
                      <span style={{
                        fontSize: 11, letterSpacing: "0.25em",
                        color: on ? DK_BG : GOLD,
                        background: on ? GOLD : "transparent",
                        border: on ? "none" : `1px solid ${GOLD}`,
                        padding: "2px 7px",
                        fontWeight: 700,
                      }}>NEW</span>
                    )}
                    <span style={{
                      color: on ? GOLD : "transparent",
                      fontSize: 13,
                    }}>▸</span>
                  </button>
                );
              })}
            </div>
            <div style={{
              borderTop: `1px solid ${DK_BORDER}`,
              padding: "10px 22px",
              fontSize: 13, letterSpacing: "0.25em", color: DK_FG_MUTED,
              display: "flex", justifyContent: "space-between",
            }}>
              <span>↑ ↓ · CLICK TO EXPLORE</span>
              <span style={{color: GOLD}}>v2</span>
            </div>
          </div>

          {/* ── Docs-style content pane ─────────────────────── */}
          <div style={{
            position: "relative",
            border: `1px solid ${active.flagship ? GOLD : DK_BORDER}`,
            background: "rgba(13,17,23,0.55)",
            padding: "24px 40px 28px",
            display: "flex", flexDirection: "column",
            minHeight: 0,
          }}>
            <Brackets
              size={12}
              color={active.flagship ? GOLD : "rgba(197,174,79,0.45)"}
              inset={-1}
              thickness={active.flagship ? 1.5 : 1}
            />

            {/* Breadcrumb */}
            <div style={{
              fontSize: 14, letterSpacing: "0.18em",
              color: DK_FG_MUTED,
              borderBottom: `1px solid ${DK_BORDER_SOFT}`,
              paddingBottom: 14,
              display: "flex", gap: 8, flexWrap: "wrap",
            }}>
              <span>{active.host || "docs.ludus.cloud"}</span>
              {active.path.split("/").filter(Boolean).map((p, i, a) => (
                <React.Fragment key={i}>
                  <span style={{color: DK_BORDER}}>/</span>
                  <span style={{color: i === a.length-1 ? GOLD : DK_FG_MUTED}}>{p}</span>
                </React.Fragment>
              ))}
            </div>

            {/* Title + tagline */}
            <div style={{marginTop: 20, display: "flex", alignItems: "flex-start", gap: 18}}>
              <div style={{
                width: 56, height: 56, flexShrink: 0,
                border: `1px solid ${active.flagship ? GOLD : DK_BORDER}`,
                background: "rgba(197,174,79,0.08)",
                color: GOLD,
                display: "flex", alignItems: "center", justifyContent: "center",
              }}>
                <FeatureIcon id={active.icon} size={36}/>
              </div>
              <div style={{flex: 1, minWidth: 0}}>
                <div style={{
                  fontSize: 40, fontWeight: 600, color: DK_FG,
                  lineHeight: 1.1, letterSpacing: "-0.01em",
                }}>
                  {active.nav}
                </div>
                <div style={{
                  fontSize: 19, color: GOLD, marginTop: 8,
                  lineHeight: 1.3,
                }}>
                  {active.tagline}
                </div>
              </div>
              {active.flagship && (
                <div style={{
                  fontSize: 12, letterSpacing: "0.3em", color: DK_BG,
                  background: GOLD, padding: "5px 10px", fontWeight: 700,
                  alignSelf: "flex-start",
                  marginTop: 4,
                }}>NEW IN v2</div>
              )}
            </div>

            {/* Body */}
            <div style={{
              fontSize: 19, color: DK_FG_MUTED,
              lineHeight: 1.5, marginTop: 20,
              maxWidth: 960,
            }}>
              {active.body}
            </div>

            {/* Snippet OR media preview */}
            {active.media ? (
              <div style={{
                marginTop: 22,
                display: "flex", justifyContent: "center",
                flex: "none",
              }}>
                <div style={{
                  border: `1px solid ${DK_BORDER}`,
                  background: "#0a0c10",
                  padding: 8,
                  width: "100%",
                  maxWidth: 500,
                }}>
                  <img
                    src={active.media}
                    alt={active.mediaAlt || ""}
                    style={{
                      display: "block",
                      width: "100%",
                      height: "auto",
                    }}
                  />
                </div>
              </div>
            ) : (
              <div style={{
                marginTop: 22,
                border: `1px solid ${DK_BORDER}`,
                background: "#0a0c10",
                padding: "14px 20px 16px",
                fontSize: 15, lineHeight: 1.65,
              }}>
                <div style={{
                  fontSize: 13, letterSpacing: "0.3em",
                  color: DK_FG_MUTED, marginBottom: 8,
                  display: "flex", justifyContent: "space-between",
                }}>
                  <span>local:~$</span>
                  <span>{active.id.toUpperCase()}</span>
                </div>
                {active.snippet.map((line, i) => {
                  const isComment = line.trim().startsWith("#") || line.trim().startsWith('"');
                  return (
                    <div key={i} style={{
                      color: isComment ? DK_FG_MUTED : DK_FG,
                      whiteSpace: "pre-wrap",
                      fontFamily: "inherit",
                    }}>{line}</div>
                  );
                })}
              </div>
            )}

            {/* Footer: docs link (real anchor, opens in new tab) */}
            <div style={{
              marginTop: "auto", paddingTop: 18,
              borderTop: `1px solid ${DK_BORDER_SOFT}`,
              display: "flex", justifyContent: "space-between",
              alignItems: "center",
              fontSize: 13, letterSpacing: "0.15em",
              color: DK_FG_MUTED,
            }}>
              <span>READ MORE →</span>
              <a
                href={`https://${active.host || "docs.ludus.cloud"}${active.path}`}
                target="_blank"
                rel="noopener noreferrer"
                style={{
                  color: GOLD,
                  textDecoration: "none",
                  borderBottom: `1px dotted ${GOLD}`,
                  paddingBottom: 1,
                }}
              >{active.host || "docs.ludus.cloud"}{active.path}</a>
            </div>
          </div>
        </div>
      </Content>
    </Slide>
  );
}

// ─── SLIDE 20 · LOVE FOR LUDUS ─────────────────────────────────
// Real tweets pulled from bsl-landing-page/Community.tsx and
// ludus.cloud/love.tsx. Ordered best-first — the top of the deck is
// the most memorable / highest-authority quotes, then broader variety.
// Post date is derived from the status ID in the URL via the Twitter
// snowflake format. Add more tweets by appending to the array.
const TWEET_DATA = [
  {
    name: "Chris Thompson", handle: "@_Mayyhem", initial: "C", avatarBg: "#ef4444",
    body: "ludus.cloud is magic. I set up, ran 3 commands, went to sleep, and have an SCCM/AD lab this morning with tons of issues to explore. Thanks @badsectorlabs for Ludus and @synzack21 and @M4yFly for the labs!",
    highlights: ["is magic", "ran 3 commands, went to sleep"],
    url: "https://x.com/_Mayyhem/status/1833898854636548404",
  },
  {
    name: "Adam Brown", handle: "@coffeegist", initial: "A", avatarBg: "#8b5cf6",
    body: "Currently using ludus.cloud to lab some stuff out. Every time I use it, at some point, I literally wind up saying the word, \"Wow\", out loud. It does so many things out of the box that I go in assuming I'm going to have to do by hand. Thank you @badsectorlabs",
    highlights: ["\"Wow\", out loud", "so many things out of the box"],
    url: "https://x.com/coffeegist/status/1846309341587996770",
  },
  {
    name: "TJ Null", handle: "@TJ_Null", initial: "T", avatarBg: "#0ea5e9",
    body: "I have to give a shoutout to @badsectorlabs for sharing Ludus. It is an actual game changer when you want to deploy your own ranges for testing on the fly.",
    highlights: ["actual game changer"],
    url: "https://x.com/TJ_Null/status/1857128014548709545",
  },
  {
    name: "rcegan", handle: "@rcegann", initial: "R", avatarBg: "#3b82f6",
    body: "Can't say enough good things about #ludus by @badsectorlabs. I've used a lot of Cyber Range management tools and none come close to how refined and reliable ludus is. Kudos to the team 👏👏👏",
    highlights: ["none come close", "refined and reliable"],
    url: "https://x.com/rcegann/status/1784790583594348663",
  },
  {
    name: "Kaitlyn DeValk-Hammond", handle: "@kaitlyn_devalk", initial: "K", avatarBg: "#f59e0b",
    body: "Seriously one of the most awesome tools I've ever used... 30 minutes and I have a small AD environment with an Elastic server + agents deployed with 0 manual effort 😎",
    highlights: ["one of the most awesome tools I've ever used", "0 manual effort"],
    url: "https://x.com/kaitlyn_devalk/status/1805817687970590869",
  },
  {
    name: "Max Harley", handle: "@0xdab0", initial: "M", avatarBg: "#ec4899",
    body: "Another quarterly appreciation post for @badsectorlabs Ludus. This time, I'll point out how dynamic it is. If you need tons of really weird one-off configurations, it's super good at handling that and keeping track of state for you https://ludus.cloud",
    highlights: ["quarterly appreciation post", "how dynamic it is"],
    url: "https://x.com/0xdab0/status/1909984238138544451",
  },
  {
    name: "Hulto", handle: "@Hultoko", initial: "H", avatarBg: "#14b8a6",
    body: "I just spun up Ludus by @badsectorlabs for the first time. Everything just works! It's brought me a great amount of joy! Thank you 😄",
    highlights: ["Everything just works", "a great amount of joy"],
    url: "https://x.com/Hultoko/status/1929686171153846469",
  },
  {
    name: "jdelta", handle: "@jdelta11", initial: "J", avatarBg: "#7c3aed",
    body: "Check out Ludus (@badsectorlabs). Absolute game changer for labs, I use it daily. e.g. easily deploy GOAD, AD lab, etc",
    highlights: ["Absolute game changer", "I use it daily"],
    url: "https://x.com/jdelta11/status/1871735923828023567",
  },
  {
    name: "Carter", handle: "@CarterMcKelvain", initial: "C", avatarBg: "#a855f7",
    body: "ludus 100% - I had our labs fully deployed with AutomatedLabs originally, but ludus is soooo much better. They are also actively developing it.",
    highlights: ["soooo much better"],
    url: "https://x.com/CarterMcKelvain/status/1775295786277859341",
  },
  {
    name: "Bryce Zuccaro", handle: "@c4ch3c4d3", initial: "B", avatarBg: "#22c55e",
    body: "Ludus is awesome! Used it to stand up GOAD & a personal elastic stack I use for testing already. Phenomenal project! Much Thanks!",
    highlights: ["Phenomenal project"],
    url: "https://x.com/c4ch3c4d3/status/1775894998346793295",
  },
  {
    name: "BlaiseBits", handle: "@BlaiseBits", initial: "B", avatarBg: "#d946ef",
    body: "Ludus is such a badass tool. You can spin up a fully configured lab, or just a shell to configure yourself to get a deeper understanding.\n\nBeing great for quick or deep work is its golden feature.",
    highlights: ["such a badass tool", "golden feature"],
    url: "https://x.com/BlaiseBits/status/1870146191117431039",
  },
  {
    name: "Ali Hadi · B!n@ry", handle: "@binaryz0ne", initial: "A", avatarBg: "#dc2626",
    body: "I used to have to do all these steps manually, now it's just 'ludus range deploy'!!! Thank you both!",
    highlights: ["'ludus range deploy'"],
    url: "https://x.com/binaryz0ne/status/1773821725680996860",
  },
  {
    name: "Fawaz", handle: "@q8fawazo", initial: "F", avatarBg: "#10b981",
    body: "Oh I love Ludus, I run it on my other server with exchange SCCM ADFS Elastic and all the nice thingies\n\nI can definitely say Ludus is one of the best projects I've ever came across and it saved me so much time",
    highlights: ["one of the best projects I've ever came across"],
    url: "https://x.com/q8fawazo/status/1871830731703697440",
  },
  {
    name: "Garrett", handle: "@unsigned_sh0rt", initial: "G", avatarBg: "#f97316",
    body: "All of my current research is done on a Ludus lab",
    highlights: ["All of my current research"],
    url: "https://x.com/unsigned_sh0rt/status/1947795994491687002",
  },
  {
    name: "Matthieu 🐙", handle: "@_Euzebius", initial: "M", avatarBg: "#059669",
    body: "Wouldn't think about hosting a lab without using Ludus now that I've been using it for months",
    highlights: ["Wouldn't think about hosting a lab without"],
    url: "https://x.com/_Euzebius/status/1947910976356937979",
  },
  {
    name: "OneCloudEmoji", handle: "@OneCloudEmoji", initial: "☁", avatarBg: "#0284c7",
    body: "I went with the ludus method + stand-alone exchange setup; found it very painless and would recommend.\n\nGoad, exchange, SCCM, ADCS ranges all in a neat row on my nuc. @badsectorlabs did a brilliant job.",
    highlights: ["brilliant job"],
    url: "https://x.com/OneCloudEmoji/status/1874943573256499210",
  },
  {
    name: "Steve Campbell", handle: "@lpha3ch0", initial: "S", avatarBg: "#2563eb",
    body: "For a home lab, it's hard to beat Ludus with the GOAD, Vulhub, and Commando roles added.",
    highlights: ["hard to beat Ludus"],
    url: "https://x.com/lpha3ch0/status/1812147439064768650",
  },
  {
    name: "Aleem Ladha", handle: "@LadhaAleem", initial: "A", avatarBg: "#b45309",
    body: "Thank you :) . Ludus streamlines the deployment of complex, multi-forest labs, offering seamless management across multiple VLANs and enabling easy sharing of cyber ranges. 🔥🔥🔥",
    highlights: ["streamlines the deployment of complex, multi-forest labs"],
    url: "https://twitter.com/LadhaAleem/status/1780781354449342932",
  },
  {
    name: "Connor B", handle: "@CzeeBzee", initial: "C", avatarBg: "#65a30d",
    body: "I run a mini pc for my cyber range running Ludus and it's insane how much money this saves me compared to cloud hosting.",
    highlights: ["insane how much money this saves"],
    url: "https://x.com/CzeeBzee/status/1920078682036588886",
  },
  {
    name: "syndrowm", handle: "@syndrowm", initial: "S", avatarBg: "#7c2d12",
    body: "For all the homelab hackers running away from ESXi... ludus from @badsectorlabs makes proxmox a compelling option",
    highlights: ["makes proxmox a compelling option"],
    url: "https://x.com/syndrowm/status/1765892598026146278",
  },
  {
    name: "N7WEra", handle: "@N7WEra", initial: "N", avatarBg: "#6366f1",
    body: "Ludus (ludus.cloud) is definitely the hot new name, and it's awesome.",
    highlights: ["hot new name"],
    url: "https://x.com/N7WEra/status/1775260760362651756",
  },
  {
    name: "bohops", handle: "@bohops", initial: "B", avatarBg: "#0369a1",
    body: "@badsectorlabs created this and it is amazing for automating labs and ranges: ludus.cloud",
    highlights: ["amazing for automating labs and ranges"],
    url: "https://x.com/bohops/status/1770916110478561451",
  },
  {
    name: "NotMe", handle: "@jessefmoore", initial: "J", avatarBg: "#be185d",
    body: "🥳 Ludus automating building VMs in a range is soooooo good! 😎🔥",
    highlights: ["soooooo good"],
    url: "https://x.com/jessefmoore/status/1823569475960234059",
  },
  {
    name: "Tzar", handle: "@dsec_net", initial: "T", avatarBg: "#991b1b",
    body: "Hetzner AX52, edr lab and goad running like a champ. Still the best and least buggy install of proxmox I've ever used before even getting to all the extra cool stuff 🤣",
    highlights: ["best and least buggy install of proxmox I've ever used"],
    url: "https://x.com/dsec_net/status/1773475547667890505",
  },
  {
    name: "GzobraJn", handle: "@gzobraJn", initial: "G", avatarBg: "#475569",
    body: "Last week, i discovered ludus.cloud\n\nNow, if you have the necessary hardware and enough network speed, no excuse to launch a lab.",
    highlights: ["no excuse to launch a lab"],
    url: "https://x.com/gzobraJn/status/1757454184931033447",
  },
  {
    name: "y3kAndy", handle: "@thefinalfl46498", initial: "Y", avatarBg: "#0891b2",
    body: "Wanted to share Ludus, my brother pmo\nIts IaaC like terraform but for deploying baremetal, so for example setting up a whole security lab using 1 template\nhttps://ludus.cloud pretty nice for projects and work",
    url: "https://x.com/thefinalfl46498/status/1835407098730999910",
  },
  {
    name: "Garrett", handle: "@unsigned_sh0rt", initial: "G", avatarBg: "#f97316",
    body: "I know I'm shilling but @badsectorlabs Ludus SCCM template by @synzack21 makes it a cake walk. Fresh SCCM lab deployed in ~2 hours without the pain",
    highlights: ["cake walk", "without the pain"],
    url: "https://x.com/unsigned_sh0rt/status/1826283957287354846",
  },
];

// Handle → local avatar path. Missing handles fall back to the
// colored initial circle. Keys are lowercased so @Hultoko and
// @hultoko both resolve. Files live in assets/avatars/ and are
// bundled with the deck — no runtime network calls.
const AVATAR_MAP = {
  "@_mayyhem":        "assets/avatars/mayyhem.jpg",
  "@coffeegist":      "assets/avatars/coffeegist.jpg",
  "@tj_null":         "assets/avatars/TJ_Null.jpg",
  "@rcegann":         "assets/avatars/rcegann.jpg",
  "@kaitlyn_devalk":  "assets/avatars/kaitlyn-devalk.jpg",
  "@0xdab0":          "assets/avatars/0xdab0.jpg",
  "@hultoko":         "assets/avatars/hultoko.png",
  "@cartermckelvain": "assets/avatars/CarterMcKelvain.jpg",
  "@c4ch3c4d3":       "assets/avatars/c4ch3c4d3.jpg",
  "@blaisebits":      "assets/avatars/BlaiseBits.jpg",
  "@binaryz0ne":      "assets/avatars/binaryz0ne.jpg",
  "@unsigned_sh0rt":  "assets/avatars/unsigned-sh0rt.jpg",
  "@_euzebius":       "assets/avatars/_Euzebius.jpg",
  "@onecloudemoji":   "assets/avatars/OneCloudEmoji.jpg",
  "@lpha3ch0":        "assets/avatars/lpha3ch0.jpg",
  "@czeebzee":        "assets/avatars/CzeeBzee.jpg",
  "@syndrowm":        "assets/avatars/syndrowm.jpg",
  "@n7wera":          "assets/avatars/n7wera.jpg",
  "@bohops":          "assets/avatars/bohops.jpg",
  "@jessefmoore":     "assets/avatars/jessefmoore.jpg",
  "@dsec_net":        "assets/avatars/dsec-net.jpg",
  "@gzobrajn":        "assets/avatars/gzobraJn.jpg",
};

// Twitter snowflake → creation date. Status IDs exceed Number.MAX_SAFE_INTEGER,
// so parse as BigInt first, shift off the worker/sequence bits, then add the
// Twitter epoch (Nov 4, 2010 UTC) and format.
const tweetDateFromUrl = (url) => {
  try {
    const m = url && url.match(/status\/(\d+)/);
    if (!m) return "";
    const TWITTER_EPOCH = 1288834974657;
    const ts = Number(BigInt(m[1]) >> 22n) + TWITTER_EPOCH;
    const d = new Date(ts);
    return d.toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric" });
  } catch (e) {
    return "";
  }
};

// X / Twitter bird-then-X mark — inline SVG so we never depend on
// remote assets. Stroke weight tuned to match X's current app mark.
const XMark = ({ size = 20 }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
    <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
  </svg>
);

// Render a tweet body with selected phrases shifted into the deck's
// gold accent color with a soft text-shadow glow when the card is
// the top of the stack. Matches the "problem crimson / solution gold"
// emphasis vocabulary used on every other slide — no extra shapes
// introduced, the highlight IS just the deck's accent treatment.
const renderTweetBody = (body, highlights, active) => {
  if (!highlights || !highlights.length) return body;
  const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const re = new RegExp("(" + highlights.map(escape).join("|") + ")", "g");
  const parts = body.split(re);
  const matches = new Set(highlights);
  return parts.map((part, i) => {
    if (matches.has(part)) {
      return (
        <span key={i} style={{
          // font-weight is discrete and snaps — don't transition it;
          // color + text-shadow fade cover the motion.
          color: active ? "#c5ae4f" : "inherit",
          textShadow: active ? "0 0 14px rgba(197,174,79,0.45)" : "none",
          fontWeight: 600,
          transition: "color 380ms ease 180ms, text-shadow 380ms ease 180ms",
        }}>{part}</span>
      );
    }
    return <React.Fragment key={i}>{part}</React.Fragment>;
  });
};

// A single tweet card — HTML reconstruction of the X post surface.
// Styled with X's dark-mode palette (neutral #000 bg, Chirp-like
// system-ui stack) so it reads as "a real tweet" despite living
// inside a JetBrains-Mono deck.
const TweetCard = ({ tweet, isActive = false, onOpen }) => {
  const avatarSrc = AVATAR_MAP[tweet.handle.toLowerCase()];
  // Track missing-file case so an onError swap reverts to the initial
  // circle without leaving a broken-image icon on the slide.
  const [avatarFailed, setAvatarFailed] = React.useState(false);
  const showPhoto = avatarSrc && !avatarFailed;
  return (
  <article
    onClick={onOpen}
    style={{
      width: 620, boxSizing: "border-box",
      padding: "22px 24px 20px",
      background: "#000",
      border: "1px solid rgba(239,243,244,0.2)",
      borderRadius: 18,
      fontFamily: `-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif`,
      color: "#e7e9ea",
      boxShadow: "0 24px 60px rgba(0,0,0,0.55), 0 0 0 1px rgba(197,174,79,0.08)",
      cursor: "pointer",
      userSelect: "none",
    }}
  >
    <header style={{display:"flex", alignItems:"flex-start", gap: 12}}>
      {showPhoto ? (
        <img
          src={avatarSrc}
          alt={tweet.name}
          onError={() => setAvatarFailed(true)}
          style={{
            width: 48, height: 48, borderRadius: "50%",
            objectFit: "cover", flexShrink: 0,
            background: tweet.avatarBg,
          }}
        />
      ) : (
        <div style={{
          width: 48, height: 48, borderRadius: "50%",
          background: tweet.avatarBg, color: "#fff", flexShrink: 0,
          display: "flex", alignItems: "center", justifyContent: "center",
          fontSize: 22, fontWeight: 700, letterSpacing: "-0.02em",
        }}>{tweet.initial}</div>
      )}
      <div style={{flex: 1, minWidth: 0}}>
        <div style={{
          fontWeight: 700, fontSize: 17, color: "#e7e9ea",
          lineHeight: 1.2, overflow: "hidden", textOverflow: "ellipsis",
        }}>{tweet.name}</div>
        <div style={{color: "#71767b", fontSize: 15, marginTop: 2}}>{tweet.handle}</div>
      </div>
      <div style={{color: "#e7e9ea", flexShrink: 0, paddingTop: 2}}>
        <XMark size={22}/>
      </div>
    </header>

    <div style={{
      marginTop: 14,
      fontSize: 21, lineHeight: 1.4, color: "#e7e9ea",
      fontWeight: 400, whiteSpace: "pre-wrap",
    }}>{renderTweetBody(tweet.body, tweet.highlights, isActive)}</div>

    <footer style={{
      marginTop: 18, paddingTop: 14,
      borderTop: "1px solid rgba(239,243,244,0.12)",
      display: "flex", justifyContent: "space-between", alignItems: "center",
      fontSize: 14, color: "#71767b",
    }}>
      <span>{tweetDateFromUrl(tweet.url) || "on X"}{tweetDateFromUrl(tweet.url) ? " · on X" : ""}</span>
      <span style={{color: "#1d9bf0", fontWeight: 500}}>Open ↗</span>
    </footer>
  </article>
  );
};

function Slide21_Love() {
  const [active, setActive] = React.useState(0);
  const { slideNo } = React.useContext(DeckCtx);
  const n = TWEET_DATA.length;

  const advance = React.useCallback(() => {
    setActive((i) => (i + 1) % n);
  }, [n]);
  const retreat = React.useCallback(() => {
    setActive((i) => (i - 1 + n) % n);
  }, [n]);

  // Arrow keys cycle the deck — only when this slide is active.
  // Same guard pattern as Slide20_OtherFeatures: read the deck-stage
  // index from the DOM so the binding tracks reordering.
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
      if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
      const stage = document.querySelector("deck-stage");
      if (!stage || stage.index !== slideNo - 1) return;
      e.preventDefault();
      e.stopPropagation();
      if (e.key === "ArrowDown") advance();
      else retreat();
    };
    window.addEventListener("keydown", onKey, true);
    return () => window.removeEventListener("keydown", onKey, true);
  }, [advance, retreat, slideNo]);

  const openActive = () => {
    const url = TWEET_DATA[active].url;
    if (url) window.open(url, "_blank", "noopener,noreferrer");
  };

  return (
    <Slide section="SIGNAL">
      <Content>
        <div style={{display:"flex", alignItems:"baseline", justifyContent:"space-between"}}>
          <div>
            <NumberedEyebrow>FIELD REPORTS</NumberedEyebrow>
            <Title style={{marginTop: 16}}>Love for Ludus</Title>
            <div style={{fontSize: TS.body, color: DK_FG_MUTED, marginTop: 14, maxWidth: 1100, lineHeight: 1.45}}>
              Unsolicited. Real posts from operators who use it.
            </div>
          </div>
          <div style={{display:"flex", gap: 26, alignItems:"center"}}>
            <div style={{textAlign:"right", borderLeft:`1px solid ${DK_BORDER}`, paddingLeft: 26}}>
              <div style={{fontSize: 12, letterSpacing:"0.3em", color: DK_FG_MUTED, fontWeight: 600}}>DECK</div>
              <div style={{fontSize: 22, color: DK_FG, marginTop: 6, fontWeight: 600}}>
                {String(active+1).padStart(2,"0")} <span style={{color: DK_FG_MUTED}}>/ {String(n).padStart(2,"0")}</span>
              </div>
            </div>
          </div>
        </div>

        {/* Stack stage — the ↑/↓ controls flank the card directly so
            they read as part of the tweet deck, not a footer chrome. */}
        <div style={{
          flex: 1, marginTop: 24, minHeight: 0,
          display: "flex", alignItems: "center", justifyContent: "center",
          gap: 22, position: "relative",
        }}>
          <div style={{position: "relative", width: 620, height: 340}}>
            {TWEET_DATA.map((t, i) => {
              const offset = (i - active + n) % n;
              const isTop = offset === 0;
              const visibleDepth = 4; // how many back cards peek out
              const visible = offset < visibleDepth;
              // Back cards fan out to the right with a slight rotation.
              const tx = offset * 16;
              const ty = offset * 10;
              const rot = offset * 2;
              const scale = 1 - offset * 0.04;
              return (
                <div
                  key={t.handle}
                  style={{
                    position: "absolute",
                    top: 0, left: 0,
                    transform: `translate(${tx}px, ${ty}px) rotate(${rot}deg) scale(${scale})`,
                    transformOrigin: "left center",
                    zIndex: n - offset,
                    opacity: visible ? 1 - offset * 0.15 : 0,
                    pointerEvents: isTop ? "auto" : "none",
                    transition: "transform 380ms cubic-bezier(.2,.8,.2,1), opacity 380ms ease",
                    filter: isTop ? "none" : "saturate(0.9)",
                  }}
                  onClick={isTop ? (e) => { e.stopPropagation(); openActive(); } : undefined}
                >
                  <TweetCard tweet={t} isActive={isTop}/>
                </div>
              );
            })}
          </div>

          {/* Inline ▲/▼ controls — paired with the card, matched to the
              ArrowUp / ArrowDown keybindings. */}
          <div style={{display: "flex", flexDirection: "column", gap: 10}}>
            <button
              onClick={(e) => { e.stopPropagation(); retreat(); }}
              aria-label="Previous tweet"
              style={{
                width: 52, height: 52,
                border: `1px solid ${GOLD}`, background: "rgba(197,174,79,0.08)",
                color: GOLD, cursor: "pointer",
                display: "flex", alignItems: "center", justifyContent: "center",
              }}
            >
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 14l6-6 6 6"/></svg>
            </button>
            <button
              onClick={(e) => { e.stopPropagation(); advance(); }}
              aria-label="Next tweet"
              style={{
                width: 52, height: 52,
                border: `1px solid ${GOLD}`, background: "rgba(197,174,79,0.08)",
                color: GOLD, cursor: "pointer",
                display: "flex", alignItems: "center", justifyContent: "center",
              }}
            >
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 10l6 6 6-6"/></svg>
            </button>
          </div>
        </div>

        {/* Minimal hint — keyboard + click to open */}
        <div style={{
          marginTop: 14, textAlign: "center",
          fontSize: 13, letterSpacing: "0.22em", color: DK_FG_MUTED, fontWeight: 600,
        }}>
          ↑ ↓ CYCLE · CLICK CARD TO OPEN ON X
        </div>
      </Content>
    </Slide>
  );
}

// ─── SLIDE 21 · OSS vs ENTERPRISE ─────────────────────────────
// ─── SLIDE 22 · WHO'S RUNNING LUDUS ─────────────────────────────
// Organizations where Ludus operators work — INDIVIDUAL PRACTITIONERS, not
// corporate partnerships. Disclaimer under the marquee makes this explicit so
// the strip isn't read as a customer-logo wall. Ordered to interleave sectors
// (enterprise / gov / edu / cyber / training) for visual diversity at every
// scroll position.
// Curated to wordmarks that survive monochrome normalization. Square seals
// (USCG/USAF/USAA/NUQ), icon-dominant SVGs (Elastic is 1:1 with tiny caption),
// and logos with gradient/fill-none artifacts (Abbott 2025, PwC momentum mark)
// were cut — they render as white blobs or rectangles under the filter.
// Also dropped USF since it reads as a duplicate of UCF at scroll speed.
const OPERATOR_LOGOS = [
  { file: "sophos.svg",               name: "Sophos" },
  { file: "mitre.svg",                name: "MITRE" },
  { file: "specterops.svg",           name: "SpecterOps" },
  { file: "hackthebox.png",           name: "HackTheBox" },
  { file: "accenture.svg",            name: "Accenture" },
  { file: "abbott.svg",               name: "Abbott" },
  { file: "mandiant.png",             name: "Mandiant" },
  { file: "elastic.svg",              name: "Elastic" },
  { file: "cybercx.png",              name: "CyberCX" },
  { file: "ut-dallas.svg",            name: "UT Dallas" },
  { file: "bishop-fox.svg",           name: "Bishop Fox" },
  { file: "coalfire.svg",             name: "Coalfire" },
  { file: "usf.svg",                  name: "University of South Florida" },
  { file: "ucf.svg",                  name: "University of Central Florida", height: 64 },
  { file: "verizon.svg",              name: "Verizon" },
  { file: "tesco.svg",                name: "Tesco" },
  { file: "pentera.svg",              name: "Pentera" },
  { file: "booz-allen.svg",           name: "Booz Allen Hamilton" },
  { file: "rit.svg",                  name: "Rochester Institute of Technology" },
  { file: "airbus.svg",               name: "Airbus" },
  { file: "guidepoint.svg",           name: "GuidePoint Security" },
  { file: "intesa-sanpaolo.svg",      name: "Intesa Sanpaolo" },
  { file: "nviso.svg",                name: "NVISO" },
  { file: "pwc.svg",                  name: "PwC" },
  { file: "tines.svg",                name: "Tines" },
  { file: "mufg.svg",                 name: "Mitsubishi UFJ" },
  { file: "mdsec.svg",                name: "MDSec" },
  { file: "truesec.svg",              name: "Truesec" },
  { file: "fox-it.png",               name: "Fox-IT" },
  { file: "pwned-labs.svg",           name: "Pwned Labs" },
  { file: "oteria.png",               name: "Oteria Cyber School" },
  { file: "usaa.svg",                 name: "USAA" },
  { file: "seb.svg",                  name: "SEB" },
  { file: "usaf.svg",                 name: "U.S. Air Force", height: 28 },
  { file: "uscg.svg",                 name: "U.S. Coast Guard" },
  { file: "levelblue.svg",            name: "LevelBlue" },
];

function Slide22_Customers() {
  const segments = [
    {
      id: "01",
      tag: "FORTUNE 100",
      title: "Enterprises",
      items: [
        "Internal red teams",
        "Threat emulation",
        "Detection engineering",
      ],
    },
    {
      id: "02",
      tag: "U.S. FEDERAL",
      title: "Government Agencies",
      items: [
        "OCO operators",
        "Threat intelligence",
        "On-prem, behind the firewall",
      ],
    },
    {
      id: "03",
      tag: "R1 RESEARCH",
      title: "Large Public Universities",
      items: [
        "Cybersecurity curricula",
        "Research labs",
        "CTF infrastructure",
      ],
    },
    {
      id: "04",
      tag: "COMMERCIAL & NON-PROFIT",
      title: "Training Providers",
      items: [
        "Hands-on lab environments",
        "Per-student range isolation",
        "Non-profits and commercial courses",
      ],
    },
  ];

  return (
    <Slide section="TRACTION">
      <Content>
        <div>
          <NumberedEyebrow>DEPLOYED WHERE</NumberedEyebrow>
          <Title style={{marginTop: 16}}>Who's running <span style={{color: GOLD}}>Ludus</span></Title>
        </div>

        {/* 2x2 grid — compacted to reserve vertical space for the operator
            marquee below without squeezing it off-slide. */}
        <div style={{
          flex: 1, marginTop: 24, minHeight: 0,
          display:"grid", gridTemplateColumns: "1fr 1fr", gridTemplateRows: "1fr 1fr",
          gap: 20,
        }}>
          {segments.map((s, i) => (
            <div key={i} style={{
              position:"relative",
              border: `1px solid ${DK_BORDER}`,
              padding: "22px 28px",
              display:"flex", flexDirection:"column",
            }}>
              <Brackets size={10} color={GOLD} inset={-1} thickness={1.2}/>

              {/* Tag */}
              <div style={{
                display:"flex", alignItems:"center", gap: 14,
                fontSize: 11, letterSpacing:"0.3em",
              }}>
                <span style={{
                  color: GOLD, fontWeight: 700,
                  padding:"3px 8px",
                  border: `1px solid ${GOLD}`,
                }}>{s.id}</span>
                <span style={{color: GOLD, fontWeight: 700}}>{s.tag}</span>
              </div>

              {/* Title */}
              <div style={{
                fontSize: 36, fontWeight: 700, color: DK_FG,
                lineHeight: 1.05, letterSpacing: "-0.01em",
                marginTop: 16,
              }}>{s.title}</div>

              {/* Activities */}
              <div style={{
                marginTop: 18, flex: 1,
                display:"flex", flexDirection:"column", gap: 2,
              }}>
                {s.items.map((item, j) => (
                  <div key={j} style={{
                    display:"flex", alignItems:"center", gap: 16,
                    padding:"6px 0",
                  }}>
                    <span style={{
                      fontFamily:"'JetBrains Mono', monospace",
                      fontSize: 12, color: GOLD, fontWeight: 700,
                      width: 30,
                    }}>.0{j+1}</span>
                    <span style={{fontSize: 18, color: DK_FG, fontWeight: 500}}>{item}</span>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>

        {/* Operator marquee — continuous-scroll strip of orgs where Ludus users
            actually work. See OPERATOR_LOGOS comment above for framing. */}
        <style>{`
          @keyframes ludus-operator-marquee {
            from { transform: translateX(0); }
            to   { transform: translateX(-33.3333%); }
          }
        `}</style>
        <div style={{marginTop: 20, display:"flex", flexDirection:"column", gap: 14}}>
          <div style={{
            display:"flex", alignItems:"center", gap: 16,
            fontSize: 11, letterSpacing:"0.3em", textTransform:"uppercase",
          }}>
            <div style={{flex:1, height:1, background: DK_BORDER}}/>
            <span style={{color: GOLD, fontWeight: 700}}>// Trusted by operators at</span>
            <div style={{flex:1, height:1, background: DK_BORDER}}/>
          </div>

          <div style={{
            overflow:"hidden", position:"relative", height: 60,
            maskImage:"linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent)",
            WebkitMaskImage:"linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent)",
          }}>
            {/* No maxWidth — wide wordmarks render at their natural width for
                the visual rhythm of a real 'trusted by' strip. All logos share
                the same height so the baseline stays consistent. */}
            <div style={{
              display:"flex", alignItems:"center", gap: 80,
              width:"max-content", height:"100%",
              animation: "ludus-operator-marquee 110s linear infinite",
            }}>
              {[...OPERATOR_LOGOS, ...OPERATOR_LOGOS, ...OPERATOR_LOGOS].map((l, i) => (
                <img
                  key={i}
                  src={`assets/logos/${l.file}`}
                  alt={l.name}
                  title={l.name}
                  style={{
                    // Two-line stacked wordmarks (USF, UCF) need extra height
                    // so their per-line letter size matches single-line logos.
                    height: l.height || 48, width: "auto",
                    objectFit: "contain", flexShrink: 0,
                    filter: "brightness(0) invert(1)",
                    opacity: 0.72,
                  }}
                />
              ))}
            </div>
          </div>
        </div>
      </Content>
    </Slide>
  );
}

// ─── SLIDE 23 · EDITIONS ─────────────────────────────────
function Slide23_Comparison() {
  // Plan columns — mirrors ludus.cloud/pricing. Pro NFR is surfaced as a
  // footnote (niche audience, keeps the table readable at deck width).
  const plans = [
    { key: "oss",   name: "Open Source",                          subtitle: "Community edition",    price: "$0",     cadence: "forever",         paid: false },
    { key: "pro",   name: "Pro",                                  subtitle: "For professionals",    price: "$3,500", cadence: "per seat / year", paid: true  },
    { key: "entSH", name: "Enterprise", sub2: "Self-Hosted",      subtitle: "Self-hosted at scale", price: "Custom", cadence: "annual contract", paid: true  },
    { key: "entFH", name: "Enterprise", sub2: "Fully Hosted",     subtitle: "We manage it for you", price: "Custom", cadence: "annual contract", paid: true  },
  ];

  // Cell values: "✓" included, "*" included (closed-source plugins),
  // "-" not in plan, "dev" on roadmap. Keys match plans[].key.
  const groups = [
    {
      label: "CORE",
      rows: [
        { label: "CLI-driven automation",          oss:"✓", pro:"✓", entSH:"✓", entFH:"✓" },
        { label: "Community role library",         oss:"✓", pro:"✓", entSH:"✓", entFH:"✓" },
        { label: "Open-source",                    oss:"✓", pro:"*", entSH:"*", entFH:"*" },
      ],
    },
    {
      label: "PRO",
      rows: [
        { label: "Web UI for visual range design",          oss:"-", pro:"✓", entSH:"✓", entFH:"✓" },
        { label: "Router roles for turnkey networking",     oss:"-", pro:"✓", entSH:"✓", entFH:"✓" },
        { label: "Pro Roles Catalog",                       oss:"-", pro:"✓", entSH:"✓", entFH:"✓" },
      ],
    },
    {
      label: "SUPPORT",
      rows: [
        { label: "Community support",                       oss:"✓", pro:"✓", entSH:"✓", entFH:"✓" },
        { label: "Priority support",                        oss:"-", pro:"✓", entSH:"✓", entFH:"✓" },
        { label: "SLA-backed support",                      oss:"-", pro:"-", entSH:"✓", entFH:"✓" },
      ],
    },
    {
      label: "ENTERPRISE",
      rows: [
        { label: "Enterprise Roles Catalog (e.g. Zeek)",    oss:"-", pro:"-", entSH:"✓", entFH:"✓" },
        { label: "Outbound WireGuard",                      oss:"-", pro:"-", entSH:"✓", entFH:"✓" },
        { label: "Windows licensing via KMS",               oss:"-", pro:"-", entSH:"✓", entFH:"✓" },
        { label: "CTFd integration",                        oss:"-", pro:"-", entSH:"✓", entFH:"✓" },
        { label: "Fully managed infrastructure & deployment", oss:"-", pro:"-", entSH:"-", entFH:"✓" },
      ],
    },
  ];

  const Mark = ({ v }) => {
    if (v === "✓")   return <span style={{color: GOLD, fontSize: 24}}>✓</span>;
    if (v === "*")   return <span style={{color: GOLD, fontSize: 24, whiteSpace:"nowrap"}}>✓<sup style={{fontSize: 14, marginLeft: 1}}>*</sup></span>;
    if (v === "dev") return <span style={{fontSize: 13, letterSpacing:"0.15em", color:"#d2c472", fontWeight: 700}}>IN DEV</span>;
    return <span style={{color: "rgba(155,169,180,0.45)", fontSize: 24}}>—</span>;
  };

  const totalRows = groups.reduce((n,g)=>n+g.rows.length, 0);
  const gridCols = "2fr 1fr 1fr 1fr 1fr";

  return (
    <Slide section="OFFERING">
      <Content>
        <div style={{display:"flex", alignItems:"baseline", justifyContent:"space-between"}}>
          <div>
            <NumberedEyebrow>EDITIONS</NumberedEyebrow>
            <Title style={{marginTop: 10}} size={48}>Plans &amp; pricing</Title>
          </div>
          <div style={{fontSize: 14, letterSpacing:"0.3em", color: GOLD, textAlign:"right", fontWeight: 700}}>
            SAME CORE ENGINE
          </div>
        </div>

        <div style={{marginTop: 14, display:"grid", gridTemplateColumns: gridCols, gridAutoRows: "min-content", border: `1px solid ${DK_BORDER}`}}>
          {/* Plan header row */}
          <div style={{padding:"10px 20px", borderBottom:`1px solid ${DK_BORDER}`, borderRight:`1px solid ${DK_BORDER}`, background:"rgba(25,28,34,0.7)", display:"flex", alignItems:"flex-end"}}>
            <div style={{fontSize: 13, letterSpacing:"0.3em", color: DK_FG_MUTED}}>CAPABILITY</div>
          </div>
          {plans.map((p, pi) => {
            const isLast = pi === plans.length - 1;
            return (
              <div key={p.key} style={{
                padding:"10px 14px 8px",
                borderBottom: p.paid ? `1px solid ${GOLD}` : `1px solid ${DK_BORDER}`,
                borderRight: isLast ? "none" : `1px solid ${DK_BORDER}`,
                background: p.paid ? "rgba(197,174,79,0.08)" : "rgba(25,28,34,0.7)",
                textAlign:"center",
              }}>
                <div style={{fontSize: 17, fontWeight: 700, color: p.paid ? GOLD : DK_FG, lineHeight: 1.1}}>
                  {p.name}{p.sub2 ? <><br/>{p.sub2}</> : null}
                </div>
                <div style={{fontSize: 14, color: DK_FG_MUTED, marginTop: 4, letterSpacing:"0.08em"}}>{p.subtitle}</div>
                <div style={{fontSize: 19, fontWeight: 700, color: DK_FG, marginTop: 5, letterSpacing:"-0.01em"}}>{p.price}</div>
                <div style={{fontSize: 14, color: DK_FG_MUTED, marginTop: 2, letterSpacing:"0.08em"}}>{p.cadence}</div>
              </div>
            );
          })}

          {groups.map((g, gi) => (
            <React.Fragment key={gi}>
              {/* Group divider spanning all 5 cols */}
              <div style={{
                gridColumn: "1 / -1",
                padding: "5px 20px",
                borderTop: gi === 0 ? "none" : `1px solid ${DK_BORDER}`,
                borderBottom: `1px solid ${DK_BORDER}`,
                background: "#0d1117",
                fontSize: 14, letterSpacing:"0.35em", color: GOLD, fontWeight: 700,
              }}>
                {g.label}
              </div>
              {g.rows.map((r, i) => {
                const last = gi === groups.length - 1 && i === g.rows.length - 1;
                return (
                  <React.Fragment key={i}>
                    <div style={{
                      padding:"2px 20px",
                      borderBottom: last ? "none" : `1px solid ${DK_BORDER_SOFT}`,
                      borderRight: `1px solid ${DK_BORDER}`,
                      fontSize: 15, color: DK_FG,
                      display:"flex", alignItems:"center",
                    }}>{r.label}</div>
                    {plans.map((p, pi) => {
                      const isLastCol = pi === plans.length - 1;
                      return (
                        <div key={p.key} style={{
                          padding:"2px 12px",
                          borderBottom: last ? "none" : `1px solid ${DK_BORDER_SOFT}`,
                          borderRight: isLastCol ? "none" : `1px solid ${DK_BORDER}`,
                          textAlign:"center",
                          background: p.paid ? "rgba(197,174,79,0.05)" : "transparent",
                          display:"flex", alignItems:"center", justifyContent:"center",
                        }}><Mark v={r[p.key]}/></div>
                      );
                    })}
                  </React.Fragment>
                );
              })}
            </React.Fragment>
          ))}
        </div>

        <div style={{marginTop: 8, display:"flex", justifyContent:"space-between", alignItems:"center", gap: 24}}>
          <div style={{display:"flex", gap: 18, fontSize: 14, letterSpacing:"0.1em", color: DK_FG_MUTED, flexWrap:"wrap", alignItems:"center", fontWeight: 500}}>
            <span><span style={{color: GOLD, fontWeight: 700}}>✓</span> Included</span>
            <span style={{whiteSpace:"nowrap"}}><span style={{color: GOLD, fontWeight: 700}}>✓<sup style={{fontSize: 10, marginLeft: 1}}>*</sup></span> Open core (OSS + closed-source catalogs)</span>
            <span><span style={{color:"rgba(155,169,180,0.6)"}}>—</span> Not available</span>
          </div>
          <div style={{fontSize: 14, letterSpacing:"0.2em", color: GOLD, whiteSpace:"nowrap"}}>
            → ludus.cloud / pricing
          </div>
        </div>
      </Content>
    </Slide>
  );
}

Object.assign(window, {
  Slide16_AI, Slide17_ThreatEm, Slide18_ModelGym, Slide19_ThreatIntel,
  Slide20_OtherFeatures, Slide21_Love, Slide22_Customers, Slide23_Comparison,
});
