// Vercel Upsell Game — main app
const { useState, useEffect, useMemo, useRef } = React;

// ===== Data =====
const PLAN_DATA = [
  { metric: "Base price", sub: "Per developer / month", hobby: "$0", pro: "$20", ent: "Custom", _sort: { hobby: 0, pro: 20, ent: 9999 } },
  { metric: "Fast Data Transfer", sub: "Bandwidth from Vercel's edge", hobby: "100 GB", pro: "1 TB", ent: "Custom", _sort: { hobby: 100, pro: 1000, ent: 99999 } },
  { metric: "Edge Requests", sub: "Every page load, every API hit", hobby: "1 M", pro: "10 M", ent: "Custom", _sort: { hobby: 1, pro: 10, ent: 9999 } },
  { metric: "Function Invocations", sub: "Backend logic runs", hobby: "1 M", pro: "Included in credit", ent: "Custom", _sort: { hobby: 1, pro: 10, ent: 9999 } },
  { metric: "Active CPU", sub: "Compute hours billed", hobby: "4 hrs", pro: "Pay as you go", ent: "Custom", _sort: { hobby: 4, pro: 1000, ent: 9999 } },
  { metric: "Function Duration", sub: "Max time per invocation", hobby: "60 sec", pro: "300 sec", ent: "900 sec", _sort: { hobby: 60, pro: 300, ent: 900 } },
  { metric: "Image Optimization", sub: "Transformations / month", hobby: "5 K", pro: "10 K + overage", ent: "Custom", _sort: { hobby: 5, pro: 10, ent: 9999 } },
  { metric: "Team seats", sub: "Developer seats included", hobby: "1", pro: "1, +$20 each", ent: "Custom", _sort: { hobby: 1, pro: 1, ent: 9999 } },
  { metric: "Spend Cap", sub: "Hard limit on overages", hobby: "Site goes offline", pro: "Default $200, soft", ent: "Custom", _sort: { hobby: 0, pro: 200, ent: 99999 }, hobbyClass: "warn" },
  { metric: "Commercial Use", sub: "Run a business on it?", hobby: "Prohibited", pro: "Allowed", ent: "Allowed", _sort: { hobby: 0, pro: 1, ent: 1 }, hobbyClass: "warn" },
  { metric: "SAML SSO", sub: "Single sign-on", hobby: "—", pro: "+$300/mo add-on", ent: "Included", _sort: { hobby: 0, pro: 300, ent: 0 } },
  { metric: "HIPAA BAA", sub: "Healthcare compliance", hobby: "—", pro: "+$350/mo add-on", ent: "Included", _sort: { hobby: 0, pro: 350, ent: 0 } },
  { metric: "Build machines", sub: "Default tier", hobby: "Standard", pro: "Turbo (30 vCPU)", ent: "Turbo + custom", _sort: { hobby: 1, pro: 2, ent: 3 } },
  { metric: "DDoS billing", sub: "Pay for attack traffic?", hobby: "N/A (caps)", pro: "Yes, $0.15/GB", ent: "Configurable", _sort: { hobby: 0, pro: 1, ent: 2 }, proClass: "warn" },
];

const SCENARIOS = [
  { id: "blog", label: "Personal Blog", gb: 30 },
  { id: "saas", label: "Side Project", gb: 85 },
  { id: "viral", label: "Goes Viral on HN", gb: 240 },
  { id: "ddos", label: "DDoS Hit (1 day)", gb: 850 },
];

const FEES = [
  { id: 0, name: "DDoS Surcharge", price: "$0.15 / GB of attack traffic", desc: "Vercel bills you for the bandwidth from malicious traffic. One documented case: a $23,000 invoice from a single attack, all of it billed at the standard egress rate.", color: "#ff5f56" },
  { id: 1, name: "ISR Reads", price: "$0.40 per 1M", desc: "Incremental Static Regeneration sounded free. Each read of stale content is metered, and a busy CMS-backed site can ring up tens of dollars before lunch.", color: "#f5d76e" },
  { id: 2, name: "Image Optimization", price: "Per source image, monthly", desc: "A HowdyGo case study found image optimization on 28,000 images would add $115/month — a 7× multiplier on the $20 subscription.", color: "#50e3c2" },
  { id: 3, name: "Build Minute Variance", price: "Up to 3× swing on identical code", desc: "Community reports note build times fluctuating from 4 to 12+ minutes for the same commit, creating cost spikes outside developer control.", color: "#a78bfa" },
  { id: 4, name: "Active CPU Time", price: "$0.128 / hour after free", desc: "I/O waits don't count, but every actual ms your code runs does. AI streaming responses count as fully active. One screenshot service used 494 GB-hrs in 12 days of testing.", color: "#fb923c" },
  { id: 5, name: "Per-Seat Tax", price: "$20 / developer / month", desc: "A 10-developer team is $200/month before serving a single byte. Adding a designer who previews deployments? Another seat. AWS Amplify and Firebase charge no per-seat fees.", color: "#60a5fa" },
  { id: 6, name: "Edge Middleware", price: "Billed every request", desc: "Authentication checks, A/B tests, geo-routing — every visitor triggers a billable edge execution before your page even loads. A constant multiplier on traffic.", color: "#f472b6" },
  { id: 7, name: "Hobby Hard Cap", price: "Cost: your uptime", desc: "Hit 100 GB on the free tier and your deployment pauses until the next 30-day cycle. No throttling, no warning page, no overage option. The site simply goes dark.", color: "#ef4444" },
];

const ADDONS = [
  { name: "SAML Single Sign-On", price: "$300", unit: "/mo", desc: "On Pro as an add-on. Free on Enterprise.", required: "Compliance often demands it", stamp: "ADD-ON" },
  { name: "HIPAA BAA", price: "$350", unit: "/mo", desc: "Healthcare compliance contract. Pro add-on; Enterprise standard.", required: "Required for any PHI", stamp: "ADD-ON" },
  { name: "Advanced Deployment Protection", price: "$150", unit: "/mo", desc: "SSO-protected previews, stricter access controls.", required: "Lock down preview links", stamp: "ADD-ON" },
  { name: "Observability Plus", price: "$10", unit: "/mo +", desc: "Extended monitoring, longer data retention.", required: "Default observability is shallow", stamp: "ADD-ON" },
  { name: "Web Analytics Plus", price: "$10", unit: "/mo +", desc: "More events and longer history than the free tier.", required: "Default analytics is capped", stamp: "ADD-ON" },
  { name: "Extra Developer Seat", price: "$20", unit: "/mo each", desc: "Pro includes 1 seat. Each additional commits seat permanently.", required: "Linear team-size tax", stamp: "REQUIRED" },
  { name: "Bandwidth Overage", price: "$0.15", unit: "/ GB", desc: "After 1 TB. AWS egress runs ~$0.08–$0.09/GB for comparison.", required: "Uncapped by default", stamp: "OVERAGE" },
  { name: "Edge Request Overage", price: "$2", unit: "/ M", desc: "Every middleware execution counts. Auth-heavy apps multiply fast.", required: "Uncapped by default", stamp: "OVERAGE" },
];

const ANECDOTES = [
  { quote: "My $20/mo plan actually cost $286.", figures: [{ lbl: "Sticker", v: "$20", cls: "" }, { lbl: "Actual", v: "$286", cls: "bad" }, { lbl: "Multiplier", v: "14×", cls: "bad" }], src: "DeployWise / 2026" },
  { quote: "A documented case reported a $23,000 bill from a DDoS attack — all attack traffic billed at the standard bandwidth rate.", figures: [{ lbl: "Bill", v: "$23,000", cls: "bad" }, { lbl: "Cause", v: "DDoS", cls: "" }, { lbl: "Billed at", v: "$0.15/GB", cls: "" }], src: "CheckThat.ai" },
  { quote: "A screenshot service used 494 GB-hours in just 12 days — projecting $160/mo over the base plan.", figures: [{ lbl: "Window", v: "12 days", cls: "" }, { lbl: "Projection", v: "+$160/mo", cls: "bad" }, { lbl: "Workload", v: "Screenshots", cls: "" }], src: "TrueFoundry" },
  { quote: "Image optimization for 28,000 images would add $115/month — a 7× multiplier on the base subscription.", figures: [{ lbl: "Images", v: "28,000", cls: "" }, { lbl: "Cost", v: "+$115/mo", cls: "bad" }, { lbl: "On top of", v: "$20 base", cls: "" }], src: "HowdyGo case study" },
];

// ===== Components =====

function TopBar() {
  return (
    <header className="topbar">
      <div className="shell topbar-inner">
        <div className="topbar-left">
          <div className="triangle" />
          <span style={{ fontFamily: "'Geist Mono', monospace", fontSize: 12, letterSpacing: "0.15em", textTransform: "uppercase", color: "var(--ink-3)" }}>
            The Upsell Game
          </span>
        </div>
        <nav>
          <a href="#receipt">Receipt</a>
          <a href="#plans">Plans</a>
          <a href="#meter">Bandwidth</a>
          <a href="#calc">Calculator</a>
          <a href="#growth">Growth</a>
          <a href="#wheel">Hidden Fees</a>
          <a href="#addons">Add-ons</a>
        </nav>
        <div className="topbar-right">
          <span className="live-dot" />
          <span>CREATED WITH <a href="https://designdotmd.directory" target="_blank" rel="noopener noreferrer">DESIGN.MD DIRECTORY</a></span>
        </div>
      </div>
    </header>
  );
}

function Hero() {
  return (
    <section className="hero" id="receipt">
      <div className="shell hero-grid">
        <div>
          <div className="hero-eyebrow">An investigation in 8 parts</div>
          <h1>
            <span className="strike">$0.</span> <span className="strike">$20.</span><br />
            Then <em>everything else.</em>
          </h1>
          <p className="lede">
            Vercel's pricing page shows three columns. The invoice shows fourteen line items. This is what they don't put on the marketing page — the per-seat tax, the bandwidth overages, the DDoS-you-pay-for, and the hard cap that takes your free site offline without warning.
          </p>
          <div className="byline">
            <div><strong>Investigation</strong> · 8 line items</div>
            <div><strong>Sources</strong> · 11 cited</div>
            <div><strong>Last verified</strong> · Apr 24, 2026</div>
          </div>
        </div>
        <div>
          <Receipt />
        </div>
      </div>
    </section>
  );
}

function Receipt() {
  return (
    <div className="receipt">
      <div className="r-head">
        <div className="logo">▲ VERCEL, INC.</div>
        <div className="meta">INV #2026-0431 · TEAM SUNRISE-7</div>
        <div className="meta">BILLING PERIOD · APR 01 — APR 30</div>
      </div>
      <div className="r-row title"><span>DESCRIPTION</span><span>AMOUNT</span></div>
      <div className="r-row"><span>Pro plan · 1 dev seat</span><span>$20.00</span></div>
      <div className="r-row"><span>Add'l dev seats × 4</span><span>$80.00</span></div>
      <div className="r-row warn"><span>Bandwidth overage · 480 GB</span><span>$72.00</span></div>
      <div className="r-row warn"><span>Edge requests · 14M over</span><span>$28.00</span></div>
      <div className="r-row warn"><span>Active CPU · 12.5 hrs over</span><span>$1.60</span></div>
      <div className="r-row warn"><span>Image optimization · 28K imgs</span><span>$23.00</span></div>
      <div className="r-row warn"><span>ISR reads · 4.2M over</span><span>$1.68</span></div>
      <div className="r-row"><span>SAML SSO add-on</span><span>$300.00</span></div>
      <div className="r-row"><span>Observability Plus</span><span>$10.00</span></div>
      <div className="r-row"><span>Build minutes · turbo</span><span>$48.30</span></div>
      <div className="r-row"><span>Credit applied</span><span>−$20.00</span></div>
      <div className="r-total">
        <span>TOTAL DUE</span>
        <span className="amt">$564.58</span>
      </div>
      <div className="r-foot">— THANK YOU FOR DEPLOYING —</div>
      <div className="r-stamp">SURPRISE</div>
    </div>
  );
}

function PlanTable({ teamSize }) {
  const [sort, setSort] = useState({ key: null, dir: 1 });
  const sortedRows = useMemo(() => {
    if (!sort.key) return PLAN_DATA;
    return [...PLAN_DATA].sort((a, b) => {
      const av = a._sort[sort.key];
      const bv = b._sort[sort.key];
      return (av - bv) * sort.dir;
    });
  }, [sort]);

  const onSort = (k) => {
    setSort((s) => ({ key: k, dir: s.key === k ? -s.dir : 1 }));
  };
  const arrow = (k) => sort.key !== k ? "↕" : sort.dir === 1 ? "↑" : "↓";
  const proPrice = (20 * teamSize).toLocaleString();

  return (
    <div style={{ overflowX: "auto" }}>
      <table className="plan-table">
        <thead>
          <tr>
            <th>Resource</th>
            <th
              className={`col-plan sortable ${sort.key === "hobby" ? "sorted" : ""}`}
              onClick={() => onSort("hobby")}
            >
              Hobby <span className="arrow">{arrow("hobby")}</span>
              <span className="price">$0 · forever · personal use</span>
            </th>
            <th
              className={`col-plan sortable ${sort.key === "pro" ? "sorted" : ""}`}
              onClick={() => onSort("pro")}
            >
              Pro <span className="arrow">{arrow("pro")}</span>
              <span className="price">${proPrice}/mo · {teamSize} dev seat{teamSize > 1 ? "s" : ""} + usage</span>
            </th>
            <th
              className={`col-plan sortable ${sort.key === "ent" ? "sorted" : ""}`}
              onClick={() => onSort("ent")}
            >
              Enterprise <span className="arrow">{arrow("ent")}</span>
              <span className="price">~$45,000/yr median · "talk to sales"</span>
            </th>
          </tr>
        </thead>
        <tbody>
          {sortedRows.map((row, i) => (
            <tr key={i}>
              <td className="metric"><strong>{row.metric}</strong>{row.sub}</td>
              <td className={`val ${row.hobbyClass || ""}`}>{row.hobby}</td>
              <td className={`val ${row.proClass || ""}`}>{row.pro}</td>
              <td className="val">{row.ent}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function BandwidthMeter() {
  const [scenario, setScenario] = useState("viral");
  const [animatedGb, setAnimatedGb] = useState(0);
  const sc = SCENARIOS.find((s) => s.id === scenario);
  const HOBBY_CAP = 100;
  const VIEW_MAX = 1000; // 1TB

  useEffect(() => {
    setAnimatedGb(0);
    const start = performance.now();
    const target = sc.gb;
    const duration = 1100;
    let rafId;
    const tick = (t) => {
      const p = Math.min(1, (t - start) / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      setAnimatedGb(target * eased);
      if (p < 1) rafId = requestAnimationFrame(tick);
    };
    rafId = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafId);
  }, [scenario]);

  const usedPct = Math.min(100, (Math.min(animatedGb, HOBBY_CAP) / VIEW_MAX) * 100);
  const overflowGb = Math.max(0, animatedGb - HOBBY_CAP);
  const overflowPct = (overflowGb / VIEW_MAX) * 100;
  const capPct = (HOBBY_CAP / VIEW_MAX) * 100;
  const proCapPct = 100; // 1TB shown as full meter
  const overageCharge = overflowGb * 0.15;
  const hobbyState = animatedGb > HOBBY_CAP ? "OFFLINE" : "OK";

  return (
    <div className="meter-wrap">
      <div>
        <div className="scenario-tabs">
          {SCENARIOS.map((s) => (
            <button
              key={s.id}
              className={`scenario-tab ${s.id === scenario ? "active" : ""}`}
              onClick={() => setScenario(s.id)}
            >
              {s.label} · {s.gb} GB
            </button>
          ))}
        </div>
        <div className="meter">
          <div className="meter-track">
            <div className="meter-fill" style={{ width: `${usedPct}%` }} />
            <div className="meter-overflow" style={{ width: `${overflowPct}%` }} />
          </div>
          <div className="meter-ticks">
            {[...Array(11)].map((_, i) => <div className="meter-tick" key={i} />)}
          </div>
          <div className="meter-cap" data-label="HOBBY CAP · 100 GB" style={{ left: `${capPct}%` }} />
          <div className="meter-cap" data-label="PRO INCL · 1 TB" style={{ left: `${proCapPct}%` }} />
        </div>
        <div className="meter-labels">
          <span>0 GB</span>
          <span>250</span>
          <span>500</span>
          <span>750</span>
          <span>1 TB</span>
        </div>
      </div>
      <div className="meter-readout">
        <div className="row"><span className="label">Bandwidth used</span><span className="v">{animatedGb.toFixed(0)} GB</span></div>
        <div className="row">
          <span className="label">On Hobby</span>
          <span className={`v ${hobbyState === "OFFLINE" ? "warn" : ""}`}>{hobbyState}</span>
        </div>
        <div className="row">
          <span className="label">On Pro · base</span>
          <span className="v">$20.00</span>
        </div>
        <div className="row">
          <span className="label">Pro · overage</span>
          <span className={`v ${overageCharge > 0 ? "warn" : ""}`}>${overageCharge.toFixed(2)}</span>
        </div>
        <div className="row">
          <span className="label">Total · Pro</span>
          <span className={`v ${overageCharge > 0 ? "warn" : ""}`}>${(20 + overageCharge).toFixed(2)}</span>
        </div>
      </div>
    </div>
  );
}

function Calculator({ teamSize }) {
  const [bandwidth, setBandwidth] = useState(800);
  const [requests, setRequests] = useState(8);
  const [cpuHrs, setCpuHrs] = useState(40);
  const [imgs, setImgs] = useState(8);

  const seatCost = 20 * teamSize;
  const bwOver = Math.max(0, bandwidth - 1000);
  const bwCost = bwOver * 0.15;
  const reqOver = Math.max(0, requests - 10);
  const reqCost = reqOver * 2;
  const cpuOver = Math.max(0, cpuHrs - 40);
  const cpuCost = cpuOver * 0.128 * 60; // approx
  const imgOver = Math.max(0, imgs - 10);
  const imgCost = imgOver * 5;
  const credit = -20;
  const total = seatCost + bwCost + reqCost + cpuCost + imgCost + credit;
  const multi = total > 0 ? (total / 20).toFixed(1) : "1.0";

  return (
    <div className="calc">
      <div className="calc-controls">
        <div className="slider-group">
          <label>Bandwidth (GB/mo) <span className="val">{bandwidth.toLocaleString()}</span></label>
          <input type="range" min="0" max="5000" step="50" value={bandwidth} onChange={(e) => setBandwidth(+e.target.value)} />
        </div>
        <div className="slider-group">
          <label>Edge requests (M/mo) <span className="val">{requests}</span></label>
          <input type="range" min="0" max="100" step="1" value={requests} onChange={(e) => setRequests(+e.target.value)} />
        </div>
        <div className="slider-group">
          <label>Active CPU (hrs/mo) <span className="val">{cpuHrs}</span></label>
          <input type="range" min="0" max="500" step="1" value={cpuHrs} onChange={(e) => setCpuHrs(+e.target.value)} />
        </div>
        <div className="slider-group">
          <label>Image optimizations (K/mo) <span className="val">{imgs}</span></label>
          <input type="range" min="0" max="100" step="1" value={imgs} onChange={(e) => setImgs(+e.target.value)} />
        </div>
        <div style={{ marginTop: 32, paddingTop: 24, borderTop: "1px solid var(--line)", fontSize: 12, color: "var(--ink-4)", fontFamily: "Geist Mono, monospace", lineHeight: 1.6 }}>
          Team size adjustable in Tweaks panel<br />
          Currently: <span style={{ color: "var(--ink)" }}>{teamSize} developer{teamSize > 1 ? "s" : ""}</span>
        </div>
      </div>
      <div className="calc-out">
        <div className="calc-line">
          <span className="lbl">{teamSize} × developer seat</span>
          <span className="v">${seatCost.toFixed(2)}</span>
        </div>
        <div className={`calc-line ${bwCost > 0 ? "over" : ""}`}>
          <span className="lbl">Bandwidth overage · {bwOver} GB</span>
          <span className="v">${bwCost.toFixed(2)}</span>
        </div>
        <div className={`calc-line ${reqCost > 0 ? "over" : ""}`}>
          <span className="lbl">Edge req overage · {reqOver}M</span>
          <span className="v">${reqCost.toFixed(2)}</span>
        </div>
        <div className={`calc-line ${cpuCost > 0 ? "over" : ""}`}>
          <span className="lbl">Active CPU overage · {cpuOver} hrs</span>
          <span className="v">${cpuCost.toFixed(2)}</span>
        </div>
        <div className={`calc-line ${imgCost > 0 ? "over" : ""}`}>
          <span className="lbl">Image optimization · {imgOver}K</span>
          <span className="v">${imgCost.toFixed(2)}</span>
        </div>
        <div className="calc-line">
          <span className="lbl">Monthly usage credit</span>
          <span className="v">−$20.00</span>
        </div>
        <div className="calc-total">
          <div>
            <div className="label">Estimated monthly bill</div>
            <div className="amount"><span className="currency">$</span>{Math.max(0, total).toFixed(0)}</div>
          </div>
          <div style={{ textAlign: "right", fontFamily: "Geist Mono, monospace", fontSize: 12, color: "var(--ink-3)" }}>
            <div>Sticker price</div>
            <div style={{ fontSize: 18, color: "var(--ink)", marginTop: 4 }}>$20</div>
          </div>
        </div>
        <div className="calc-multiplier">
          That's <strong>{multi}×</strong> the headline price · uncapped by default
        </div>
      </div>
    </div>
  );
}

function MAUChart({ teamSize }) {
  const points = [
    { mau: "1K", seats: 20 * teamSize, bw: 0, fn: 0, addons: 0 },
    { mau: "10K", seats: 20 * teamSize, bw: 0, fn: 0, addons: 0 },
    { mau: "50K", seats: 20 * teamSize, bw: 22, fn: 8, addons: 0 },
    { mau: "100K", seats: 20 * teamSize, bw: 75, fn: 28, addons: 10 },
    { mau: "250K", seats: 20 * teamSize, bw: 195, fn: 80, addons: 20 },
    { mau: "500K", seats: 20 * teamSize, bw: 420, fn: 165, addons: 30 },
    { mau: "1M", seats: 20 * teamSize, bw: 870, fn: 340, addons: 60 },
  ];
  const totals = points.map((p) => p.seats + p.bw + p.fn + p.addons);
  const max = Math.max(...totals, 1);
  const W = 900, H = 360, PAD = { l: 60, r: 30, t: 20, b: 50 };
  const CW = W - PAD.l - PAD.r, CH = H - PAD.t - PAD.b;
  const barW = CW / points.length * 0.55;
  const xFor = (i) => PAD.l + (i + 0.5) * (CW / points.length);
  const yFor = (v) => PAD.t + CH - (v / max) * CH;

  const yTicks = [0, 0.25, 0.5, 0.75, 1].map((f) => Math.round(max * f / 50) * 50);
  const [hover, setHover] = useState(null);

  return (
    <div className="chart-wrap" style={{ position: "relative" }}>
      <div className="chart-head">
        <div>
          <div style={{ fontSize: 12, color: "var(--ink-4)", fontFamily: "Geist Mono, monospace", letterSpacing: "0.15em", textTransform: "uppercase" }}>
            Monthly cost · stacked
          </div>
          <div style={{ fontSize: 24, fontWeight: 600, marginTop: 6 }}>
            From a $20 plan to <span style={{ color: "var(--money)" }}>${totals[totals.length - 1].toFixed(0)}/mo</span>
          </div>
        </div>
        <div className="chart-legend">
          <div className="legend-item"><div className="legend-swatch" style={{ background: "#fafafa" }} />Seats ({teamSize}×$20)</div>
          <div className="legend-item"><div className="legend-swatch" style={{ background: "#a1a1a1" }} />Bandwidth</div>
          <div className="legend-item"><div className="legend-swatch" style={{ background: "#525252" }} />Functions / CPU</div>
          <div className="legend-item"><div className="legend-swatch" style={{ background: "#262626", borderColor: "#fafafa" }} />Add-ons</div>
        </div>
      </div>
      <svg className="chart-svg" viewBox={`0 0 ${W} ${H}`}>
        <g className="grid">
          {yTicks.map((v, i) => (
            <line key={i} x1={PAD.l} x2={W - PAD.r} y1={yFor(v)} y2={yFor(v)} />
          ))}
        </g>
        <g className="axis">
          {yTicks.map((v, i) => (
            <text key={i} x={PAD.l - 10} y={yFor(v) + 4} textAnchor="end">${v}</text>
          ))}
          <line x1={PAD.l} x2={PAD.l} y1={PAD.t} y2={H - PAD.b} />
          <line x1={PAD.l} x2={W - PAD.r} y1={H - PAD.b} y2={H - PAD.b} />
        </g>
        {points.map((p, i) => {
          let stack = 0;
          const segs = [
            { v: p.seats, fill: "#fafafa" },
            { v: p.bw, fill: "#a1a1a1" },
            { v: p.fn, fill: "#525252" },
            { v: p.addons, fill: "#262626", stroke: "#fafafa" },
          ];
          const x = xFor(i) - barW / 2;
          return (
            <g key={i}
              onMouseEnter={() => setHover({ i, x: xFor(i), y: yFor(totals[i]) })}
              onMouseLeave={() => setHover(null)}
              style={{ cursor: "pointer" }}>
              {segs.map((s, j) => {
                const h = (s.v / max) * CH;
                if (h <= 0) return null;
                const y = yFor(stack + s.v);
                stack += s.v;
                return <rect key={j} x={x} y={y} width={barW} height={h} fill={s.fill} stroke={s.stroke || "none"} />;
              })}
              <text x={xFor(i)} y={yFor(totals[i]) - 8} textAnchor="middle" fill="var(--ink-2)" fontSize="11" fontFamily="Geist Mono, monospace">
                ${totals[i].toFixed(0)}
              </text>
            </g>
          );
        })}
        <g className="axis">
          {points.map((p, i) => (
            <text key={i} x={xFor(i)} y={H - PAD.b + 22} textAnchor="middle">{p.mau} MAU</text>
          ))}
        </g>
      </svg>
      {hover && (
        <div className="chart-tooltip show" style={{
          left: `${(hover.x / W) * 100}%`,
          top: `${(hover.y / H) * 100}%`,
          transform: "translate(-50%, -110%)"
        }}>
          <div>{points[hover.i].mau} monthly users</div>
          <div style={{ color: "var(--ink-3)" }}>Seats · ${points[hover.i].seats}</div>
          <div style={{ color: "var(--ink-3)" }}>Bandwidth · ${points[hover.i].bw}</div>
          <div style={{ color: "var(--ink-3)" }}>Compute · ${points[hover.i].fn}</div>
          <div style={{ color: "var(--ink-3)" }}>Add-ons · ${points[hover.i].addons}</div>
          <div className="total">Total · ${totals[hover.i]}</div>
        </div>
      )}
    </div>
  );
}

function Wheel() {
  const [rotation, setRotation] = useState(0);
  const [spinning, setSpinning] = useState(false);
  const [result, setResult] = useState(null);
  const [count, setCount] = useState(0);

  const SLICE = 360 / FEES.length;

  const spin = () => {
    if (spinning) return;
    setSpinning(true);
    const targetIndex = Math.floor(Math.random() * FEES.length);
    const fullSpins = 5 + Math.floor(Math.random() * 3);
    const finalAngle = fullSpins * 360 + (360 - targetIndex * SLICE - SLICE / 2);
    const newRotation = rotation + finalAngle;
    setRotation(newRotation);
    setTimeout(() => {
      setResult(FEES[targetIndex]);
      setCount((c) => c + 1);
      setSpinning(false);
    }, 4500);
  };

  // SVG wheel
  const R = 200;
  const CX = 240, CY = 240;
  const slices = FEES.map((f, i) => {
    const a0 = (i * SLICE - 90) * Math.PI / 180;
    const a1 = ((i + 1) * SLICE - 90) * Math.PI / 180;
    const x0 = CX + R * Math.cos(a0), y0 = CY + R * Math.sin(a0);
    const x1 = CX + R * Math.cos(a1), y1 = CY + R * Math.sin(a1);
    const path = `M ${CX} ${CY} L ${x0} ${y0} A ${R} ${R} 0 0 1 ${x1} ${y1} Z`;
    const ta = ((i + 0.5) * SLICE - 90) * Math.PI / 180;
    const tx = CX + (R * 0.62) * Math.cos(ta);
    const ty = CY + (R * 0.62) * Math.sin(ta);
    const rot = (i + 0.5) * SLICE;
    return { path, fill: i % 2 === 0 ? "#0a0a0a" : "#1a1a1a", stroke: f.color, tx, ty, rot, name: f.name };
  });

  return (
    <div className="wheel-wrap">
      <div className="wheel-stage">
        <div className="wheel-pin" />
        <svg className="wheel-svg" viewBox="0 0 480 480" style={{ transform: `rotate(${rotation}deg)` }}>
          <circle cx={CX} cy={CY} r={R + 4} fill="none" stroke="#fafafa" strokeWidth="2" />
          {slices.map((s, i) => (
            <g key={i}>
              <path d={s.path} fill={s.fill} stroke={s.stroke} strokeWidth="1.5" />
              <text
                x={s.tx} y={s.ty}
                textAnchor="middle"
                fill={s.stroke}
                fontSize="11"
                fontFamily="Geist Mono, monospace"
                fontWeight="600"
                transform={`rotate(${s.rot} ${s.tx} ${s.ty})`}
              >
                {s.name.length > 14 ? s.name.split(" ")[0] : s.name.toUpperCase()}
              </text>
            </g>
          ))}
        </svg>
        <button className="wheel-center" onClick={spin} disabled={spinning}>
          {spinning ? "..." : "Spin"}
        </button>
      </div>
      <div className="wheel-result">
        <div className="label-tag">Hidden Fee · drawn at random</div>
        {result ? (
          <>
            <h3>{result.name}</h3>
            <div className="price-tag">{result.price}</div>
            <p>{result.desc}</p>
            <div className="wheel-counter">Spins: {count.toString().padStart(3, "0")} · {FEES.length} fees in the deck</div>
          </>
        ) : (
          <>
            <h3 style={{ color: "var(--ink-4)" }}>—</h3>
            <p className="placeholder">Spin the wheel. Eight categories of charges, all real, all in the wild. Each one bills outside the $20 sticker.</p>
            <div className="wheel-counter">Spins: 000 · {FEES.length} fees in the deck</div>
          </>
        )}
      </div>
    </div>
  );
}

function AddonsGrid() {
  return (
    <div className="addons-grid">
      {ADDONS.map((a, i) => (
        <div className="addon-card" key={i}>
          <div className={`stamp ${a.stamp === "REQUIRED" || a.stamp === "OVERAGE" ? "required" : ""}`}>{a.stamp}</div>
          <div>
            <div className="price">{a.price}<small> {a.unit}</small></div>
            <div className="name">{a.name}</div>
          </div>
          <div className="desc">{a.desc}</div>
          <div className="addon-tooltip">{a.required}</div>
        </div>
      ))}
    </div>
  );
}

function Anecdotes() {
  return (
    <div className="anecdotes">
      {ANECDOTES.map((a, i) => (
        <div className="anec" key={i}>
          <div className="src">{a.src}</div>
          <div className="quote-mark">&ldquo;</div>
          <blockquote>{a.quote}</blockquote>
          <div className="figures">
            {a.figures.map((f, j) => (
              <div className="fig" key={j}>
                <span className="lbl">{f.lbl}</span>
                <span className={`v ${f.cls}`}>{f.v}</span>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

// ===== App =====
function App() {
  const defaults = /*EDITMODE-BEGIN*/{
    "teamSize": 5
  }/*EDITMODE-END*/;

  const [tweaks, setTweak] = useTweaks(defaults);
  const teamSize = tweaks.teamSize;

  return (
    <>
      <TopBar />

      <Hero />

      <section className="block" id="plans">
        <div className="shell">
          <div className="block-head">
            <div className="block-num"><span className="pill">01</span>The plans, in detail</div>
            <h2>Three columns. <em>Fourteen ways the bill grows.</em></h2>
          </div>
          <PlanTable teamSize={teamSize} />
          <p style={{ marginTop: 24, fontSize: 13, color: "var(--ink-4)", fontFamily: "Geist Mono, monospace", maxWidth: 800 }}>
            Tap any plan column to sort by that price. Hobby's "<span style={{ color: "var(--warn)" }}>site goes offline</span>" cap is the line that surprises new users most — there's no overage option, just downtime until day 30.
          </p>
        </div>
      </section>

      <section className="pull-quote">
        <div className="shell">
          <q>At a glance, Vercel's pricing looks unbelievably expensive. $550/TB traffic, and $60k/yr for a 128MB function running at 100% utilization.</q>
          <div className="attr">— Hacker News, surfaced via Costbench</div>
        </div>
      </section>

      <section className="block" id="meter">
        <div className="shell">
          <div className="block-head">
            <div className="block-num"><span className="pill">02</span>Bandwidth meter</div>
            <h2>100 GB feels like a lot. <em>Until traffic shows up.</em></h2>
          </div>
          <BandwidthMeter />
        </div>
      </section>

      <section className="block" id="calc">
        <div className="shell">
          <div className="block-head">
            <div className="block-num"><span className="pill">03</span>Live bill calculator</div>
            <h2>Drag the sliders. <em>Watch the $20 plan disappear.</em></h2>
          </div>
          <Calculator teamSize={teamSize} />
          <p style={{ marginTop: 24, fontSize: 13, color: "var(--ink-4)", fontFamily: "Geist Mono, monospace", maxWidth: 800 }}>
            Includes the $20 monthly usage credit. Real bills also include build minutes, ISR reads, blob storage, and any add-ons selected — none of which are sliders here. The actual ceiling is uncapped by default.
          </p>
        </div>
      </section>

      <section className="block" id="growth">
        <div className="shell">
          <div className="block-head">
            <div className="block-num"><span className="pill">04</span>Cost vs. growth</div>
            <h2>The bill scales <em>faster than your traffic does.</em></h2>
          </div>
          <MAUChart teamSize={teamSize} />
          <p style={{ marginTop: 24, fontSize: 13, color: "var(--ink-4)", fontFamily: "Geist Mono, monospace", maxWidth: 800 }}>
            Modeled on standard SaaS traffic patterns: ~2 KB/page, 6 page-loads per session, 1 session/MAU/day. Functions assume 200ms active CPU per page. Add-ons scale conservatively. Hover bars for breakdown.
          </p>
        </div>
      </section>

      <section className="block" id="wheel">
        <div className="shell">
          <div className="block-head">
            <div className="block-num"><span className="pill">05</span>Spin the hidden-fee wheel</div>
            <h2>Eight charges they don't put on <em>the marketing page.</em></h2>
          </div>
          <Wheel />
        </div>
      </section>

      <section className="block" id="addons">
        <div className="shell">
          <div className="block-head">
            <div className="block-num"><span className="pill">06</span>The add-ons grid</div>
            <h2>Compliance, observability, and seats — <em>all priced separately.</em></h2>
          </div>
          <AddonsGrid />
        </div>
      </section>

      <section className="block" id="anecdotes">
        <div className="shell">
          <div className="block-head">
            <div className="block-num"><span className="pill">07</span>Receipts in the wild</div>
            <h2>Real bills <em>from real teams.</em></h2>
          </div>
          <Anecdotes />
        </div>
      </section>

      <section className="block" id="hobby-cap" style={{ background: "var(--bg-2)" }}>
        <div className="shell">
          <div className="block-head">
            <div className="block-num"><span className="pill">08</span>The hard cap</div>
            <h2>Hit 100 GB on Hobby — <em>and your site goes dark.</em></h2>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 60, alignItems: "center" }}>
            <div>
              <p style={{ fontSize: 17, lineHeight: 1.6, color: "var(--ink-2)", marginBottom: 24 }}>
                There is no overage option on Hobby. No throttling. No warning page. No "pay $5 to get back online." The deployment pauses until the 30-day rolling window resets.
              </p>
              <p style={{ fontSize: 17, lineHeight: 1.6, color: "var(--ink-2)", marginBottom: 24 }}>
                For a portfolio that gets unexpectedly Hacker News'd, that means hours of downtime during your highest-attention moment. For anything a real user depends on — even unpaid client work — this is a non-starter.
              </p>
              <p style={{ fontSize: 17, lineHeight: 1.6, color: "var(--ink-2)" }}>
                The only escape: upgrade to Pro and add a credit card to the uncapped meter.
              </p>
            </div>
            <div style={{ border: "1px solid var(--line)", padding: 32, fontFamily: "Geist Mono, monospace", fontSize: 13, lineHeight: 1.7 }}>
              <div style={{ color: "var(--warn)", marginBottom: 12 }}>$ curl -I https://your-portfolio.vercel.app</div>
              <div style={{ color: "var(--ink-3)" }}>HTTP/2 402 Payment Required</div>
              <div style={{ color: "var(--ink-3)" }}>x-vercel-error: DEPLOYMENT_PAUSED</div>
              <div style={{ color: "var(--ink-3)" }}>x-reason: hobby-tier-bandwidth-exceeded</div>
              <div style={{ color: "var(--ink-3)" }}>x-resets: 2026-05-12T00:00:00Z</div>
              <div style={{ color: "var(--ink-3)" }}>retry-after: 11d 14h 23m</div>
              <div style={{ marginTop: 20, color: "var(--ink-4)" }}>// approximate behavior — the user-facing<br />// page renders Vercel's "deployment paused" notice</div>
            </div>
          </div>
        </div>
      </section>

      <section className="shell" style={{ paddingTop: 30, paddingBottom: 0 }}>
        <aside className="disclaimer" aria-label="Methodology and corrections">
          <div className="disclaimer-eyebrow">Methodology &amp; Corrections</div>
          <p>
            This report was researched with assistance from <strong>Grok Deep Research</strong> and <strong>Perplexity</strong>, then cross-referenced against Vercel's published pricing pages and product documentation. All figures were verified against primary sources at the time of publication.
          </p>
          <p>
            Pricing is a moving target and AI-assisted research is fallible. If a figure no longer holds, or a claim appears inaccurate, please open an issue at{" "}
            <a href="https://github.com/bidah/theupsellgame" target="_blank" rel="noopener noreferrer">
              github.com/bidah/theupsellgame
            </a>
            . Corrections submitted in good faith will be reviewed and credited.
          </p>
        </aside>
      </section>

      <footer className="footer">
        <div className="shell">
          <div className="marks">
            <span>CREATED BY <a href="https://x.com/bidah" target="_blank" rel="noopener noreferrer">ROFI</a></span>
            <span>·</span>
            <span>▲ The Upsell Game</span>
            <span>·</span>
            <span>Created with <a href="https://designdotmd.directory" target="_blank" rel="noopener noreferrer">design.md directory</a></span>
            <span>·</span>
            <span>An editorial · not affiliated with Vercel, Inc.</span>
          </div>
          <p className="small">
            Sources: Vercel pricing & docs (vercel.com/pricing, vercel.com/docs), DeployWise, Costbench, CheckThat.ai, Schematic HQ, Kuberns, Flexprice, TrueFoundry, Get AI Perks, Deploy Handbook, HowdyGo case study. Pricing verified Apr 24, 2026 — figures may have shifted since publication. All numbers above are based on publicly published rates as of that date. This page exists to inform — read the docs before signing up.
          </p>
        </div>
      </footer>

      <TweaksPanel title="Tweaks">
        <TweakSection label="Team size">
          <TweakSlider
            label="Developer seats"
            min={1}
            max={50}
            value={tweaks.teamSize}
            unit=" devs"
            onChange={(v) => setTweak("teamSize", v)}
          />
          <div style={{ fontSize: 11, color: "var(--ink-4)", fontFamily: "Geist Mono, monospace", marginTop: 12, lineHeight: 1.5 }}>
            Pro is billed per developer seat. Adjust to see the seat tax compound across the table, calculator, and chart above.
          </div>
        </TweakSection>
      </TweaksPanel>
    </>
  );
}

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