// Shared content, hooks, and the Klaviyo Flow Simulation component.

const CONTENT = {
  name: "Kalen Jordan",
  business: "Kalen Jordan",
  email: "me@kalenjordan.com",
  tagline: "I build commerce tools in public.",
  subTagline: "A commerce engineer shipping open-source tools, apps, and AI-assisted automation for the Shopify ecosystem — out loud, almost daily.",
  social: {
    github: "https://github.com/kalenjordan",
    x: "https://x.com/kalenjordan",
    linkedin: "https://www.linkedin.com/in/kalen",
  },
  stats: [
    ["13+", "years in commerce engineering"],
    ["~500", "GitHub stars across open source"],
    ["30+", "Talking Shop podcast episodes"],
  ],
  services: [
    {
      id: "klaviyo",
      num: "01",
      name: "Klaviyo Flow Optimization",
      short: "Audit, rebuild, and tune the flows that actually move revenue.",
      body: [
        "I spent a decade building an email marketing platform (acquired), so the mental model for how flows, segments, and deliverability actually work is second nature. Recently I ran a small experiment auditing a handful of Klaviyo accounts and the patterns were consistent: abandon series firing at 2am, welcome flows with no brand voice, filters that quietly exclude the people you want to reach, metrics installed but never wired to anything.",
        "The fix isn't more emails. It's fewer, sharper, better-timed ones. I go deep into your account, find the leaks, and rebuild the core flows — welcome, browse abandon, abandoned cart, post-purchase, win-back — around what your data actually says.",
      ],
      deliverables: [
        "Full account audit with a prioritized leak list",
        "Flow rebuilds (welcome, browse, cart, post-purchase, win-back)",
        "Segment & filter cleanup",
        "A/B test plan and reporting",
        "Handoff docs so your team can maintain it",
      ],
    },
    {
      id: "outbound",
      num: "02",
      name: "AI Outbound for SaaS",
      short: "Outbound that looks at the actual product, not just firmographic glitter.",
      body: [
        "A lot of SaaS outbound still feels like: “Hey {first_name}, noticed you’re in ecommerce…” Which is technically personalization in the same way ketchup is technically a vegetable.",
        "The more interesting angle is going deeper into the actual website footprint. For an ecommerce search app, for example: are they handling typos? Do Spanish searches work? Are “under $50” searches understood? Is autocomplete helping or just quietly vibing in the corner?",
        "That gives you a much better reason to reach out than “I saw your company exists.” The models are finally good enough for this to get interesting. Not spray and pray with slightly nicer copy. More like: inspect the site, identify the weak spot, explain the opportunity clearly, and make the outreach feel like someone actually looked.",
      ],
      deliverables: [
        "Custom site-inspection pipeline for your product",
        "Signal → message mapping specific to your ICP",
        "LLM prompts tuned to your voice",
        "Sending stack setup (Smartlead, Instantly, or custom)",
        "Iteration loop based on reply data",
      ],
    },
    {
      id: "automation",
      num: "03",
      name: "Automation Consulting",
      short: "Take the tedious work out of your operators' day.",
      body: [
        "Every team has 5–10 workflows that should have been automated two years ago. Reports pulled by hand. Data copy-pasted between tools. Alerts that should have fired but didn’t. I find them and kill them.",
      ],
      deliverables: [
        "Workflow discovery + prioritization",
        "Implementation in Zapier, n8n, Make, or custom code",
        "Monitoring so you know when things break",
      ],
    },
    {
      id: "ai",
      num: "04",
      name: "AI Strategy",
      short: "Figure out where AI actually earns its keep in your business.",
      body: [
        "Half hype, half real. I help you sort which. Short engagement, concrete output: a list of the 3–5 places AI is worth building against, what it would look like, and what it would cost.",
      ],
      deliverables: [
        "Landscape audit against your ops",
        "Opportunity ranking with rough ROI",
        "Build-vs-buy recommendations",
        "Optional: prototype of the top candidate",
      ],
    },
    {
      id: "apps",
      num: "05",
      name: "Custom App Development",
      short: "When no off-the-shelf tool fits, build the thin thing that does.",
      body: [
        "Usually a narrow internal tool, a Shopify app, a Klaviyo integration, or a workflow that glues together systems that don’t talk. Small surface area, high leverage.",
      ],
      deliverables: [
        "Spec + scope",
        "Build (Node, TypeScript, Postgres, etc.)",
        "Deploy + monitoring",
        "Docs + handoff",
      ],
    },
  ],
};

// Portfolio projects — transcribed from career-workspace/career/projects/*.md.
// `featured: true` projects get a /work/{id}.html case-study page and appear in
// the homepage "Selected work" list. Others are index-only (external links).
const PROJECTS = [
  {
    id: "b2b-accelerator",
    num: "12",
    name: "B2B Accelerator",
    year: "2025",
    era: "shopify",
    status: "In progress",
    oneLiner: "A reusable B2B app / accelerator for Shopify, reworked from custom client builds.",
    stack: ["Shopify", "B2B"],
    links: { tweet: "https://x.com/kalenjordan/status/1960779372144537894" },
    body: [
      "Reworks custom B2B client builds into a reusable app/accelerator for Shopify, spinning off feature work including bulk AI product content — productizing repeated B2B client needs.",
    ],
    skills: ["Shopify B2B", "Turning bespoke client work into products", "Product strategy for an accelerator model"],
    featured: false,
  },
  {
    id: "netsuite-patchworks",
    num: "13",
    name: "NetSuite × Patchworks",
    year: "2025",
    era: "shopify",
    status: "Client project · shipped",
    oneLiner: "Order, product, and inventory sync between Shopify and NetSuite for a medical-supply wholesaler, built on the Patchworks iPaaS.",
    stack: ["NetSuite", "Patchworks", "Shopify", "iPaaS"],
    links: {},
    body: [
      "Integrated a medical-supply wholesaler's Shopify store with NetSuite ERP using the Patchworks integration platform — syncing orders, products, inventory, pricing, and customer records with error handling and repeatable business logic.",
    ],
    skills: ["NetSuite integration", "iPaaS / Patchworks", "ERP ↔ ecommerce data sync"],
    featured: false,
  },
  {
    id: "netsuite-node",
    num: "14",
    name: "NetSuite × Node",
    year: "2025",
    era: "shopify",
    status: "Client project · shipped",
    oneLiner: "Custom Node integration syncing a furniture wholesaler with NetSuite — replaced a Workato build and ran far better.",
    stack: ["NetSuite", "Node.js", "Workato", "Shopify"],
    links: {},
    body: [
      "Built the NetSuite integration for a furniture wholesaler. It started on Workato, but I replaced that with a custom Node.js API integration that proved more reliable, flexible, and maintainable — full control over sync logic, retries, and error handling.",
    ],
    skills: ["NetSuite integration", "Custom API integration (Node.js)", "Workato", "ERP ↔ ecommerce data sync"],
    featured: false,
  },
  {
    id: "spacex-redesign",
    num: "17",
    name: "SpaceX Store Redesign",
    year: "2026",
    era: "shopify",
    status: "Demo · shipped",
    oneLiner: "A from-scratch redesign of the SpaceX online store, built fast with Claude auto mode and the Shopify MCP — plus a companion importer that loads the real SpaceX catalog.",
    stack: ["Shopify theme (Liquid)", "Claude (auto mode)", "Shopify MCP", "Admin GraphQL"],
    links: { tweet: "https://x.com/kalenjordan/status/2066329152660136447" },
    media: { kind: "image", src: "/img/spacex-redesign.jpg", label: "screenshot" },
    body: [
      "A full redesign of the @SpaceX online store, done as a build-in-public demo to celebrate the IPO. The striking part was the speed — driven largely by Claude's auto mode against the Shopify MCP, so the model could read and write the theme directly.",
      "Paired with a separate React Router app that imports the public SpaceX catalog into a Shopify store through the Admin GraphQL API — products, options, variants, prices, SKUs, weights, tags, and images — so the redesigned storefront runs on real data.",
    ],
    skills: ["AI-driven theme development (Claude auto mode + Shopify MCP)", "Shopify theme design & Liquid", "Catalog import via Admin GraphQL", "Building in public / fast turnaround demos"],
    featured: true,
  },
  {
    id: "metasync",
    num: "01",
    name: "Metasync",
    year: "2025",
    era: "shopify",
    status: "Open source · ~48★ · v2 active",
    oneLiner: "Store-to-store sync for Shopify — metaobject definitions and data, metafields, products, and theme files.",
    stack: ["Node.js", "Shopify Admin GraphQL", "SQLite"],
    links: { github: "https://github.com/kalenjordan/metasync", tweet: "https://x.com/kalenjordan/status/1956722925622509988" },
    body: [
      "Copies configuration and data between Shopify stores: metaobject definitions and entries, metafields, products, and theme files. It was born from needing to stand up a dev store quickly, then took on a life of its own.",
      "v2 re-architected the whole thing around batched processing, a SQLite database for fast source-to-target ID mapping, and deferred resolution of inter-object dependencies. It's now my most-starred recent project (~48★), used for dev-store setup on client projects and ongoing prod→staging syncs — solving a long-standing ecosystem pain.",
    ],
    skills: ["Complex data sync with dependency ordering", "SQLite for cross-system state mapping", "Batched, throughput-aware processing", "Deep metafields / metaobjects / PIM modeling", "Shopify Admin GraphQL at scale"],
    featured: true,
  },
  {
    id: "shopworker",
    num: "02",
    name: "Shopworker",
    year: "2025",
    era: "shopify",
    status: "Open source · ~20★ · active",
    oneLiner: "Webhook-triggered Shopify jobs running on Cloudflare Workers — designed to be one- or few-shot prompted from Cursor or Claude.",
    stack: ["Cloudflare Workers", "Durable Objects & Queues", "Shopify Admin GraphQL", "Node.js", "CLI"],
    links: { site: "https://shopworker.dev", github: "https://github.com/kalenjordan/shopworker", tweet: "https://x.com/kalenjordan/status/1919794381596364957" },
    body: [
      "Lets you stand up jobs that trigger from Shopify webhooks and run on Cloudflare Workers. It's built to be driven by AI coding tools — you prompt a use case and it scaffolds the job. Ships with a CLI to run tests manually and to enable or disable webhooks, plus a Google Sheets connector, durable-object-backed queuing, and a CSV email-attachment order importer.",
      "It became the engine behind essentially all of my client automation work. One demo: I two-shot-prompted a job that triggers on order creation, calls an external API, and pipes a field from the response into an invoice-send mutation.",
    ],
    skills: ["Event-driven architecture (webhooks → queued jobs)", "Cloudflare Workers / Durable Objects / Queues", "Shopify Admin GraphQL mutations", "CLI design; AI-promptable codebase design", "Open-source maintainership"],
    featured: true,
  },
  {
    id: "flowassist",
    num: "03",
    name: "FlowAssist",
    year: "2024",
    era: "shopify",
    status: "Shipped · active",
    oneLiner: "Q&A bot and service for Shopify Flow — ask questions, get answers, keep a running library of flow ideas.",
    stack: ["OpenAI Assistants API", "Cloudflare AutoRAG", "Shopify Flow"],
    links: { tweet: "https://x.com/kalenjordan/status/1872391899203432579" },
    body: [
      "Answers questions about Shopify Flow and maintains a library of flow ideas and templates.",
      "Originally built on OpenAI Assistants, then rebuilt on Cloudflare AutoRAG for significantly better retrieval quality — a full RAG re-architecture driven entirely by retrieval performance.",
    ],
    skills: ["LLM application building; RAG", "Migrating AI providers for quality (OpenAI → AutoRAG)", "Shopify Flow domain expertise", "Product naming and launch"],
    featured: true,
  },
  {
    id: "talking-shop",
    num: "04",
    name: "Talking Shop",
    year: "2024",
    era: "shopify",
    status: "Active · 30+ episodes",
    oneLiner: "“The worst Shopify dev podcast of all time” — grew into a real show with live streams, sponsors, and a hackathon.",
    stack: ["Podcast / video production", "AI-generated chapters & transcripts", "Web"],
    links: { site: "https://talkingshop.dev", x: "https://x.com/talkingshopdev", tweet: "https://x.com/kalenjordan/status/1917929927229264348" },
    body: [
      "A Shopify developer podcast (co-hosted with @d_rbn) that grew into live Twitter Spaces and video streams with a rotating crew. I built the site and tooling — AI-generated chapters, timecoded transcripts, and browsable AI prompts.",
      "30+ episodes in, with real sponsors (Hookdeck, Osello, Smile) and a hackathon announced in 2026. It's become a meaningful community and developer-relations surface.",
    ],
    skills: ["Community building & developer relations", "Content production at a regular cadence", "Applied AI (transcript / chapter generation)", "Web build for the show site"],
    featured: true,
  },
  {
    id: "flow-helper",
    num: "19",
    name: "Flow Helper",
    year: "2024",
    era: "shopify",
    status: "Shipped · app + site + service",
    oneLiner: "My Shopify Flow brand: a Gadget app adding Flow actions and triggers, a marketing site, and a “Shopify automation as a service” consulting offer.",
    stack: ["Gadget", "Shopify Flow", "Remix", "Node.js"],
    links: { tweet: "https://x.com/kalenjordan/status/1804247318406787358" },
    media: { kind: "image", src: "/img/flow-helper.webp", label: "screenshot" },
    body: [
      "Flow Helper packages my Shopify Flow work under one brand. The app extends Flow with custom actions and triggers — a transactional-email action, webhook triggers, a marketing-consent trigger, and the standout: a GraphQL action that runs a query or mutation and exposes the results to later steps in the same flow, filling a real gap in Flow's native capabilities. It was monetized free-for-X-runs then paid, and had real production users within weeks.",
      "Alongside the app I built a marketing site offering “Shopify automation as a service,” with templates, posts, and a booking flow — positioning Flow, iPaaS, and custom integration help as a single consulting offer. It's the umbrella the broader Flow product line grew out of.",
    ],
    skills: ["Productizing a service into a brand", "Shopify Flow actions, triggers & GraphQL extensions", "Gadget app development", "Marketing site build (Remix)", "Usage-based pricing / monetization", "Automation consulting positioning"],
    featured: false,
  },
  {
    id: "flow-extensions",
    num: "06",
    name: "Flow Extensions",
    year: "2025",
    era: "shopify",
    status: "Open source · ~10★",
    oneLiner: "Open-sourced Gadget app bundling Flow goodies — abandoned-cart→Klaviyo action, webhook Flow trigger, and a custom-UI Flow action.",
    stack: ["Gadget", "Shopify Flow", "Klaviyo", "JavaScript"],
    links: { github: "https://github.com/kalenjordan/flow-extensions", tweet: "https://x.com/kalenjordan/status/1887626568563847602" },
    body: [
      "A Gadget app containing reusable Flow building blocks: a Flow action that sends abandoned carts to Klaviyo, a Flow trigger for webhooks, and a Flow action with a custom configuration UI.",
      "It originated from a paid client engagement, then I generalized it into a reusable public asset (~10★) — a good example of turning client delivery into community and portfolio value.",
    ],
    skills: ["Gadget app development", "Flow actions, triggers, custom config UI", "Klaviyo integration", "Client delivery → open source"],
    featured: true,
  },
  {
    id: "flow-templates",
    num: "07",
    name: "Flow Templates",
    year: "2024",
    era: "shopify",
    status: "Shipped · first template released",
    oneLiner: "Templates published directly into Shopify's official Flow library, with one-click installs.",
    stack: ["Shopify Flow"],
    links: { tweet: "https://x.com/kalenjordan/status/1807808998936092710" },
    body: [
      "Authored and submitted Flow templates that appear in Shopify's native Flow template library, replacing clunky download-and-import with one-click installs.",
    ],
    skills: ["Shopify Flow authoring", "Working within Shopify's review process and first-party surfaces"],
    featured: false,
  },
  {
    id: "bulk-ai-content",
    num: "08",
    name: "Bulk AI Product Content",
    year: "2025",
    era: "shopify",
    status: "Shipped",
    oneLiner: "Bulk AI generation of product and meta descriptions, with bulk approvals and quick previews.",
    stack: ["LLM content generation", "Shopify Admin GraphQL", "Prompt engineering"],
    links: { tweet: "https://x.com/kalenjordan/status/1984270720810008716" },
    body: [
      "Bulk-generates product and meta descriptions across a catalog, piping in native product/variant fields, metafields, and any existing copy as inputs, then offering bulk approvals and quick previews — built around a real workflow rather than raw generation.",
    ],
    skills: ["LLM content generation & prompt engineering", "Shopify product data, variants, metafields", "Review / approval workflows for AI output"],
    featured: false,
  },
  {
    id: "admin-block-builder",
    num: "18",
    name: "Admin Block Builder",
    year: "2025",
    era: "shopify",
    status: "Prototype · validated interest",
    oneLiner: "A visual builder for Shopify admin blocks — group, lay out, and render metafields as native admin UI extensions, no hard-coding.",
    stack: ["Gadget", "Shopify Admin UI Extensions", "Admin GraphQL", "React"],
    links: { tweet: "https://x.com/kalenjordan/status/1909419702759157839" },
    body: [
      "Started from a real gap: there was no good way to group metafields, render them in different ways (lists, images, cards), and lay them out in the Shopify admin. So I built a generic admin-block builder — a tree-based editor with a variable selector for native fields and metafields, currency formatting, and JSON import/export — that outputs product and company admin-block extensions.",
      "It drew more inbound interest than most ideas I'd shipped, including what may be the first NetSuite admin block — one of the more genuinely novel app concepts I've prototyped.",
    ],
    skills: ["Shopify Admin UI Extensions", "Generic config-driven UI / layout builders", "Metafields & metaobjects modeling", "Gadget app development", "Product validation from build-in-public"],
    featured: false,
  },
  {
    id: "shopadmin-cli",
    num: "09",
    name: "shopadmin-cli",
    year: "2025",
    era: "shopify",
    status: "Open source · active",
    oneLiner: "A growing grab-bag CLI of Shopify admin utilities, designed to also be driven by Claude for testing.",
    stack: ["TypeScript", "Shopify Admin GraphQL", "CLI"],
    links: { github: "https://github.com/kalenjordan/shopadmin-cli", tweet: "https://x.com/kalenjordan/status/1971699168600113441" },
    body: [
      "A CLI of Shopify admin utilities — create orders and products, fetch data or metafields, run arbitrary GraphQL — explicitly built so an AI agent can run admin actions for testing.",
    ],
    skills: ["TypeScript CLI development", "Shopify Admin GraphQL", "Designing tools to be agent-operable"],
    featured: false,
  },
  {
    id: "shop-context",
    num: "10",
    name: "Shop Context",
    year: "2025",
    era: "shopify",
    status: "Open source · Chrome Web Store",
    oneLiner: "Chrome extension published to the Chrome Web Store, with a fast follow-up release.",
    stack: ["Chrome extension", "JavaScript"],
    links: { site: "https://chromewebstore.google.com/detail/shop-context/imljloilpddpflepncbecapbdigdhlmc", github: "https://github.com/kalenjordan/shopcontext", tweet: "https://x.com/kalenjordan/status/1956717055677497499" },
    body: [
      "A Chrome extension for Shopify context, shipped through the Web Store review process and open-sourced — a ship-then-iterate exercise (v1.0.0 out, v1.0.1 right behind it).",
    ],
    skills: ["Chrome extension development & Web Store publishing", "Ship-then-iterate discipline"],
    featured: false,
  },
  {
    id: "app-deploy-cli",
    num: "11",
    name: "App Deploy CLI",
    year: "2025",
    era: "shopify",
    status: "Shipped",
    oneLiner: "CLI for deploying environment-specific Shopify apps, with scopes managed in a sane JS array instead of Shopify's gnarly TOML.",
    stack: ["Node.js", "Shopify CLI / app config", "CLI"],
    links: { tweet: "https://x.com/kalenjordan/status/1961110231037186413" },
    body: [
      "Deploys environment-specific Shopify apps and manages access scopes in a readable JS array rather than the comma-separated-no-newlines TOML format Shopify uses — a developer-experience fix for a real friction point.",
    ],
    skills: ["CLI / build tooling", "Shopify app deployment & configuration", "Multi-environment deployment workflows"],
    featured: false,
  },
  {
    id: "shopapps",
    num: "15",
    name: "shopapps.info",
    year: "2025",
    era: "shopify",
    status: "Shipped",
    oneLiner: "Chrome extension and site that layers AI-generated Q&A onto Shopify App Store listings.",
    stack: ["Chrome extension", "AI content generation", "Web"],
    links: { tweet: "https://x.com/kalenjordan/status/1906514768204107959" },
    body: [
      "A Chrome extension and companion site that adds questions-and-answers to Shopify App Store listings, explaining each app's value proposition in a familiar review-style format — rapid AI-assisted product creation in a familiar UX.",
    ],
    skills: ["Chrome extension development", "AI content generation in a structured format", "Landing-page / product launch"],
    featured: false,
  },
  {
    id: "swipe-it",
    num: "20",
    name: "Swipe It",
    year: "2025",
    era: "shopify",
    status: "Shop Mini · submitted",
    oneLiner: "A swipe-to-discover Shop Mini — flick through a store's catalog one product at a time, Tinder-style, and save the ones you like.",
    stack: ["Shop Minis SDK", "React", "Vite", "Cloudflare Workers", "TypeScript"],
    links: {},
    body: [
      "A mobile Shop Mini that turns product discovery into a swipe deck: one product card at a time, swipe to pass or save. A Cloudflare Worker backs the search queries, with saved-product fetching and a tap-through to product details.",
      "Built on the Shop Minis SDK and submitted to the Shop Mini store — a small, focused experiment in a more playful storefront browsing pattern.",
    ],
    skills: ["Shop Minis SDK development", "Mobile-first React / touch gestures", "Cloudflare Workers backend", "Shop Mini submission process"],
    featured: false,
  },
  {
    id: "search-edge-case-finder",
    num: "15",
    name: "Search Edge-Case Finder",
    year: "2026",
    era: "shopify",
    status: "Shipped · outreach tool",
    oneLiner: "Hunts for important missing edge cases in a store's search functionality, built as a sales / outreach play.",
    stack: ["Node.js", "Search analysis"],
    links: { tweet: "https://x.com/kalenjordan/status/2047054891789533298" },
    body: [
      "Analyzes a store's search functionality and surfaces important missing edge cases, packaged for outreach — turning a technical audit into a sales conversation.",
    ],
    skills: ["Search / relevance analysis", "Tooling that doubles as a sales motion"],
    featured: false,
  },
  {
    id: "magento-oss",
    num: "16",
    name: "Magento OSS Portfolio",
    year: "2013–2019",
    era: "magento",
    status: "Open source · historical · ~488★",
    oneLiner: "A decade-spanning body of open-source Magento modules and community tooling — hundreds of GitHub stars and lasting adoption.",
    stack: ["PHP", "Magento 1 & 2", "SQL / MySQL", "Elasticsearch"],
    links: { github: "https://github.com/kalenjordan?tab=repositories" },
    body: [
      "A sustained run of Magento developer tooling across the 2010s. Highlights include custom-reports (141★, custom Magento reports in SQL), magerun-addons (116★, commands for the n98-magerun CLI), magehero (39★, a community project), clean-elastic-search (22★, Elasticsearch-powered admin search), and magesplit (18★, split testing via Google Analytics) — plus many more email, quote, reporting, and theming modules.",
      "Altogether ~488 stars across my own repos and years of community adoption: a long track record of building developer-facing tools others rely on, and a recognized name in the Magento community of the era.",
    ],
    skills: ["Deep Magento 1 & 2 module development", "PHP at production depth", "SQL / MySQL (custom reporting was the flagship)", "Elasticsearch integration", "CLI tooling", "Long-term OSS maintainership & community building"],
    featured: true,
  },
];

const ASCII = {
  solid: "─".repeat(80),
  dotted: "· ".repeat(40).trim(),
  dashed: "─ ".repeat(40).trim(),
  thick: "━".repeat(80),
  double: "═".repeat(80),
  chevrons: "> ".repeat(40).trim(),
};

// ----------------------------------------------------------------------------
// Klaviyo Flow Simulation
// ----------------------------------------------------------------------------
function KlaviyoFlowSim({ palette = "swiss", compact = false }) {
  const colors = {
    terminal: {
      line: "#1a1a1a", node: "#1a1a1a", nodeBg: "transparent",
      text: "#1a1a1a", muted: "#6a6a64", accent: "#c2410c", bg: "transparent",
    },
    swiss: {
      line: "#2a2a28", node: "#2a2a28", nodeBg: "#f5f1e8",
      text: "#2a2a28", muted: "#7a776f", accent: "#1a1a1a", bg: "transparent",
    },
    brutalist: {
      line: "#111", node: "#111", nodeBg: "#f7e94a",
      text: "#111", muted: "#555", accent: "#e11d48", bg: "transparent",
    },
  }[palette];

  const [tick, setTick] = React.useState(0);
  const [metrics, setMetrics] = React.useState({
    triggered: 1247, opened: 612, clicked: 184, converted: 47,
  });

  React.useEffect(() => {
    const id = setInterval(() => {
      setTick((t) => (t + 1) % 8);
      setMetrics((m) => ({
        triggered: m.triggered + Math.floor(Math.random() * 3),
        opened: m.opened + Math.floor(Math.random() * 2),
        clicked: m.clicked + (Math.random() > 0.7 ? 1 : 0),
        converted: m.converted + (Math.random() > 0.92 ? 1 : 0),
      }));
    }, 900);
    return () => clearInterval(id);
  }, []);

  const nodes = [
    { id: "t",  col: 0, row: 1, label: "TRIGGER", sub: "checkout_started", kind: "trigger" },
    { id: "w1", col: 1, row: 1, label: "WAIT",    sub: "30 min",           kind: "wait" },
    { id: "s1", col: 2, row: 1, label: "SPLIT",   sub: "cart > $100?",     kind: "split" },
    { id: "e1", col: 3, row: 0, label: "EMAIL 1A", sub: "high-value recovery", kind: "email" },
    { id: "e2", col: 3, row: 2, label: "EMAIL 1B", sub: "standard recovery",   kind: "email" },
    { id: "w2", col: 4, row: 0, label: "WAIT",    sub: "24 hr",            kind: "wait" },
    { id: "w3", col: 4, row: 2, label: "WAIT",    sub: "24 hr",            kind: "wait" },
    { id: "e3", col: 5, row: 0, label: "EMAIL 2", sub: "incentive + urgency", kind: "email" },
    { id: "e4", col: 5, row: 2, label: "EMAIL 2", sub: "soft reminder",    kind: "email" },
    { id: "x",  col: 6, row: 1, label: "CONVERT", sub: "placed_order",     kind: "goal" },
  ];

  const edges = [
    ["t","w1"],["w1","s1"],["s1","e1"],["s1","e2"],
    ["e1","w2"],["e2","w3"],["w2","e3"],["w3","e4"],
    ["e3","x"],["e4","x"],
  ];

  const colW = compact ? 150 : 180;
  const rowH = compact ? 90 : 100;
  const nodeW = compact ? 130 : 150;
  const nodeH = compact ? 54 : 62;
  const totalW = 7 * colW;
  const totalH = 3 * rowH;

  const pos = (n) => ({ x: n.col * colW + (colW - nodeW) / 2, y: n.row * rowH + (rowH - nodeH) / 2 });
  const nodeById = Object.fromEntries(nodes.map((n) => [n.id, n]));

  return (
    <div style={{ width: "100%", color: colors.text, fontFamily: "inherit" }}>
      <svg viewBox={`0 0 ${totalW} ${totalH}`} style={{ width: "100%", height: "auto", display: "block" }}>
        {edges.map(([fromId, toId], i) => {
          const a = nodeById[fromId];
          const b = nodeById[toId];
          const pa = pos(a);
          const pb = pos(b);
          const x1 = pa.x + nodeW, y1 = pa.y + nodeH / 2;
          const x2 = pb.x,         y2 = pb.y + nodeH / 2;
          const midX = (x1 + x2) / 2;
          const d = `M ${x1} ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${x2} ${y2}`;
          const active = (tick + i) % 3 === 0;
          return (
            <g key={`e-${i}`}>
              <path d={d} stroke={colors.line} strokeWidth="1" fill="none" />
              {active && (
                <circle r="3" fill={colors.accent}>
                  <animateMotion dur="0.9s" repeatCount="1" path={d} />
                </circle>
              )}
            </g>
          );
        })}
        {nodes.map((n) => {
          const p = pos(n);
          const isPulsing = (n.id === "t" && tick % 2 === 0) || (n.id === "x" && tick % 4 === 0);
          return (
            <g key={n.id} transform={`translate(${p.x}, ${p.y})`}>
              <rect x="0" y="0" width={nodeW} height={nodeH}
                fill={n.kind === "goal" && palette === "brutalist" ? colors.accent : colors.nodeBg}
                stroke={colors.node} strokeWidth={n.kind === "goal" ? 2 : 1} />
              <text x={nodeW / 2} y={nodeH / 2 - 4} textAnchor="middle" fontSize="10"
                fontWeight="600" fontFamily="JetBrains Mono, monospace"
                fill={isPulsing ? colors.accent : colors.text} style={{ letterSpacing: "0.08em" }}>
                {n.label}
              </text>
              <text x={nodeW / 2} y={nodeH / 2 + 12} textAnchor="middle" fontSize="9"
                fontFamily="JetBrains Mono, monospace" fill={colors.muted}>
                {n.sub}
              </text>
            </g>
          );
        })}
      </svg>
      <div className="metrics-grid" style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", marginTop: 20,
        borderTop: `1px solid ${colors.line}`, borderBottom: `1px solid ${colors.line}`, fontFamily: "inherit" }}>
        {[
          ["TRIGGERED", metrics.triggered, ""],
          ["OPENED", metrics.opened, `${Math.round((metrics.opened / metrics.triggered) * 100)}%`],
          ["CLICKED", metrics.clicked, `${Math.round((metrics.clicked / metrics.triggered) * 100)}%`],
          ["REVENUE", `$${(metrics.converted * 87).toLocaleString()}`, `${metrics.converted} orders`],
        ].map(([label, val, pct], i) => (
          <div key={label} style={{ padding: "14px 16px", borderLeft: i === 0 ? "none" : `1px solid ${colors.line}`, fontFamily: "inherit" }}>
            <div style={{ fontSize: 10, color: colors.muted, letterSpacing: "0.1em" }}>{label}</div>
            <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4, color: colors.text, fontVariantNumeric: "tabular-nums" }}>{val}</div>
            {pct && <div style={{ fontSize: 10, color: colors.accent, marginTop: 2 }}>{pct}</div>}
          </div>
        ))}
      </div>
    </div>
  );
}

// ----------------------------------------------------------------------------
// Contact form — posts to /api/contact
// ----------------------------------------------------------------------------
function ContactForm({ palette = "swiss", defaultService = "" }) {
  const colors = {
    terminal: { border: "#1a1a1a", bg: "transparent", text: "#1a1a1a", accent: "#c2410c", btnBg: "#1a1a1a", btnText: "#efece4" },
    swiss:    { border: "#2a2a28", bg: "#fafaf5",      text: "#2a2a28", accent: "#1a1a1a", btnBg: "#1a1a1a", btnText: "#fafaf5" },
    brutalist:{ border: "#111",    bg: "#fff",          text: "#111",    accent: "#e11d48", btnBg: "#f7e94a", btnText: "#111" },
    dark:     { border: "#3a352e", bg: "#211e1a",      text: "#e6e4dd", accent: "#d97757", btnBg: "#d97757", btnText: "#1a1815" },
  }[palette];

  const [form, setForm] = React.useState({ name: "", email: "", service: defaultService, msg: "" });
  const [status, setStatus] = React.useState("idle"); // idle | sending | sent | error

  const input = {
    width: "100%", background: colors.bg, border: "none",
    borderBottom: `1px solid ${colors.border}`, padding: "10px 8px",
    fontFamily: "inherit", fontSize: 14, color: colors.text, outline: "none",
  };
  const label = { fontSize: 11, color: colors.text, opacity: 0.6, letterSpacing: "0.08em", display: "block", marginTop: 18 };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus("sending");
    try {
      const res = await fetch("/api/contact", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(form),
      });
      if (res.ok) {
        setStatus("sent");
      } else {
        setStatus("error");
      }
    } catch {
      setStatus("error");
    }
  };

  if (status === "sent") {
    return (
      <div style={{ padding: "40px 0", color: colors.text }}>
        <div style={{ fontSize: 13, color: colors.accent, letterSpacing: "0.1em" }}>&gt; MESSAGE SENT</div>
        <div style={{ marginTop: 12, fontSize: 16 }}>Thanks — I'll reply within 1–2 business days.</div>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} style={{ color: colors.text }}>
      <label style={label}>NAME</label>
      <input style={input} name="name" autoComplete="name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required />

      <label style={label}>EMAIL</label>
      <input style={input} type="email" name="email" autoComplete="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} required />

      <label style={label}>SERVICE</label>
      <select style={{ ...input, appearance: "none", cursor: "pointer" }}
        value={form.service} onChange={(e) => setForm({ ...form, service: e.target.value })} required>
        <option value="">— select —</option>
        {CONTENT.services.map((s) => (
          <option key={s.id} value={s.id}>{s.name}</option>
        ))}
        <option value="other">Something else</option>
      </select>

      <label style={label}>WHAT ARE YOU WORKING ON?</label>
      <textarea rows="4" style={{ ...input, resize: "vertical", paddingTop: 10 }}
        value={form.msg} onChange={(e) => setForm({ ...form, msg: e.target.value })} required />

      {status === "error" && (
        <div style={{ marginTop: 12, fontSize: 13, color: colors.accent }}>
          Something went wrong — please email me directly at {CONTENT.email}
        </div>
      )}

      <button type="submit" disabled={status === "sending"} style={{
        marginTop: 28, padding: "12px 28px", background: colors.btnBg, color: colors.btnText,
        border: `1px solid ${colors.border}`, fontFamily: "inherit", fontSize: 13,
        fontWeight: 600, letterSpacing: "0.1em", cursor: "pointer",
        opacity: status === "sending" ? 0.6 : 1,
      }}>
        {status === "sending" ? "SENDING…" : "SEND →"}
      </button>
    </form>
  );
}

function Cursor({ color = "#1a1a1a" }) {
  const [on, setOn] = React.useState(true);
  React.useEffect(() => {
    const id = setInterval(() => setOn((v) => !v), 530);
    return () => clearInterval(id);
  }, []);
  return (
    <span style={{ display: "inline-block", width: "0.55em", height: "1em",
      background: on ? color : "transparent", verticalAlign: "text-bottom", marginLeft: "0.08em" }} />
  );
}

Object.assign(window, { CONTENT, PROJECTS, ASCII, KlaviyoFlowSim, ContactForm, Cursor });
