// THE VAULT — premium experience. Hyro x Creatine $50k.
// Real mechanical concentric dial (drag with momentum, snap detents, tactile),
// cinematic vault-open sequence, dynamic animated states. Turnstile fixed
// (explicit render + token callback).
const { useState, useEffect, useRef, useCallback } = React;

const API = "";
const SITEKEY = "0x4AAAAAADk92IJUZ966T0Dm";

/* ------------------------------------------------------------------ */
/* Turnstile (explicit render, token via callback)                    */
/* ------------------------------------------------------------------ */
// Managed Turnstile. Renders a normal (small) widget that is invisible for
// most legit users and only shows a quick checkbox for suspicious sessions.
// Keeps the latest token; fresh() resets + waits for a new one. If it can't
// get a token, we surface the widget so the user can click to verify.
function useTurnstile() {
  const ref = useRef(null);
  const widgetId = useRef(null);
  const tokenRef = useRef("");
  const waiters = useRef([]);
  const [needsUI, setNeedsUI] = useState(false);
  const settle = (t) => {
    tokenRef.current = t || "";
    if (t) {
      const fns = waiters.current; waiters.current = [];
      fns.forEach((fn) => fn(t));
      setNeedsUI(false);
    }
  };
  useEffect(() => {
    let tries = 0;
    const mount = () => {
      if (!window.turnstile || !ref.current) {
        if (tries++ < 120) return setTimeout(mount, 120);
        return;
      }
      if (widgetId.current != null) return;
      try {
        widgetId.current = window.turnstile.render(ref.current, {
          sitekey: SITEKEY,
          theme: "dark",
          callback: (t) => settle(t),
          "error-callback": () => { tokenRef.current = ""; },
          "expired-callback": () => { tokenRef.current = ""; }
        });
      } catch (_) {}
    };
    mount();
  }, []);
  // Resolve with a token. If one is already cached, use it; else reset + wait.
  const fresh = useCallback(() => {
    return new Promise((resolve) => {
      if (tokenRef.current) { const t = tokenRef.current; tokenRef.current = ""; return resolve(t); }
      const ready = window.turnstile && widgetId.current != null;
      let done = false;
      const finish = (t) => { if (!done) { done = true; resolve(t || ""); } };
      waiters.current.push(finish);
      if (ready) { try { window.turnstile.reset(widgetId.current); } catch (_) {} }
      // if no token within 4s, reveal the widget so the user can verify
      setTimeout(() => { if (!done) setNeedsUI(true); }, 4000);
      setTimeout(() => finish(""), 12000);
    });
  }, []);
  return { ref, fresh, needsUI };
}

/* ------------------------------------------------------------------ */
/* Animated number (ticks up to target)                               */
/* ------------------------------------------------------------------ */
function Ticker({ value, dur = 900 }) {
  const [v, setV] = useState(0);
  const from = useRef(0);
  useEffect(() => {
    const start = performance.now();
    const a = from.current;
    const b = value;
    let raf;
    const tick = (now) => {
      const p = Math.min(1, (now - start) / dur);
      const e = 1 - Math.pow(1 - p, 3);
      setV(Math.round(a + (b - a) * e));
      if (p < 1) raf = requestAnimationFrame(tick);
      else from.current = b;
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [value, dur]);
  return <span>{v.toLocaleString()}</span>;
}

/* ------------------------------------------------------------------ */
/* Countdown                                                          */
/* ------------------------------------------------------------------ */
function Countdown({ target }) {
  const [now, setNow] = useState(Date.now());
  useEffect(() => {
    const t = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(t);
  }, []);
  let diff = Math.max(0, new Date(target).getTime() - now);
  const d = Math.floor(diff / 86400000); diff -= d * 86400000;
  const h = Math.floor(diff / 3600000); diff -= h * 3600000;
  const m = Math.floor(diff / 60000); diff -= m * 60000;
  const s = Math.floor(diff / 1000);
  const Unit = ({ v, l }) => (
    <div className="cd-unit">
      <div className="cd-num">{String(v).padStart(2, "0")}</div>
      <div className="cd-lbl">{l}</div>
    </div>
  );
  return (
    <div className="countdown">
      <Unit v={d} l="DAYS" /><span className="cd-sep">:</span>
      <Unit v={h} l="HRS" /><span className="cd-sep">:</span>
      <Unit v={m} l="MIN" /><span className="cd-sep">:</span>
      <Unit v={s} l="SEC" />
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Mechanical concentric dial — Canvas, drag w/ momentum + detents    */
/* Outer ring = digit 1 ... centre ring = digit 4.                    */
/* ------------------------------------------------------------------ */
function VaultDial({ onChange, disabled, spinning }) {
  const canvasRef = useRef(null);
  const wrapRef = useRef(null);
  // ring rotation angles (radians) — the COMMITTED selection. Never mutated by
  // the unlocking spin (that uses a separate visual offset).
  const ang = useRef([0, 0, 0, 0]);
  const vel = useRef([0, 0, 0, 0]);
  const active = useRef(-1);
  const last = useRef(null);
  const spinOff = useRef([0, 0, 0, 0]); // visual-only offset during unlocking
  const [digits, setDigits] = useState([0, 0, 0, 0]);
  const [activeRing, setActiveRing] = useState(0);
  const STEP = (Math.PI * 2) / 10;
  const radii = [0.92, 0.7, 0.49, 0.28]; // fraction of half-size, outer->inner
  const dpr = Math.min(window.devicePixelRatio || 1, 2);

  const digitOf = (i) => {
    const a = ((-ang.current[i] % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
    return Math.round(a / STEP) % 10;
  };
  // single source of truth: read live committed angles -> digits + code string
  const liveCode = () => [0, 1, 2, 3].map(digitOf).join("");
  const syncReadout = () => {
    const d = [0, 1, 2, 3].map(digitOf);
    setDigits((prev) => (prev.join("") === d.join("") ? prev : d));
    if (onChange) onChange(d.join(""));
  };

  // when unlocking starts/stops, reset the visual spin offset (keeps selection)
  useEffect(() => {
    if (!spinning) spinOff.current = [0, 0, 0, 0];
  }, [spinning]);

  // render loop
  useEffect(() => {
    let raf;
    const cv = canvasRef.current;
    const ctx = cv.getContext("2d");
    const draw = () => {
      const W = cv.width, H = cv.height, half = W / 2;
      ctx.clearRect(0, 0, W, H);
      ctx.save();
      ctx.translate(half, half);

      if (spinning) {
        // visual-only spin; does NOT touch committed ang.current
        for (let i = 0; i < 4; i++) spinOff.current[i] += 0.4 * (i % 2 ? -1 : 1);
      } else {
        // momentum + detent snapping on committed angles
        for (let i = 0; i < 4; i++) {
          if (active.current !== i) {
            if (Math.abs(vel.current[i]) > 0.0005) {
              ang.current[i] += vel.current[i];
              vel.current[i] *= 0.92;
            } else {
              vel.current[i] = 0;
              const target = Math.round(ang.current[i] / STEP) * STEP;
              ang.current[i] += (target - ang.current[i]) * 0.25;
            }
          }
        }
      }
      // keep readout locked to the live dial EVERY frame (never drifts)
      syncReadout();

      for (let i = 0; i < 4; i++) {
        const r = radii[i] * half;
        const inner = (i < 3 ? radii[i + 1] : 0.12) * half;
        const mid = (r + inner) / 2;
        const drawAng = ang.current[i] + (spinning ? spinOff.current[i] : 0);
        const isAct = activeRing === i && !disabled && !spinning;

        // ring band (brushed steel)
        const grad = ctx.createRadialGradient(0, 0, inner, 0, 0, r);
        grad.addColorStop(0, isAct ? "#3a0a0d" : "#241012");
        grad.addColorStop(0.5, isAct ? "#7a0d14" : "#3a1c1f");
        grad.addColorStop(1, isAct ? "#4a0a0e" : "#1c0c0e");
        ctx.beginPath();
        ctx.arc(0, 0, r, 0, Math.PI * 2);
        ctx.arc(0, 0, inner, 0, Math.PI * 2, true);
        ctx.fillStyle = grad;
        ctx.fill();

        // ring edge highlight
        ctx.lineWidth = 1.5;
        ctx.strokeStyle = isAct ? "rgba(255,244,236,.55)" : "rgba(255,244,236,.1)";
        ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.stroke();

        // numerals + tick detents
        for (let n = 0; n < 10; n++) {
          const a = n * STEP + drawAng - Math.PI / 2;
          const nx = Math.cos(a) * mid;
          const ny = Math.sin(a) * mid;
          // tick
          const tx1 = Math.cos(a) * (r - 3), ty1 = Math.sin(a) * (r - 3);
          const tx2 = Math.cos(a) * (r - 9), ty2 = Math.sin(a) * (r - 9);
          ctx.strokeStyle = "rgba(255,255,255,.12)";
          ctx.lineWidth = 1;
          ctx.beginPath(); ctx.moveTo(tx1, ty1); ctx.lineTo(tx2, ty2); ctx.stroke();
          // numeral, upright. highlight the digit currently at the top marker.
          const topDigit = ((Math.round((-drawAng) / STEP) % 10) + 10) % 10;
          const sel = topDigit === n;
          ctx.save();
          ctx.translate(nx, ny);
          ctx.rotate(a + Math.PI / 2);
          ctx.font = `${sel ? 700 : 500} ${Math.round(half * (i === 0 ? 0.072 : 0.06))}px 'Trebuchet MS',sans-serif`;
          ctx.textAlign = "center";
          ctx.textBaseline = "middle";
          ctx.fillStyle = sel ? "#FFF4EC" : "rgba(255,244,236,.4)";
          if (sel && isAct) { ctx.shadowColor = "rgba(255,244,236,.85)"; ctx.shadowBlur = 10; }
          ctx.fillText(String(n), 0, 0);
          ctx.restore();
        }
      }

      // centre hub
      const hub = ctx.createRadialGradient(-6, -6, 2, 0, 0, 0.16 * half);
      hub.addColorStop(0, "#ffe9dd");
      hub.addColorStop(1, "#5e0a0e");
      ctx.beginPath(); ctx.arc(0, 0, 0.13 * half, 0, Math.PI * 2);
      ctx.fillStyle = hub; ctx.fill();
      ctx.strokeStyle = "rgba(0,0,0,.5)"; ctx.lineWidth = 2; ctx.stroke();

      ctx.restore();

      // top marker
      ctx.fillStyle = "#FFF4EC";
      ctx.beginPath();
      ctx.moveTo(half - 10, 2); ctx.lineTo(half + 10, 2); ctx.lineTo(half, 20);
      ctx.closePath();
      ctx.shadowColor = "rgba(255,244,236,.85)"; ctx.shadowBlur = 8; ctx.fill();
      ctx.shadowBlur = 0;

      raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () => cancelAnimationFrame(raf);
  }, [activeRing, disabled, spinning]);

  // sizing
  useEffect(() => {
    const cv = canvasRef.current;
    const resize = () => {
      const s = Math.min(wrapRef.current.clientWidth, 360);
      cv.width = s * dpr; cv.height = s * dpr;
      cv.style.width = s + "px"; cv.style.height = s + "px";
    };
    resize();
    window.addEventListener("resize", resize);
    return () => window.removeEventListener("resize", resize);
  }, [dpr]);

  // pointer handling
  const ringAt = (px, py) => {
    const cv = canvasRef.current;
    const rect = cv.getBoundingClientRect();
    const half = rect.width / 2;
    const x = px - rect.left - half, y = py - rect.top - half;
    const d = Math.sqrt(x * x + y * y) / half;
    for (let i = 0; i < 4; i++) {
      const outer = radii[i], inner = i < 3 ? radii[i + 1] : 0.12;
      if (d <= outer && d > inner) return i;
    }
    return -1;
  };
  const angleAt = (px, py) => {
    const cv = canvasRef.current;
    const rect = cv.getBoundingClientRect();
    const half = rect.width / 2;
    return Math.atan2(py - rect.top - half, px - rect.left - half);
  };
  const onDown = (e) => {
    if (disabled || spinning) return;
    const p = e.touches ? e.touches[0] : e;
    const i = ringAt(p.clientX, p.clientY);
    if (i < 0) return;
    active.current = i;
    setActiveRing(i);
    last.current = { a: angleAt(p.clientX, p.clientY), t: performance.now() };
    vel.current[i] = 0;
  };
  const onMove = (e) => {
    if (active.current < 0) return;
    e.preventDefault();
    const p = e.touches ? e.touches[0] : e;
    const a = angleAt(p.clientX, p.clientY);
    let da = a - last.current.a;
    if (da > Math.PI) da -= Math.PI * 2;
    if (da < -Math.PI) da += Math.PI * 2;
    const i = active.current;
    ang.current[i] += da;
    const dt = Math.max(8, performance.now() - last.current.t);
    vel.current[i] = da / dt * 16;
    last.current = { a, t: performance.now() };
    syncReadout();
  };
  const onUp = () => {
    if (active.current < 0) return;
    active.current = -1;
  };
  useEffect(() => {
    const mv = (e) => onMove(e);
    const up = () => onUp();
    window.addEventListener("mousemove", mv);
    window.addEventListener("mouseup", up);
    window.addEventListener("touchmove", mv, { passive: false });
    window.addEventListener("touchend", up);
    return () => {
      window.removeEventListener("mousemove", mv);
      window.removeEventListener("mouseup", up);
      window.removeEventListener("touchmove", mv);
      window.removeEventListener("touchend", up);
    };
  });

  const nudge = (dir) => {
    const i = activeRing;
    ang.current[i] = Math.round(ang.current[i] / STEP) * STEP - dir * STEP;
    vel.current[i] = 0;
    syncReadout();
  };

  return (
    <div className="dial-block">
      <div className="dial-canvas-wrap" ref={wrapRef}>
        <canvas
          ref={canvasRef}
          className="dial-canvas"
          onMouseDown={onDown}
          onTouchStart={onDown}
        />
      </div>
      <div className="dial-readout">
        {digits.map((d, i) => (
          <span key={i} className={"ro-digit" + (activeRing === i ? " act" : "")}>{d}</span>
        ))}
      </div>
      <div className="dial-controls">
        <button className="nudge" onClick={() => nudge(-1)} disabled={disabled || spinning} aria-label="left">‹</button>
        <div className="ring-pick">
          {[0, 1, 2, 3].map((i) => (
            <button key={i} className={activeRing === i ? "on" : ""} onClick={() => setActiveRing(i)} disabled={disabled || spinning}>{i + 1}</button>
          ))}
        </div>
        <button className="nudge" onClick={() => nudge(1)} disabled={disabled || spinning} aria-label="right">›</button>
      </div>
      <div className="dial-hint">Drag a ring to spin it · outer ring is the first number</div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Live eliminated-codes wall                                         */
/* ------------------------------------------------------------------ */
function Wall({ open }) {
  const [items, setItems] = useState([]);
  const [count, setCount] = useState(0);
  const load = useCallback(() => {
    fetch(API + "/api/wall?limit=500").then((x) => x.json()).then((r) => {
      setItems((r.items || []).slice().reverse());
      setCount(r.count || 0);
    }).catch(() => {});
  }, []);
  useEffect(() => { load(); const t = setInterval(load, 8000); return () => clearInterval(t); }, [load]);
  if (!open) return null;
  return (
    <div className="wall">
      <div className="wall-head">
        <span className="wall-dot" /> ELIMINATED · <Ticker value={count} /> / 10,000
      </div>
      <div className="wall-grid">
        {items.map((it) => (<span key={it.code} className="wall-code">{it.code}</span>))}
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* Cinematic vault-open overlay (win)                                 */
/* ------------------------------------------------------------------ */
// Count-up hook: animates a number 0 -> target over `dur` ms once `run` is true.
function useCountUp(target, dur, run) {
  const [val, setVal] = useState(0);
  useEffect(() => {
    if (!run) return;
    let raf, start;
    const ease = (t) => 1 - Math.pow(1 - t, 4); // strong ease-out for a punchy finish
    const step = (ts) => {
      if (start == null) start = ts;
      const p = Math.min(1, (ts - start) / dur);
      setVal(Math.round(target * ease(p)));
      if (p < 1) raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    // Guarantee we always land on the exact target even if rAF timing stalls
    // (headless capture, background tab, reduced-motion).
    const guard = setTimeout(() => setVal(target), dur + 250);
    return () => { cancelAnimationFrame(raf); clearTimeout(guard); };
  }, [target, dur, run]);
  return val;
}


/* Decorative rivets evenly placed around the viewport edge. */
function Rivets() {
  const items = [];
  const n = 22;
  for (let i = 0; i < n; i++) {
    const t = i / n;
    items.push({ key: "t" + i, style: { top: "8px", left: `calc(${t * 100}% + 6px)` } });
    items.push({ key: "b" + i, style: { bottom: "8px", left: `calc(${t * 100}% + 6px)` } });
  }
  const m = 12;
  for (let i = 1; i < m; i++) {
    const t = i / m;
    items.push({ key: "l" + i, style: { left: "8px", top: `calc(${t * 100}% )` } });
    items.push({ key: "r" + i, style: { right: "8px", top: `calc(${t * 100}% )` } });
  }
  return (
    <div className="rivets" aria-hidden="true">
      {items.map((r) => <span key={r.key} className="rivet" style={r.style} />)}
    </div>
  );
}

/* Retracting bolts (animate out when .opening). */
function Bolts({ retracted }) {
  return (
    <div className={"bolts" + (retracted ? " retracted" : "")} aria-hidden="true">
      {/* left/right edge locking bolts */}
      <span className="bolt h l" style={{ top: "30%", left: "1.5%" }} />
      <span className="bolt h l" style={{ top: "50%", left: "1.5%" }} />
      <span className="bolt h l" style={{ top: "70%", left: "1.5%" }} />
      <span className="bolt h r" style={{ top: "30%", right: "1.5%" }} />
      <span className="bolt h r" style={{ top: "50%", right: "1.5%" }} />
      <span className="bolt h r" style={{ top: "70%", right: "1.5%" }} />
      {/* top/bottom locking bolts */}
      <span className="bolt v t" style={{ left: "32%", top: "2%" }} />
      <span className="bolt v t" style={{ left: "68%", top: "2%" }} />
      <span className="bolt v b" style={{ left: "32%", bottom: "2%" }} />
      <span className="bolt v b" style={{ left: "68%", bottom: "2%" }} />
    </div>
  );
}

/* The full-bleed vault DOOR. Renders the door surface, frame, rivets, bolts,
   the chamber behind, and the two splitting leaves. `opening` swings it open.
   Children render on the door face (the content layer). */
function Door({ stage, winner, children }) {
  // stage: 0 none, 1 tension, 2 bolts+rumble, 3 doors burst+zoom, 4 money, 5 winner/next
  const opening = stage >= 3;
  const cls =
    "vault-root" +
    (opening ? " opening" : "") +
    (stage >= 1 ? " win-stage stage-" + stage : "");
  return (
    <div className={cls}>
      {/* chamber behind the door (revealed on open) */}
      <div className="chamber">
        <div className="chamber-light" />
        {winner && (
          <WinReveal stage={stage} winner={winner} />
        )}
      </div>

      {/* door surface + content on its face */}
      <div className="door" />
      <div className="door-plate" />
      <div className="door-frame" />
      <Rivets />
      <Bolts retracted={stage >= 2} />

      {/* seam light leak during tension/rumble */}
      <div className="seam-leak" />

      {/* splitting leaves (cover the door, swing open) */}
      <div className="leaf left" />
      <div className="leaf right" />

      {/* white flash on burst */}
      <div className="burst-flash" />

      <div className="content">{children}</div>
    </div>
  );
}

/* Staged win reveal inside the chamber. */
function WinReveal({ stage, winner }) {
  const amount = 50000;
  const counted = useCountUp(amount, 1700, stage >= 4);
  const fmt = "$" + counted.toLocaleString("en-AU");
  return (
    <React.Fragment>
      <Confetti run={stage >= 4} />
      <div className="reveal">
        <div className="rv-kicker">THE VAULT IS CRACKED</div>
        <div className="rv-cash">
          <div className="rv-cash-label">YOU JUST WON</div>
          <div className="cash-amount">{fmt}</div>
          <div className="rv-aud">AUD CASH</div>
        </div>
        <div className="rv-code">WINNING CODE <b>{winner.code}</b></div>
        <div className="rv-winner">{winner.first} {winner.lastInitial}. · {winner.state}</div>
        <div className="rv-next">
          <div className="rv-next-title">WHAT HAPPENS NEXT</div>
          <ol className="rv-steps">
            <li><span className="rv-n">1</span> Check your inbox — we’ve emailed your winning address right now.</li>
            <li><span className="rv-n">2</span> Our team verifies your entry and ID within 48 hours.</li>
            <li><span className="rv-n">3</span> We organise your $50,000 payout. The money is yours.</li>
          </ol>
          <a className="cta" href="https://drinkhyro.com">BACK TO HYRO</a>
        </div>
      </div>
    </React.Fragment>
  );
}

/* Falling cash / confetti — red + cream only. */
function Confetti({ run }) {
  if (!run) return null;
  const pieces = Array.from({ length: 44 }, (_, i) => i);
  return (
    <div className="cine-confetti" aria-hidden="true">
      {pieces.map((i) => {
        const left = Math.random() * 100;
        const delay = Math.random() * 1.6;
        const dur = 2.6 + Math.random() * 2.4;
        const drift = (Math.random() * 2 - 1) * 90;
        const rot = (Math.random() * 2 - 1) * 720;
        const cash = i % 3 === 0;
        const cls = "conf" + (cash ? " conf-cash" : i % 2 ? " conf-cream" : " conf-red");
        const style = { left: left + "%", animationDelay: delay + "s", animationDuration: dur + "s", "--drift": drift + "px", "--rot": rot + "deg" };
        return <span key={i} className={cls} style={style}>{cash ? "$" : ""}</span>;
      })}
    </div>
  );
}

/* ------------------------------------------------------------------ */
/* App                                                                */
/* ------------------------------------------------------------------ */
function App() {
  const [state, setState] = useState(null);
  const refresh = useCallback(async () => {
    try { setState(await fetch(API + "/api/state").then((x) => x.json())); } catch (_) {}
  }, []);
  useEffect(() => { refresh(); const t = setInterval(refresh, 12000); return () => clearInterval(t); }, [refresh]);

  const [email, setEmail] = useState("");
  const [elig, setElig] = useState(null);
  const [phase, setPhase] = useState("idle");
  const [code, setCode] = useState("0000");
  const codeRef = useRef("0000");
  const setLiveCode = useCallback((c) => { codeRef.current = c; setCode(c); }, []);
  const [result, setResult] = useState(null);
  const [showWall, setShowWall] = useState(false);
  const [tries, setTries] = useState(0);
  const [meta, setMeta] = useState({ first: "", lastInitial: "", state: "" });
  const [winStage, setWinStage] = useState(0);
  const ts = useTurnstile();

  const opensAt = (state && state.opensAt) || "2026-07-24T00:00:00+10:00";
  const open = state && state.open;
  const won = state && state.won;
  const winner = (won && state.winner) || (result && result.result === "won" && result.winner) || null;

  // cinematic win timeline (video-like, step-by-step with zooms)
  useEffect(() => {
    if (!winner) return;
    const marks = [
      [1, 200],    // tension: darken, dial locks, "code is correct"
      [2, 1700],   // bolts retract + rumble + seam light leak
      [3, 3100],   // doors BURST open + white flash + zoom into chamber
      [4, 4200],   // money explosion: $50,000 count-up + confetti
      [5, 6400]    // winner + what-happens-next steps
    ];
    const timers = marks.map(([s, ms]) => setTimeout(() => setWinStage(s), ms));
    return () => timers.forEach(clearTimeout);
  }, [winner]);

  const checkEligibility = async () => {
    setPhase("checking");
    try {
      const r = await fetch(API + "/api/eligibility", {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email })
      }).then((x) => x.json());
      setElig(r); setPhase("ready");
    } catch (_) { setPhase("idle"); }
  };

  const submit = async () => {
    if (phase === "unlocking") return;
    const guess = codeRef.current;
    setPhase("unlocking");
    setResult(null);
    const token = await ts.fresh();
    await new Promise((r) => setTimeout(r, 2200));
    try {
      const r = await fetch(API + "/api/attempt", {
        method: "POST", headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, code: guess, turnstileToken: token, winnerMeta: meta })
      }).then((x) => x.json());
      setResult(r); setTries((t) => t + 1);
      if (r.result === "won") { refresh(); }
      else {
        setPhase("result");
        const e = await fetch(API + "/api/eligibility", {
          method: "POST", headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ email })
        }).then((x) => x.json());
        setElig(e);
      }
    } catch (_) { setPhase("ready"); }
  };

  /* ----- WON: door opens to reveal chamber ----- */
  if (winner) {
    return (
      <Door stage={winStage} winner={winner}>
        {winStage < 3 && (
          <div className="plate tension-plate">
            <div className="kicker">{winStage >= 1 ? "CODE ACCEPTED" : "VERIFYING…"}</div>
            <div className="tension-code">{winner.code}</div>
            <div className="unlocking-label">{winStage >= 2 ? "UNLOCKING THE VAULT…" : "· · ·"}</div>
          </div>
        )}
      </Door>
    );
  }

  /* ----- PRE-OPEN countdown ----- */
  if (!open) {
    return (
      <Door stage={0} winner={null}>
        <div className="kicker">DEATH TO TUB</div>
        <h1 className="headline">TUBBY'S $50,000 IS<br />LOCKED IN THE VAULT</h1>
        <p className="sub">One 4-digit code. One winner. The vault opens in</p>
        <div className="plate">
          <Countdown target={opensAt} />
        </div>
        <a className="cta" href="https://drinkhyro.com/products/hyro-lemon-lime-creatine">SECURE YOUR ENTRIES</a>
        <p className="fine">Every pouch of Hyro + Creatine = one unlock attempt.</p>
        <a className="tc-link" href="#tcs">Terms &amp; Conditions</a>
      </Door>
    );
  }

  /* ----- OPEN ----- */
  return (
    <Door stage={0} winner={null}>
      <div className="kicker">THE VAULT IS OPEN</div>

      {(phase === "idle" || phase === "checking") && (
        <div className="plate">
          <div className="entry">
            <h2>Enter your email to begin</h2>
            <input type="email" placeholder="you@email.com" value={email} onChange={(e) => setEmail(e.target.value)} />
            <button className="unlock-btn" disabled={!email.includes("@") || phase === "checking"} onClick={checkEligibility}>
              {phase === "checking" ? "CHECKING…" : "BEGIN"}
            </button>
          </div>
        </div>
      )}

      {elig && phase !== "idle" && phase !== "checking" && (
        <div className="plate">
          <div className="attempt-area">
            <div className={"attempts-left" + (elig.remaining > 0 ? "" : " none")}>
              {elig.remaining > 0 ? `${elig.remaining} attempt${elig.remaining === 1 ? "" : "s"} remaining` : "No attempts remaining"}
            </div>

            {elig.remaining > 0 && (
              <VaultDial onChange={setLiveCode} disabled={phase === "result"} spinning={phase === "unlocking"} />
            )}

            {phase === "ready" && elig.remaining > 0 && (
              <button className="unlock-btn big" onClick={submit}>UNLOCK</button>
            )}
            {phase === "unlocking" && <div className="unlocking-label">UNLOCKING…</div>}

            {phase === "result" && result && (
              <div className={"result " + result.result}>
                {result.result === "miss" && (
                  <React.Fragment>
                    <h3 className="r-miss">LOCKED</h3>
                    <p>{result.code} is wrong. It's now eliminated.</p>
                    {elig.remaining > 0
                      ? <button className="again-btn" onClick={() => setPhase("ready")}>TRY AGAIN</button>
                      : <p className="fine">You're out of attempts. Grab more pouches for more guesses.</p>}
                  </React.Fragment>
                )}
                {result.result === "duplicate" && (
                  <React.Fragment>
                    <h3 className="r-dup">ALREADY TRIED</h3>
                    <p>{result.code} is already on the wall. Pick another — no attempt used.</p>
                    <button className="again-btn" onClick={() => setPhase("ready")}>PICK ANOTHER</button>
                  </React.Fragment>
                )}
                {!["miss", "duplicate", "won"].includes(result.result) && (
                  <React.Fragment>
                    <h3 className="r-err">{result.error === "turnstile_failed" ? "VERIFY YOU'RE HUMAN" : "TRY AGAIN"}</h3>
                    <p>{result.result === "no_attempts" ? "No attempts left on this email." : result.result === "closed" ? "The vault is closed." : "Couldn't process that attempt."}</p>
                    <button className="again-btn" onClick={() => setPhase("ready")}>RETRY</button>
                  </React.Fragment>
                )}
              </div>
            )}

            {tries > 0 && (
              <a className="view-wall" onClick={() => setShowWall((v) => !v)}>
                {showWall ? "Hide eliminated codes" : "View other attempts"}
              </a>
            )}
          </div>
        </div>
      )}

      <div className={"ts-wrap" + (ts.needsUI ? " needs" : "")}>
        <div ref={ts.ref} />
        {ts.needsUI && <div className="ts-hint">Tap to verify, then UNLOCK again.</div>}
      </div>
      <Wall open={showWall} />
      <a className="tc-link" href="#tcs">Terms &amp; Conditions</a>
    </Door>
  );
}

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