// VELOUR marketing site — the "Begin" onboarding. Six stages: your name, what you
// want to be different (goals → pillars), how your days feel (tone), your first
// ritual, a short composing sequence, then a day-one dashboard shaped by the
// answers. On the fresh (new-user) site the answers persist: the name/goals/tone
// land in velour_fresh_onboarding_v1 and the chosen ritual is seeded into the
// shared rituals template, so the real dashboard opens already theirs. On the
// demo site it stays pure theater. Opened anywhere via window.velourOpenSignup().
//
// IT ASKS FOR THE TERMS NOW (2 Sep 2026). Until then this door collected a name
// and an email address, created a real account, and never mentioned the Privacy
// Policy or the Terms — which were written, wired and reachable the whole time.
// VELOUR was taking money against documents no member had accepted and holding no
// record that any of them had. The box is unticked by default, gates the button,
// and is re-checked server-side in api/signup.js, because a control enforced only
// where it is drawn is the same "presentation, not protection" gap the plan cap
// had. The acceptance is recorded on the account by migration 016.
// Mobile has the identical flow in src/ui_kits/app/LandingScreen.jsx.
const { Button } = window.VelourDesignSystem_380ed4;

function signupDateLabel() {
  // Same "today" the app uses everywhere: offset-corrected, then derived from the
  // local calendar date, so the day-one date always matches the dashboard's header.
  const d = new Date();
  const off = Number(window.VELOUR_DAY_OFFSET) || 0;
  if (off) d.setDate(d.getDate() + off);
  return new Date(window.velourDateISO(d) + "T00:00:00").toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" });
}

// Persist the onboarding answers into the fresh account (fresh pages only).
// Mirrors the schema DashRituals/app.jsx read: a sequences template
// (velour_fresh_rituals_sequences) whose habits the Today/Rituals views hydrate.
const ONBOARDING_KEY = "velour_fresh_onboarding_v1";
function persistOnboarding({ name, goals, feel, ritual }) {
  if (!window.VELOUR_FRESH) return;
  try {
    localStorage.setItem(ONBOARDING_KEY, JSON.stringify({ name, goals, feel, ritual, ts: Date.now() }));
    if (!ritual) return;
    const SEQ_KEY = "velour_fresh_rituals_sequences";
    let seqs = null;
    try { seqs = JSON.parse(localStorage.getItem(SEQ_KEY) || "null"); } catch (e) {}
    if (!seqs || !seqs.length) {
      seqs = [
        { id: "morning", name: "Morning ritual", time: "7:00 AM", icon: "sun", habits: [] },
        { id: "evening", name: "Evening wind-down", time: "9:30 PM", icon: "moon-stars", habits: [] },
      ];
    }
    const exists = seqs.some((s) => (s.habits || []).some((h) => h.label === ritual));
    if (exists) return;
    const evening = /\bphone\b|\bscreens?\b|\bbed(room)?\b|\bnight\b|wind.?down/i.test(ritual);
    const icon = /page|journal|writ|line/i.test(ritual) ? "notebook"
      : /\bphone\b|\bscreens?\b/i.test(ritual) ? "phone-x"
      : /walk|step/i.test(ritual) ? "footprints"
      : /stretch|move/i.test(ritual) ? "sun"
      : "sparkle";
    const target = seqs.find((s) => s.id === (evening ? "evening" : "morning")) || seqs[0];
    target.habits = [...(target.habits || []), { id: "h-onb-" + Date.now().toString(36), libId: null, label: ritual, icon, streak: 0 }];
    localStorage.setItem(SEQ_KEY, JSON.stringify(seqs));
  } catch (e) {}
}

const SEQ_STEP_MS = 2150;

// What you'd like to be different — each choice maps to a pillar the day-one
// dashboard brings forward.
const GOAL_OPTIONS = [
  { key: "rituals", color: "var(--sage)", label: "Mornings that hold their shape", sub: "Routines you return to" },
  { key: "wellness", color: "var(--teal)", label: "Sleep and energy that restore", sub: "Rest, movement, water" },
  { key: "wardrobe", color: "var(--amber)", label: "A closet that earns its keep", sub: "Fewer, better, worn" },
  { key: "efficiency", color: "var(--terracotta)", label: "Fewer hours lost to noise", sub: "Time and money, kept" },
];

// How the days feel — sets the tone of the day-one welcome, never a diagnosis.
const FEEL_OPTIONS = [
  { key: "full", label: "Full, but scattered", lede: "We'll begin by giving the fullness a shape." },
  { key: "quiet", label: "Quiet, but drifting", lede: "We'll begin with one anchor to build the day around." },
  { key: "heavy", label: "Structured, but heavy", lede: "We'll keep the structure and set down some of the weight." },
  { key: "varies", label: "Honestly, it varies", lede: "Then we'll build something steady enough to meet any day." },
];

const RITUAL_OPTIONS = [
  "Morning pages, 10 minutes",
  "Stretch before coffee",
  "Phone stays outside the bedroom",
  "A ten-minute walk, no headphones",
];

const PILLAR_META = {
  rituals: { name: "Rituals", color: "var(--sage)" },
  wellness: { name: "Wellness", color: "var(--teal)" },
  wardrobe: { name: "Wardrobe", color: "var(--amber)" },
  efficiency: { name: "Efficiency", color: "var(--terracotta)" },
};

const QUESTION_STAGES = ["name", "goals", "feel", "ritual"];

function SignupOverlay({ open, onClose, onNavigate }) {
  const [stage, setStage] = React.useState("name");
  const [name, setName] = React.useState("");
  const [userName, setUserName] = React.useState("Jennifer");
  const [goals, setGoals] = React.useState([]);
  const [feel, setFeel] = React.useState(null);
  const [ritual, setRitual] = React.useState(null);
  const [customRitual, setCustomRitual] = React.useState("");
  const [seqIdx, setSeqIdx] = React.useState(-1);
  const [seqVisible, setSeqVisible] = React.useState(false);
  const [cardsIn, setCardsIn] = React.useState(0);
  const [email, setEmail] = React.useState("");
  // Unticked by default, and it stays that way. A box that arrives already
  // ticked records that a page was rendered, not that a person agreed — which is
  // the whole difference between having this record and not having it.
  const [acceptedTerms, setAcceptedTerms] = React.useState(false);
  const [createState, setCreateState] = React.useState("idle"); // idle | creating | sent | error
  const [createError, setCreateError] = React.useState(null);
  const nameRef = React.useRef(null);
  const timers = React.useRef([]);
  const reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  const t = (fn, ms) => { timers.current.push(setTimeout(fn, ms)); };
  const clearTimers = () => { timers.current.forEach(clearTimeout); timers.current = []; };

  React.useEffect(() => {
    if (open) {
      setStage("name"); setName(""); setGoals([]); setFeel(null); setRitual(null); setCustomRitual("");
      setSeqIdx(-1); setSeqVisible(false); setCardsIn(0);
      // Reopening the flow is a fresh start, and that has to include the box:
      // a stale tick carried over from a previous visit is an acceptance nobody
      // made this time.
      setEmail(""); setAcceptedTerms(false); setCreateState("idle"); setCreateError(null);
      document.body.style.overflow = "hidden";
      t(() => nameRef.current && nameRef.current.focus(), 400);
    } else {
      document.body.style.overflow = "";
      clearTimers();
    }
    return () => { document.body.style.overflow = ""; clearTimers(); };
  }, [open]);

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  // CAPITALISE A NAME, NEVER A STAND-IN. The fallback used to run through the
  // same title-casing as a real name, so a member who left the field blank was
  // greeted "Welcome, There." and "Good afternoon, There." — as though There
  // were their name. Only something the member actually typed gets the capital.
  // The web fallback was "Jennifer" — the demo persona — so a real member who
  // skipped the field was greeted by the name of the woman in the mockups. On
  // the fresh surface the stand-in is now the word "there"; the demo keeps
  // Jennifer, because there it IS her account.
  const submitName = () => {
    const typed = name.trim().replace(/[<>]/g, "");
    const fallback = window.VELOUR_FRESH ? "there" : "Jennifer";
    const n = typed ? typed.charAt(0).toUpperCase() + typed.slice(1) : fallback;
    setUserName(n);
    setStage("goals");
  };

  const chosenRitual = ritual === "__custom" ? (customRitual.trim() || "A ritual of your own") : ritual;

  // The composing sequence — reflects the answers back, one line at a time.
  const runSequence = () => {
    setStage("seq");
    const lines = 4;
    const step = reduced ? 350 : SEQ_STEP_MS;
    for (let i = 0; i < lines; i++) {
      if (i > 0) t(() => setSeqVisible(false), i * step);
      t(() => { setSeqIdx(i); setSeqVisible(true); }, i * step + (i === 0 ? 60 : 450));
    }
    t(() => {
      setStage("dash");
      for (let i = 0; i < 4; i++) t(() => setCardsIn(i + 1), 250 + i * 180);
    }, lines * step + (reduced ? 200 : 900));
  };

  // The real account, created only here — after every question is answered.
  // Server-side: see api/signup.js and the note at the top of this file for why
  // the door with the questions is now the only one that makes accounts.
  const createAccount = () => {
    if (createState === "creating") return;
    const address = email.trim();
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(address)) {
      setCreateError("That doesn't look like an email address.");
      setCreateState("error");
      return;
    }
    // The button is disabled without this, so reaching here untickled means the
    // disabled state was got round rather than that the member agreed. Say so
    // plainly instead of sending a request the server will refuse anyway.
    if (!acceptedTerms) {
      setCreateError("The terms and privacy policy need accepting first.");
      setCreateState("error");
      return;
    }
    setCreateState("creating"); setCreateError(null);
    fetch("/api/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      // Send the page we are on so the link comes back here. VELOUR has two entry
      // files and they read different storage, so landing on the wrong one shows
      // the demo's data instead of the member's empty account.
      // The browser's own zone, sent once, at the only moment the server can learn
      // it. It decides which calendar day a receipt names — see api/signup.js
      // cleanZone and migration 010. Wrapped because Intl can be absent or
      // restricted in hardened browsers, and a missing zone is fine: receipts fall
      // back to UTC, exactly as they did before this existed.
      // The fact of the acceptance, and nothing else about it. The version and
      // the timestamp are the server's to decide — see TERMS_VERSION in
      // api/signup.js and the trigger in migration 016.
      // THE ANSWERS GO TO THE SERVER TOO, NOT ONLY TO localStorage.
      // public.onboarding_answers has existed since migration 001 and the admin
      // member panel renders it as "What they told us at onboarding" — but
      // nothing had ever written a row, so that panel read "—" for every real
      // member and only the demo seed ever filled it. The three answers were
      // already in hand here; they were simply never sent.
      body: JSON.stringify({
        email: address,
        name: userName,
        acceptedTerms: true,
        timezone: (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || null; } catch (e) { return null; } })(),
        redirectTo: location.origin + location.pathname,
        onboarding: {
          goals: picked,
          feel: feel,
          ritual: chosenRitual,
          ritualIsCustom: ritual === "__custom",
        },
      }),
    }).then((r) => r.json().catch(() => ({})).then((data) => {
      if (!r.ok) throw new Error(data.error || "Could not create that account just now.");
      return data;
    })).then(() => {
      persistOnboarding({ name: userName, goals: picked, feel, ritual: chosenRitual });
      setCreateState("sent");
    }).catch((e) => {
      setCreateError(e.message || "Could not create that account just now.");
      setCreateState("error");
    });
  };

  if (!open) return null;

  const picked = goals.length ? goals : ["rituals"];
  const pillarNames = picked.map((g) => PILLAR_META[g].name);
  const seqLines = [
    `Welcome, ${userName}.`,
    `Setting your first ritual — ${(chosenRitual || "one small act").toLowerCase()}…`,
    pillarNames.length === 1 ? `Bringing ${pillarNames[0]} forward…`
      : pillarNames.length === 4 ? "Bringing all four pillars forward…"
      : `Bringing ${pillarNames.slice(0, -1).join(", ")} and ${pillarNames[pillarNames.length - 1]} forward…`,
    "Composing your first week…",
  ];
  const hour = new Date().getHours();
  const daypart = hour < 12 ? "morning" : hour < 18 ? "afternoon" : "evening";
  const feelOpt = FEEL_OPTIONS.find((f) => f.key === feel);
  const stepIdx = QUESTION_STAGES.indexOf(stage);
  const pipColors = ["var(--sage)", "var(--teal)", "var(--amber)", "var(--terracotta)"];

  // Day-one cards, goal pillars first — each chosen focus carries a quiet tag.
  const baseCards = [
    { key: "rituals", num: "1", unit: "ritual set", line: chosenRitual ? `${chosenRitual} — your first check-in starts the ring.` : "Your first check-in starts the ring." },
    { key: "wellness", num: "0", unit: "of 100", line: "Your score composes itself over seven days." },
    { key: "wardrobe", num: "0", unit: "pieces", line: "Add your capsule. Every wear lowers the cost." },
    { key: "efficiency", num: "$0", unit: "kept", line: "Time and money start counting the moment you do." },
  ];
  const dashCards = [...baseCards].sort((a, b) => (picked.includes(b.key) ? 1 : 0) - (picked.includes(a.key) ? 1 : 0));

  const optionBtn = (selected, color) => ({
    display: "flex", alignItems: "flex-start", gap: 14, width: "min(460px, 88vw)", textAlign: "left",
    padding: "16px 18px", borderRadius: 14, cursor: "pointer", fontFamily: "var(--font-sans)",
    border: `1.5px solid ${selected ? color : "var(--border)"}`,
    background: selected ? `color-mix(in srgb, ${color} 7%, transparent)` : "var(--surface-card)",
    transition: "border-color .3s cubic-bezier(.16,1,.3,1), background .3s cubic-bezier(.16,1,.3,1), transform .3s cubic-bezier(.16,1,.3,1)",
  });

  const backBtn = (to) => (
    <button onClick={() => setStage(to)} style={{ border: "none", background: "none", cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: 13, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--text-tertiary)", marginTop: 30 }}>Back</button>
  );

  const stageWrap = { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "72px 24px 40px", textAlign: "center" };
  const stageH2 = { fontFamily: "var(--font-serif)", fontWeight: 500, fontSize: "clamp(28px, 4.6vw, 42px)", lineHeight: 1.15, maxWidth: "22ch", color: "var(--text-primary)", margin: "26px 0 0" };
  const stageSub = { color: "var(--text-tertiary)", marginTop: 14, maxWidth: "46ch", fontSize: 15.5, lineHeight: 1.6 };

  // Progress pips across the four questions.
  const progress = stepIdx >= 0 && (
    <div aria-label={`Step ${stepIdx + 1} of 4`} style={{ display: "flex", gap: 10, alignItems: "center" }}>
      {QUESTION_STAGES.map((_, j) => (
        <i key={j} style={{ width: 8, height: 8, borderRadius: "50%", background: j <= stepIdx ? pipColors[j] : "var(--border)", transform: j === stepIdx ? "scale(1.3)" : "none", transition: "background .5s cubic-bezier(.16,1,.3,1), transform .5s cubic-bezier(.16,1,.3,1)" }}></i>
      ))}
    </div>
  );

  return (
    <div role="dialog" aria-modal="true" aria-label="Begin with VELOUR" style={{ position: "fixed", inset: 0, zIndex: 10000, background: "var(--bg-page)", display: "flex", flexDirection: "column", overflowY: "auto" }}>
      <style>{`
        @keyframes vlsStageIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
        .vls-stage { animation: vlsStageIn .8s cubic-bezier(.16,1,.3,1); }
        .vls-name { border: none; border-bottom: 1.5px solid var(--border); background: transparent; font-family: var(--font-serif); font-size: clamp(26px, 4vw, 36px); text-align: center; padding: 8px 4px; width: min(420px, 86vw); color: var(--text-primary); transition: border-color .4s; }
        .vls-name:focus { outline: none; border-color: var(--ink); }
        .vls-name::placeholder { color: var(--border); }
        @media (hover: hover) and (pointer: fine) { .vls-opt:hover { transform: translateY(-2px); } }
        @media (prefers-reduced-motion: reduce) { .vls-stage { animation: none; } .vls-opt:hover { transform: none; } }
      `}</style>
      <button onClick={onClose} style={{ position: "fixed", top: 24, right: 28, zIndex: 10010, border: "none", background: "none", cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: 13, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--text-tertiary)" }}>Close</button>

      {stage === "name" && (
        <div className="vls-stage" style={stageWrap}>
          <window.Wordmark size={12} />
          <div style={{ marginTop: 30 }}>{progress}</div>
          <h2 style={stageH2}>Before anything is tracked,<br />one question.</h2>
          <p style={stageSub}>VELOUR speaks to you by name. The rest of the setup is three quiet questions — under a minute, and nothing is graded.</p>
          <input ref={nameRef} className="vls-name" type="text" placeholder="Your first name" autoComplete="given-name" maxLength={24} value={name} onChange={(e) => setName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") submitName(); }} style={{ marginTop: 40 }} />
          <div style={{ marginTop: 38 }}><Button size="lg" iconRight="arrow-right" onClick={submitName}>Begin</Button></div>
        </div>
      )}

      {stage === "goals" && (
        <div className="vls-stage" style={stageWrap}>
          {progress}
          <h2 style={stageH2}>What would you like to be different, {userName}?</h2>
          <p style={stageSub}>Choose any that apply. VELOUR shapes itself around what you pick — nothing else gets loud.</p>
          <div style={{ display: "grid", gap: 12, marginTop: 36 }}>
            {GOAL_OPTIONS.map((g) => {
              const on = goals.includes(g.key);
              return (
                <button key={g.key} className="vls-opt" aria-pressed={on} onClick={() => setGoals((gs) => on ? gs.filter((x) => x !== g.key) : [...gs, g.key])} style={optionBtn(on, g.color)}>
                  <span aria-hidden="true" style={{ width: 9, height: 9, borderRadius: "50%", background: g.color, marginTop: 6, flexShrink: 0, transform: on ? "scale(1.4)" : "none", transition: "transform .3s cubic-bezier(.16,1,.3,1)" }}></span>
                  <span>
                    <span style={{ display: "block", fontSize: 15.5, fontWeight: 500, color: "var(--text-primary)" }}>{g.label}</span>
                    <span style={{ display: "block", fontSize: 13, color: "var(--text-tertiary)", marginTop: 3 }}>{g.sub}</span>
                  </span>
                </button>
              );
            })}
          </div>
          <div style={{ marginTop: 36 }}>
            <Button size="lg" iconRight="arrow-right" disabled={goals.length === 0} onClick={() => goals.length && setStage("feel")}>{goals.length ? "Continue" : "Choose at least one"}</Button>
          </div>
          {backBtn("name")}
        </div>
      )}

      {stage === "feel" && (
        <div className="vls-stage" style={stageWrap}>
          {progress}
          <h2 style={stageH2}>How do your days feel right now?</h2>
          <p style={stageSub}>There's no wrong answer. This only sets the tone of your first week.</p>
          <div style={{ display: "grid", gap: 12, marginTop: 36 }}>
            {FEEL_OPTIONS.map((f) => (
              <button key={f.key} className="vls-opt" aria-pressed={feel === f.key} onClick={() => { setFeel(f.key); t(() => setStage("ritual"), reduced ? 80 : 420); }} style={optionBtn(feel === f.key, "var(--ink)")}>
                <span style={{ display: "block", fontSize: 15.5, fontWeight: 500, color: "var(--text-primary)", fontStyle: "italic", fontFamily: "var(--font-serif)" }}>{f.label}</span>
              </button>
            ))}
          </div>
          {backBtn("goals")}
        </div>
      )}

      {stage === "ritual" && (
        <div className="vls-stage" style={stageWrap}>
          {progress}
          <h2 style={stageH2}>Choose your first ritual. Just one.</h2>
          <p style={stageSub}>Small on purpose. The rest can come later — this one starts the hold.</p>
          <div style={{ display: "grid", gap: 12, marginTop: 36 }}>
            {RITUAL_OPTIONS.map((r) => (
              <button key={r} className="vls-opt" aria-pressed={ritual === r} onClick={() => setRitual(r)} style={optionBtn(ritual === r, "var(--sage)")}>
                <span aria-hidden="true" style={{ width: 20, height: 20, borderRadius: "50%", border: `1.5px solid ${ritual === r ? "var(--sage)" : "var(--border)"}`, background: ritual === r ? "var(--sage)" : "transparent", flexShrink: 0, display: "grid", placeItems: "center", transition: "background .3s, border-color .3s" }}>
                  <svg width="11" height="9" viewBox="0 0 11 9" fill="none" style={{ opacity: ritual === r ? 1 : 0, transition: ".3s" }}><path d="M1 4.5L4 7.5L10 1" stroke="#F7F4EF" strokeWidth="1.8" strokeLinecap="round" /></svg>
                </span>
                <span style={{ fontSize: 15.5, color: "var(--text-primary)" }}>{r}</span>
              </button>
            ))}
            <button className="vls-opt" aria-pressed={ritual === "__custom"} onClick={() => setRitual("__custom")} style={optionBtn(ritual === "__custom", "var(--sage)")}>
              <span aria-hidden="true" style={{ width: 20, height: 20, borderRadius: "50%", border: `1.5px solid ${ritual === "__custom" ? "var(--sage)" : "var(--border)"}`, background: ritual === "__custom" ? "var(--sage)" : "transparent", flexShrink: 0 }}></span>
              <span style={{ width: "100%" }}>
                <span style={{ display: "block", fontSize: 15.5, color: "var(--text-primary)" }}>Write your own</span>
                {ritual === "__custom" && (
                  <input autoFocus type="text" maxLength={48} placeholder="One small act, in your words" value={customRitual}
                    onChange={(e) => setCustomRitual(e.target.value)}
                    onKeyDown={(e) => { if (e.key === "Enter" && customRitual.trim()) runSequence(); }}
                    onClick={(e) => e.stopPropagation()}
                    style={{ marginTop: 10, width: "100%", border: "none", borderBottom: "1.5px solid var(--border)", background: "transparent", fontFamily: "var(--font-serif)", fontSize: 17, padding: "4px 2px", color: "var(--text-primary)", outline: "none" }} />
                )}
              </span>
            </button>
          </div>
          <div style={{ marginTop: 36 }}>
            <Button size="lg" iconRight="arrow-right" disabled={!ritual || (ritual === "__custom" && !customRitual.trim())} onClick={() => (ritual && !(ritual === "__custom" && !customRitual.trim())) && runSequence()}>Set this ritual</Button>
          </div>
          {backBtn("feel")}
        </div>
      )}

      {stage === "seq" && (
        <div className="vls-stage" style={{ ...stageWrap, padding: "40px 24px" }}>
          <p style={{ fontFamily: "var(--font-serif)", fontSize: "clamp(22px, 3.4vw, 32px)", fontStyle: "italic", minHeight: "3em", maxWidth: "30ch", lineHeight: 1.4, color: "var(--text-primary)", margin: 0 }}>
            <span style={{ opacity: seqVisible ? 1 : 0, transition: "opacity 1s cubic-bezier(.16,1,.3,1)" }}>{seqIdx >= 0 ? seqLines[seqIdx] : ""}</span>
          </p>
          <div aria-hidden="true" style={{ display: "flex", gap: 10, marginTop: 52 }}>
            {pipColors.map((c, j) => (
              <i key={j} style={{ width: 8, height: 8, borderRadius: "50%", background: j <= seqIdx ? c : "var(--border)", transform: j <= seqIdx ? "scale(1.25)" : "none", transition: "background .6s cubic-bezier(.16,1,.3,1), transform .6s cubic-bezier(.16,1,.3,1)" }}></i>
            ))}
          </div>
        </div>
      )}

      {stage === "dash" && (
        <div className="vls-stage" style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "flex-start" }}>
          <div style={{ width: "100%", maxWidth: 980, margin: "0 auto", padding: "96px 24px 70px", textAlign: "left" }}>
            <p style={{ color: "var(--text-tertiary)", fontSize: 13, letterSpacing: "0.14em", textTransform: "uppercase", margin: 0 }}>{signupDateLabel()} · Day one</p>
            <h2 style={{ fontFamily: "var(--font-serif)", fontWeight: 500, fontSize: "clamp(30px, 4.6vw, 48px)", lineHeight: 1.12, color: "var(--text-primary)", margin: "12px 0 0" }}>Good {daypart}, {userName}.</h2>
            <p style={{ fontFamily: "var(--font-serif)", fontStyle: "italic", color: "var(--text-tertiary)", marginTop: 22, fontSize: "clamp(16px, 2vw, 20px)", maxWidth: "50ch" }}>
              {feelOpt ? feelOpt.lede : "This is day one."} Nothing to measure yet — which means everything from here is compounding.
            </p>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 18, marginTop: 46 }}>
              {dashCards.map((c, i) => {
                const meta = PILLAR_META[c.key];
                const focused = picked.includes(c.key);
                return (
                  <div key={c.key} style={{
                    position: "relative", overflow: "hidden", border: `1px solid ${focused ? `color-mix(in srgb, ${meta.color} 45%, var(--border))` : "var(--border)"}`, borderRadius: 20,
                    background: "var(--surface-card)", padding: "26px 26px 24px",
                    opacity: cardsIn > i ? 1 : 0, transform: cardsIn > i ? "none" : "translateY(22px)",
                    transition: "opacity .8s cubic-bezier(.16,1,.3,1), transform .8s cubic-bezier(.16,1,.3,1)",
                  }}>
                    <span aria-hidden="true" style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: 3, background: meta.color }}></span>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                      <window.Eyebrow color={meta.color}>{meta.name}</window.Eyebrow>
                      {focused && <span style={{ fontSize: 10.5, letterSpacing: "0.12em", textTransform: "uppercase", color: meta.color, border: `1px solid color-mix(in srgb, ${meta.color} 40%, transparent)`, borderRadius: 999, padding: "3px 10px" }}>First focus</span>}
                    </div>
                    <div style={{ fontFamily: "var(--font-serif)", fontWeight: 500, fontSize: 46, lineHeight: 1, margin: "16px 0 4px", color: "var(--text-primary)" }}>
                      {c.num}<small style={{ fontSize: "0.4em", color: "var(--text-tertiary)", fontFamily: "var(--font-sans)", letterSpacing: "0.1em", textTransform: "uppercase", marginLeft: 6 }}>{c.unit}</small>
                    </div>
                    <p style={{ fontSize: 14, color: "var(--text-tertiary)", marginTop: 8, fontStyle: "italic", fontFamily: "var(--font-serif)" }}>{c.line}</p>
                  </div>
                );
              })}
            </div>
            <div style={{ marginTop: 56, textAlign: "center" }}>
              {window.VELOUR_FRESH ? (
                createState === "sent" ? (
                  <React.Fragment>
                    <div style={{ fontFamily: "var(--font-serif)", fontSize: 22, color: "var(--text-primary)", letterSpacing: "-0.01em", marginBottom: 10 }}>Check your email.</div>
                    <p style={{ fontSize: 14.5, color: "var(--text-secondary)", lineHeight: 1.6, maxWidth: 420, margin: "0 auto" }}>
                      A sign-in link is on its way to <span style={{ color: "var(--text-primary)" }}>{email}</span>. Open it on this device and your space is ready.
                    </p>
                  </React.Fragment>
                ) : (
                  <React.Fragment>
                    <label htmlFor="vls-signup-email" style={{ fontSize: 11.5, letterSpacing: "0.04em", color: "var(--text-tertiary)", marginBottom: 8, display: "block" }}>Your email, so this can follow you</label>
                    <input id="vls-signup-email" type="email" autoComplete="email" value={email}
                      onChange={(e) => { setEmail(e.target.value); if (createState === "error") setCreateState("idle"); }}
                      onKeyDown={(e) => { if (e.key === "Enter") createAccount(); }}
                      placeholder="you@example.com"
                      style={{ width: "min(340px, 88vw)", padding: "13px 15px", borderRadius: 12, border: "var(--border-width) solid var(--border)", background: "var(--surface-card)", fontFamily: "var(--font-sans)", fontSize: 15, color: "var(--text-primary)", outline: "none", boxSizing: "border-box", textAlign: "center", margin: "0 auto", display: "block" }} />
                    {createState === "error" && (
                      <p style={{ fontSize: 13.5, color: "var(--text-secondary)", marginTop: 12 }}>{createError}</p>
                    )}
                    {/* The documents open in a NEW TAB. Navigating this one would
                        unmount the overlay and take six stages of answers with it —
                        the member would come back from reading the terms to an empty
                        form. A plain anchor rather than window.open, because an
                        anchor is a user gesture and no popup blocker sits between a
                        member and the thing they are being asked to agree to. */}
                    <label style={{ display: "flex", alignItems: "flex-start", gap: 10, maxWidth: "min(340px, 88vw)", margin: "20px auto 0", textAlign: "left", cursor: "pointer" }}>
                      <input type="checkbox" checked={acceptedTerms}
                        onChange={(e) => { setAcceptedTerms(e.target.checked); if (createState === "error") setCreateState("idle"); }}
                        style={{ width: 16, height: 16, marginTop: 2, accentColor: "var(--ink)", flexShrink: 0, cursor: "pointer" }} />
                      <span style={{ fontSize: 13.5, lineHeight: 1.5, color: "var(--text-secondary)" }}>
                        I agree to the <a href={window.velourLegalUrl("terms")} target="_blank" rel="noopener noreferrer" style={{ color: "var(--text-primary)" }}>Terms</a> and the <a href={window.velourLegalUrl("privacy")} target="_blank" rel="noopener noreferrer" style={{ color: "var(--text-primary)" }}>Privacy Policy</a>.
                      </span>
                    </label>
                    <div style={{ marginTop: 22 }}>
                      <Button size="lg" iconRight="arrow-right" disabled={createState === "creating" || !email.trim() || !acceptedTerms} onClick={createAccount}>
                        {createState === "creating" ? "Creating…" : "Enter your space"}
                      </Button>
                    </div>
                    <p style={{ color: "var(--text-tertiary)", fontSize: 13.5, marginTop: 16 }}>Free to start, and free to stay. No card, no countdown.</p>
                  </React.Fragment>
                )
              ) : (
                <React.Fragment>
                  <Button size="lg" iconRight="arrow-right" onClick={() => { onClose(); onNavigate && onNavigate("dashboard"); }}>Enter your space</Button>
                  <p style={{ color: "var(--text-tertiary)", fontSize: 13.5, marginTop: 16 }}>Free to start, and free to stay. No card, no countdown.</p>
                </React.Fragment>
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

window.SignupOverlay = SignupOverlay;
