/* global React, Button, Eyebrow, Wordmark, Ostrich, Icon, sbx, signOut, getRefVia, lookupReferralCode, setTypedReferralCode, normalizeReferralCode */
// Welcome / Sign-up / Sign-in screens.
// Shown when there's no active Supabase session (handled by AuthGate in index.html).

function AuthScreens() {
  const [view, setView] = React.useState('welcome'); // welcome | signin | signup | forgot

  return (
    <div style={{
      position: 'absolute', inset: 0,
      background: 'linear-gradient(160deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      color: 'var(--paper)',
      overflow: 'hidden',
      display: 'flex', flexDirection: 'column',
    }}>
      {/* Decorative grain + mascot */}
      <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
      <img src="assets/mascot-full-cream.svg" alt="" aria-hidden="true" style={{
        position: 'absolute', right: -30, bottom: -40, width: 220, opacity: 0.08,
        transform: 'rotate(-8deg)', pointerEvents: 'none',
      }}/>

      {view === 'welcome' && <WelcomeView onSignUp={() => setView('signup')} onSignIn={() => setView('signin')}/>}
      {view === 'signup'  && <SignUpView  onBack={() => setView('welcome')} onSignInInstead={() => setView('signin')}/>}
      {view === 'signin'  && <SignInView  onBack={() => setView('welcome')} onSignUpInstead={() => setView('signup')} onForgot={() => setView('forgot')}/>}
      {view === 'forgot'  && <ForgotPasswordView onBack={() => setView('signin')}/>}
    </div>
  );
}

// ─── Welcome ─────────────────────────────────────────────────
function WelcomeView({ onSignUp, onSignIn }) {
  // Referral link attribution (?via=@handle, stashed pre-auth) — show the
  // friend's $10 up front; it's claimed automatically after signup.
  const refVia = typeof getRefVia === 'function' ? getRefVia() : null;
  return (
    <div style={{ position: 'relative', flex: 1, display: 'flex', flexDirection: 'column', padding: '0 28px', color: 'var(--paper)' }}>
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center' }}>
        <Wordmark variant="white" size={190}/>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 38, lineHeight: 1, letterSpacing: '-0.02em', marginTop: 26, whiteSpace: 'nowrap', color: 'var(--paper)' }}>
          Pitch &amp; Putt, Rated.
        </div>
        <div className="caption-serif" style={{ fontSize: 18, marginTop: 16, opacity: 0.85, maxWidth: 340, color: 'var(--paper)', lineHeight: 1.4 }}>
          Miami's pitch &amp; putt network.<br/>
          Real matches, real ratings, real bragging rights.
        </div>

        {refVia && (
          <div style={{
            marginTop: 20, padding: '10px 18px', borderRadius: 999,
            background: 'rgba(255,255,255,0.14)', backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)',
            border: '1px solid rgba(255,255,255,0.28)',
            fontSize: 13, fontWeight: 700, color: 'var(--paper)',
          }}>
            {/^sbx-?/i.test(refVia)
              ? <>🎟 You've got <span style={{ fontWeight: 800 }}>$10 off your first round</span></>
              : <>🎟 @{refVia} sent you <span style={{ fontWeight: 800 }}>$10 off your first round</span></>}
          </div>
        )}

        <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 36, width: '100%', maxWidth: 360 }}>
          <Button variant="paper" size="lg" full onClick={onSignUp}>
            Create account
            <Icon.ArrowRight size={16}/>
          </Button>
          <Button variant="outlineWhite" size="lg" full onClick={onSignIn}>
            Sign In
          </Button>
        </div>
      </div>

      <div style={{ fontSize: 10, opacity: 0.6, textAlign: 'center', paddingBottom: 20, fontFamily: 'var(--font-mono)', letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--paper)' }}>
        v1 prototype · real data · real matches
      </div>
    </div>
  );
}

// ─── Sign Up (one question at a time, slides right→left) ─────
function SignUpView({ onBack, onSignInInstead }) {
  const [firstName, setFirstName] = React.useState('');
  const [lastName, setLastName]   = React.useState('');
  const [gender, setGender]       = React.useState('');
  const [dob, setDob]             = React.useState('');
  const [email, setEmail]         = React.useState('');
  const [password, setPassword]   = React.useState('');
  const [emailStatus, setEmailStatus] = React.useState('idle'); // idle|checking|free|taken
  const [handle, setHandle]       = React.useState('');
  const [handleStatus, setHandleStatus] = React.useState('idle'); // idle|checking|available|taken|invalid
  // Referral code (optional last step) — pre-filled when they arrived
  // through a share link; a typed code wins over the link at claim time.
  const [refCode, setRefCode]     = React.useState(() => (typeof getRefVia === 'function' && getRefVia()) || '');
  const [refStatus, setRefStatus] = React.useState('idle'); // idle|checking|valid|invalid
  const [createdUserId, setCreatedUserId] = React.useState(null); // set once the auth user exists
  const [step, setStep]           = React.useState(0);
  const [busy, setBusy]           = React.useState(false);
  const [err, setErr]             = React.useState('');

  const emailOk = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email.trim());
  const cleanHandle = sanitizeHandle(handle);

  // Live "email already in use" check (debounced). Falls back to "free" if the
  // email_exists function isn't deployed, so the flow still works (the final
  // sign-up will catch a dupe).
  React.useEffect(() => {
    if (!emailOk) { setEmailStatus('idle'); return undefined; }
    setEmailStatus('checking');
    const t = setTimeout(async () => {
      try {
        const { data, error } = await sbx.rpc('email_exists', { p_email: email.trim() });
        setEmailStatus(error ? 'free' : (data ? 'taken' : 'free'));
      } catch (_) { setEmailStatus('free'); }
    }, 450);
    return () => clearTimeout(t);
  }, [email, emailOk]);

  // Live @handle availability check (debounced) for the final step. We're not
  // signed in yet, so profiles SELECT is blocked by RLS — use the email_for_handle
  // RPC (SECURITY DEFINER, callable while logged out) instead: a returned email
  // means the handle is taken.
  React.useEffect(() => {
    if (!handle) { setHandleStatus('idle'); return undefined; }
    if (!isValidHandle(cleanHandle)) { setHandleStatus('invalid'); return undefined; }
    setHandleStatus('checking');
    const t = setTimeout(async () => {
      try {
        const { data, error } = await sbx.rpc('email_for_handle', { handle_input: cleanHandle });
        setHandleStatus(error ? 'available' : (data ? 'taken' : 'available'));
      } catch (_) { setHandleStatus('available'); }
    }, 400);
    return () => clearTimeout(t);
  }, [handle, cleanHandle]);

  // Live referral-code validity check (debounced, anonymous — the RPC only
  // ever answers true/false). An unreachable RPC counts as valid so the
  // optional field never blocks sign-up.
  React.useEffect(() => {
    if (!refCode.trim()) { setRefStatus('idle'); return undefined; }
    setRefStatus('checking');
    const t = setTimeout(async () => {
      const ok = await lookupReferralCode(refCode);
      setRefStatus(ok === false ? 'invalid' : 'valid');
    }, 400);
    return () => clearTimeout(t);
  }, [refCode]);

  // One step per question. Optional steps are always "valid" (can advance blank).
  const steps = [
    { key: 'firstName', eyebrow: 'Your name', title: "What's your first name?", valid: !!firstName.trim(),
      field: <Input value={firstName} onChange={setFirstName} autoComplete="given-name" placeholder="First name" enterKeyHint="next"/> },
    { key: 'lastName', eyebrow: 'Your name', title: 'And your last name?', valid: !!lastName.trim(),
      field: <Input value={lastName} onChange={setLastName} autoComplete="family-name" placeholder="Last name" enterKeyHint="next"/> },
    { key: 'email', eyebrow: 'Sign-in details', title: "What's your email?", valid: emailOk && emailStatus !== 'taken' && emailStatus !== 'checking',
      field: (
        <div>
          <Input value={email} onChange={setEmail} type="email" autoComplete="email" autoCapitalize="off" placeholder="you@example.com" enterKeyHint="next"/>
          <div style={{ marginTop: 8, fontSize: 12, fontFamily: 'var(--font-mono)', minHeight: 18, textAlign: 'left',
            color: emailStatus === 'taken' ? 'var(--loss-soft)' : 'rgba(234,226,206,0.6)' }}>
            {emailStatus === 'taken' ? 'Email already in use, use another or sign in.'
              : emailStatus === 'checking' ? 'Checking…'
              : (email && !emailOk ? 'That doesn’t look like an email yet.' : '')}
          </div>
          {emailStatus === 'taken' && (
            <button type="button" onClick={onSignInInstead} style={{
              marginTop: 14, width: '100%', padding: '14px', borderRadius: 12, border: 'none', cursor: 'pointer',
              background: 'var(--paper)', color: 'var(--forest)', fontWeight: 800, fontSize: 15,
              display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
            }}>Sign in <Icon.ArrowRight size={16}/></button>
          )}
        </div>
      ) },
    { key: 'password', eyebrow: 'Sign-in details', title: 'Create a password', hint: 'At least 6 characters', valid: password.length >= 6,
      field: <Input value={password} onChange={setPassword} type="password" autoComplete="new-password" placeholder="••••••••" enterKeyHint="next"/> },
    { key: 'gender', eyebrow: 'About you (optional)', title: 'What is your gender?', valid: true,
      field: <ChoiceButtons value={gender} onChange={setGender} options={[
        ['male', 'Male'], ['female', 'Female'], ['skip', 'Prefer not to say'],
      ]}/> },
    { key: 'dob', eyebrow: 'About you (optional)', title: "When's your birthday?", valid: true,
      field: <WheelPicker value={dob} onChange={setDob}/> },
    { key: 'handle', eyebrow: 'Almost there', title: 'Pick a display name', valid: handleStatus === 'available',
      field: (
        <div>
          <div style={{
            display: 'flex', alignItems: 'center', textAlign: 'left',
            background: 'rgba(255,255,255,0.08)', border: `1px solid ${borderFor(handleStatus)}`,
            borderRadius: 14, padding: '0 14px', transition: 'border-color 0.15s',
          }}>
            <span style={{ fontFamily: 'var(--font-display)', fontSize: 20, color: 'rgba(234,226,206,0.5)' }}>@</span>
            <input value={handle} onChange={e => setHandle(e.target.value)} placeholder="yourname"
              autoCapitalize="off" autoCorrect="off" autoComplete="off" spellCheck={false} maxLength={20}
              style={{ flex: 1, padding: '15px 8px', background: 'transparent', border: 'none', color: 'var(--paper)', fontSize: 20, fontFamily: 'var(--font-display)', outline: 'none' }}/>
            <StatusIcon status={handleStatus}/>
          </div>
          <div style={{ marginTop: 8, fontSize: 12, fontFamily: 'var(--font-mono)', color: statusTextColor(handleStatus), minHeight: 18 }}>
            {statusText(handleStatus, cleanHandle)}
          </div>
        </div>
      ) },
    { key: 'refCode', eyebrow: 'Last step — optional', title: 'Got a referral code?',
      hint: 'Leave it blank to skip.',
      valid: !refCode.trim() || refStatus === 'valid',
      field: (
        <div>
          <input value={refCode} onChange={e => setRefCode(e.target.value)} placeholder="SBX-XXXX"
            autoCapitalize="characters" autoCorrect="off" autoComplete="off" spellCheck={false} maxLength={12}
            style={{
              width: '100%', boxSizing: 'border-box', textAlign: 'center',
              background: 'rgba(255,255,255,0.08)',
              border: `1px solid ${refStatus === 'invalid' ? 'var(--loss-soft)' : refStatus === 'valid' ? 'rgba(234,226,206,0.8)' : 'rgba(234,226,206,0.25)'}`,
              borderRadius: 14, padding: '15px 14px', color: 'var(--paper)',
              fontSize: 20, fontFamily: 'var(--font-mono)', letterSpacing: '0.14em', outline: 'none',
              textTransform: 'uppercase', transition: 'border-color 0.15s',
            }}/>
          <div style={{ marginTop: 8, fontSize: 12, fontFamily: 'var(--font-mono)', minHeight: 18,
            color: refStatus === 'invalid' ? 'var(--loss-soft)' : 'rgba(234,226,206,0.7)' }}>
            {refStatus === 'valid' ? '✓ $10 off your first round is loaded.'
              : refStatus === 'invalid' ? 'Code not found — check it or leave blank.'
              : refStatus === 'checking' ? 'Checking…'
              : ''}
          </div>
        </div>
      ) },
  ];
  const last = steps.length - 1;
  const cur = steps[step];

  // Keyboard-aware: when the on-screen keyboard opens the visual viewport shrinks.
  // We lift the centered content by half that amount so it re-centers in the
  // space that's left; when the keyboard hides, it eases back to centre.
  const inset = useKeyboardInset();

  // Drop focus when moving between steps so the keyboard tucks away and you see
  // the screen first — you tap the field to bring it back.
  function blur() { if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); }
  function next() {
    if (!cur.valid || busy) return;
    blur();
    if (step < last) { setErr(''); setStep(s => s + 1); }
    else submit();
  }
  function back() {
    blur();
    if (step === 0) onBack();
    else { setErr(''); setStep(s => s - 1); }
  }

  async function submit() {
    setBusy(true); setErr('');
    // Stash the typed referral code BEFORE the account exists — the App
    // claims it automatically once the profile lands (typed wins over any
    // share-link attribution).
    if (refCode.trim() && refStatus === 'valid') setTypedReferralCode(refCode);
    // Create the auth user once. If a later retry happens (e.g. handle taken),
    // reuse the existing user instead of signing up again with the same email.
    let userId = createdUserId;
    if (!userId) {
      const { data: signUpData, error: signUpErr } = await sbx.auth.signUp({ email: email.trim(), password });
      if (signUpErr) { setErr(signUpErr.message); setBusy(false); return; }
      const user = signUpData.user;
      if (!user) { setErr('Sign-up did not return a user. Check your email confirmation settings.'); setBusy(false); return; }
      if (!signUpData.session) { setErr('Account created. Check your email for a confirmation link, then sign in.'); setBusy(false); return; }
      userId = user.id;
      setCreatedUserId(userId);
    }

    // Branded bouncing-logo loader while we finish + land in the app.
    if (window.spp_beginLanding) window.spp_beginLanding();
    const landedAt = Date.now();

    // New users start unrated — SBX 0 (no history), not the table's 4.0 default.
    const { error: profileErr } = await sbx.from('profiles').upsert({
      id: userId, first_name: firstName.trim(), last_name: lastName.trim(),
      gender: gender || null, dob: dob || null, handle: '@' + cleanHandle, sbx: 0,
    }, { onConflict: 'id' });
    if (profileErr) {
      if (window.spp_endLanding) window.spp_endLanding();
      if (/duplicate|unique/i.test(profileErr.message)) {
        // Handle was taken in the moment between the check and the insert.
        setHandleStatus('taken');
        setStep(steps.findIndex(s => s.key === 'handle'));
        setErr('That display name was just taken — pick another.');
      } else {
        setErr('Account created but profile save failed: ' + profileErr.message);
      }
      setBusy(false);
      return;
    }
    // Hold the bounce for at least 2s, then load the app (gate routes to home).
    const elapsed = Date.now() - landedAt;
    if (elapsed < 2000) await new Promise(r => setTimeout(r, 2000 - elapsed));
    if (window.reloadProfile) await window.reloadProfile();
    if (window.spp_endLanding) window.spp_endLanding();
    setBusy(false);
  }

  return (
    <div style={{ position: 'relative', flex: 1, display: 'flex', flexDirection: 'column', padding: '0 24px' }}>
      <BackButton onClick={back}/>

      {/* Centered content — lifts up by half the keyboard height when focused */}
      <div style={{
        flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
        transform: `translateY(-${inset / 2}px)`, transition: 'transform 0.28s ease',
      }}>
        <div style={{ width: '100%', maxWidth: 360, textAlign: 'center' }}>
          {/* Progress */}
          <div style={{ display: 'flex', gap: 6, justifyContent: 'center', marginBottom: 24 }}>
            {steps.map((s, i) => (
              <div key={s.key} style={{
                width: i === step ? 22 : 7, height: 7, borderRadius: 99,
                background: i <= step ? 'var(--paper)' : 'rgba(234,226,206,0.28)',
                transition: 'width 0.35s ease, background 0.35s ease',
              }}/>
            ))}
          </div>

          {/* Sliding questions */}
          <div style={{ overflow: 'hidden' }}>
            <div style={{
              display: 'flex', width: `${steps.length * 100}%`,
              transform: `translateX(-${step * (100 / steps.length)}%)`,
              transition: 'transform 0.45s cubic-bezier(0.4, 0, 0.2, 1)',
            }}>
              {steps.map((s, i) => (
                <div key={s.key} aria-hidden={i !== step} style={{ width: `${100 / steps.length}%`, flexShrink: 0, padding: '0 4px', opacity: i === step ? 1 : 0, transition: 'opacity 0.3s ease', pointerEvents: i === step ? 'auto' : 'none' }}>
                  <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', letterSpacing: '0.14em', textTransform: 'uppercase', opacity: 0.6, marginBottom: 12 }}>{s.eyebrow}</div>
                  <div style={{ fontFamily: 'var(--font-display)', fontSize: 28, lineHeight: 1.08, letterSpacing: '-0.02em' }}>{s.title}</div>
                  <div style={{ marginTop: 22 }}>
                    {s.field}
                    {s.hint && <div style={{ fontSize: 12, opacity: 0.6, marginTop: 10 }}>{s.hint}</div>}
                  </div>
                  {/* Next / submit, right under the field (hidden when email is taken — the Sign in button takes over) */}
                  {!(s.key === 'email' && emailStatus === 'taken') && (
                    <Button variant="paper" size="lg" full disabled={!s.valid || busy} onClick={next} style={{ marginTop: 22 }}>
                      {i < last ? 'Next' : (busy ? 'Creating…' : 'Create account')}
                      {!busy && <Icon.ArrowRight size={16}/>}
                    </Button>
                  )}
                </div>
              ))}
            </div>
          </div>

          {err && <div style={{ marginTop: 16, fontSize: 13, color: 'var(--loss-soft)', background: 'rgba(155,58,46,0.2)', padding: '10px 12px', borderRadius: 12 }}>{err}</div>}
        </div>
      </div>
    </div>
  );
}

// ─── Keyboard inset (visual viewport) ────────────────────────
function useKeyboardInset() {
  const [inset, setInset] = React.useState(0);
  React.useEffect(() => {
    const vv = window.visualViewport;
    if (!vv) return undefined;
    const onResize = () => setInset(Math.max(0, window.innerHeight - vv.height - vv.offsetTop));
    vv.addEventListener('resize', onResize);
    vv.addEventListener('scroll', onResize);
    onResize();
    return () => { vv.removeEventListener('resize', onResize); vv.removeEventListener('scroll', onResize); };
  }, []);
  return inset;
}

// ─── Choice buttons (gender) ─────────────────────────────────
function ChoiceButtons({ value, onChange, options }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      {options.map(([v, l]) => {
        const on = value === v;
        return (
          <button key={v} type="button" onClick={() => onChange(v)} style={{
            padding: '15px 16px', borderRadius: 14, fontSize: 16, fontWeight: 600, fontFamily: 'var(--font-body)',
            cursor: 'pointer', transition: 'background 0.15s, border-color 0.15s',
            background: on ? 'var(--paper)' : 'rgba(255,255,255,0.08)',
            border: `1px solid ${on ? 'var(--paper)' : 'rgba(234,226,206,0.2)'}`,
            color: on ? 'var(--forest)' : 'var(--paper)',
          }}>{l}</button>
        );
      })}
    </div>
  );
}

// ─── Birthday wheel picker ───────────────────────────────────
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
function WheelPicker({ value, onChange }) {
  const thisYear = new Date().getFullYear();
  const years = React.useMemo(() => { const a = []; for (let y = thisYear - 13; y >= 1925; y--) a.push(y); return a; }, [thisYear]);
  const days = React.useMemo(() => Array.from({ length: 31 }, (_, i) => i + 1), []);

  const parsed = (value && /^\d{4}-\d{2}-\d{2}$/.test(value)) ? value.split('-').map(Number) : null;
  const [y, setY] = React.useState(parsed ? parsed[0] : thisYear - 25);
  const [m, setM] = React.useState(parsed ? parsed[1] : 1); // 1-12
  const [d, setD] = React.useState(parsed ? parsed[2] : 1); // 1-31

  React.useEffect(() => {
    onChange(`${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [y, m, d]);

  return (
    <div style={{ position: 'relative', display: 'flex', justifyContent: 'center', gap: 6 }}>
      {/* Selected band */}
      <div style={{ position: 'absolute', top: 40, left: 0, right: 0, height: 40, borderTop: '1px solid rgba(234,226,206,0.28)', borderBottom: '1px solid rgba(234,226,206,0.28)', pointerEvents: 'none' }}/>
      <WheelColumn width={64} items={days} value={d} onChange={setD} fmt={String}/>
      <WheelColumn width={132} items={MONTHS.map((_, i) => i + 1)} value={m} onChange={setM} fmt={(mm) => MONTHS[mm - 1]}/>
      <WheelColumn width={84} items={years} value={y} onChange={setY} fmt={String}/>
    </div>
  );
}

const WHEEL_ITEM = 40;
function WheelColumn({ items, value, onChange, fmt, width }) {
  const ref = React.useRef(null);
  const idx = Math.max(0, items.indexOf(value));

  // Position to the current value on mount (no smooth — instant).
  React.useEffect(() => {
    if (ref.current) ref.current.scrollTop = idx * WHEEL_ITEM;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  function onScroll() {
    const i = Math.max(0, Math.min(items.length - 1, Math.round(ref.current.scrollTop / WHEEL_ITEM)));
    if (items[i] !== value) onChange(items[i]);
  }

  return (
    <div ref={ref} onScroll={onScroll} className="scroll-hide" style={{
      height: WHEEL_ITEM * 3, width, overflowY: 'scroll', scrollSnapType: 'y mandatory',
      WebkitMaskImage: 'linear-gradient(to bottom, transparent, #000 32%, #000 68%, transparent)',
      maskImage: 'linear-gradient(to bottom, transparent, #000 32%, #000 68%, transparent)',
    }}>
      <div style={{ height: WHEEL_ITEM }}/>
      {items.map((it) => {
        const on = it === value;
        return (
          <div key={it} style={{
            height: WHEEL_ITEM, display: 'flex', alignItems: 'center', justifyContent: 'center',
            scrollSnapAlign: 'center', fontFamily: 'var(--font-body)', fontSize: 18,
            color: on ? 'var(--paper)' : 'rgba(234,226,206,0.45)', fontWeight: on ? 700 : 500,
            transition: 'color 0.15s',
          }}>{fmt(it)}</div>
        );
      })}
      <div style={{ height: WHEEL_ITEM }}/>
    </div>
  );
}

// ─── Sign In ─────────────────────────────────────────────────
function SignInView({ onBack, onSignUpInstead, onForgot }) {
  const [identity, setIdentity] = React.useState(''); // email or @handle
  const [password, setPassword] = React.useState('');
  const [remember, setRemember] = React.useState(true);
  const [busy, setBusy]         = React.useState(false);
  const [err, setErr]           = React.useState('');

  async function submit(e) {
    e.preventDefault();
    if (!identity || !password || busy) return;
    setBusy(true); setErr('');

    // Resolve to an email. Strict email-shape regex so "@handle" or
    // a bare "handle" both go through the username path. If it doesn't
    // match the email regex, look up the email via the email_for_handle
    // RPC (SECURITY DEFINER on the server, normalizes @-prefix).
    let email = identity.trim();
    const isEmail = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
    if (!isEmail) {
      const handle = email.replace(/^@/, '').toLowerCase();
      const { data: foundEmail, error: lookupErr } = await sbx.rpc('email_for_handle', { handle_input: handle });
      if (lookupErr || !foundEmail) {
        setErr('No account found for that username. Try signing in with your email instead.');
        setBusy(false);
        return;
      }
      email = foundEmail;
    }

    const { error } = await sbx.auth.signInWithPassword({ email, password });
    if (error) { setErr(error.message); setBusy(false); return; }

    // "Remember me" controls session persistence past tab close.
    // Supabase persists in localStorage by default (≈ "remember on").
    // If the user unchecked it, sign them out when the tab/window closes.
    if (!remember) {
      sessionStorage.setItem('spp_session_only', '1');
      window.addEventListener('beforeunload', () => { sbx.auth.signOut(); });
    } else {
      sessionStorage.removeItem('spp_session_only');
    }

    // Branded bouncing-logo loader for at least 2s, then land in the app.
    if (window.spp_beginLanding) window.spp_beginLanding();
    await new Promise(r => setTimeout(r, 2000));
    if (window.spp_endLanding) window.spp_endLanding();
    setBusy(false);
    // AuthGate picks up the new session automatically.
  }

  const inset = useKeyboardInset();
  return (
    <form onSubmit={submit} style={{ position: 'relative', flex: 1, display: 'flex', flexDirection: 'column', padding: '0 24px' }}>
      <BackButton onClick={onBack}/>

      <div style={{
        flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
        transform: `translateY(-${inset / 2}px)`, transition: 'transform 0.28s ease',
      }}>
        <div style={{ width: '100%', maxWidth: 360, textAlign: 'center' }}>
          <Eyebrow color="var(--paper)">Welcome back</Eyebrow>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 34, lineHeight: 0.95, marginTop: 10, letterSpacing: '-0.02em' }}>
            Sign in.
          </div>

          <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 28, textAlign: 'left' }}>
            <Field label="Email or username">
              <Input value={identity} onChange={setIdentity} type="text" autoComplete="username" autoCapitalize="off" placeholder="you@example.com or @handle"/>
            </Field>
            <Field label="Password">
              <Input value={password} onChange={setPassword} type="password" autoComplete="current-password"/>
            </Field>
          </div>

          {/* Remember me */}
          <button type="button" onClick={() => setRemember(r => !r)} style={{
            marginTop: 16, display: 'flex', alignItems: 'center', gap: 10, justifyContent: 'center',
            color: 'var(--paper)', padding: 0, width: '100%',
          }}>
            <span style={{
              width: 20, height: 20, borderRadius: 6,
              background: remember ? 'var(--paper)' : 'transparent',
              border: '1.5px solid var(--paper)',
              display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
            }}>
              {remember && (
                <svg width="12" height="12" viewBox="0 0 14 14" fill="none">
                  <path d="M2 7.5l3.2 3L12 3.5" stroke="var(--forest)" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/>
                </svg>
              )}
            </span>
            <span style={{ fontSize: 13, fontFamily: 'var(--font-body)', opacity: 0.9 }}>Remember me</span>
          </button>

          {err && <div style={{ marginTop: 14, fontSize: 13, color: 'var(--loss-soft)', background: 'rgba(155,58,46,0.2)', padding: '10px 12px', borderRadius: 12 }}>{err}</div>}

          <Button variant="paper" size="lg" full disabled={!identity || !password || busy} onClick={submit} style={{ marginTop: 22 }}>
            {busy ? 'Signing in…' : 'Sign in'}
            {!busy && <Icon.ArrowRight size={16}/>}
          </Button>

          <button type="button" onClick={onForgot} style={{
            marginTop: 16, fontSize: 13, fontFamily: 'var(--font-mono)', width: '100%',
            color: 'var(--paper)', opacity: 0.7, textAlign: 'center', letterSpacing: '0.06em',
          }}>
            Forgot your password?
          </button>
          <button type="button" onClick={onSignUpInstead} style={{
            marginTop: 8, fontSize: 13, fontFamily: 'var(--font-mono)', width: '100%',
            color: 'var(--paper)', opacity: 0.7, textAlign: 'center', letterSpacing: '0.06em',
          }}>
            New here? <u>Create an account</u>
          </button>
        </div>
      </div>
    </form>
  );
}

// ─── Forgot password (request reset email) ──────────────────
function ForgotPasswordView({ onBack }) {
  const [email, setEmail] = React.useState('');
  const [busy, setBusy]   = React.useState(false);
  const [err, setErr]     = React.useState('');
  const [sent, setSent]   = React.useState(false);

  async function submit(e) {
    e.preventDefault();
    if (!email.trim() || busy) return;
    setBusy(true); setErr('');
    const { error } = await sbx.auth.resetPasswordForEmail(email.trim(), {
      redirectTo: window.redirectOrigin ? window.redirectOrigin() : (window.location.origin + window.location.pathname),
    });
    if (error) { setErr(error.message); setBusy(false); return; }
    setSent(true);
    setBusy(false);
  }

  return (
    <form onSubmit={submit} style={{ position: 'relative', flex: 1, display: 'flex', flexDirection: 'column', padding: '60px 24px 24px' }}>
      <BackButton onClick={onBack}/>

      <div style={{ marginTop: 12 }}>
        <Eyebrow color="var(--paper)">Password reset</Eyebrow>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 34, lineHeight: 0.95, marginTop: 10, letterSpacing: '-0.02em' }}>
          {sent ? 'Check your email.' : 'Reset it.'}
        </div>
        <div className="caption-serif" style={{ fontSize: 15, marginTop: 10, opacity: 0.8, maxWidth: 340 }}>
          {sent
            ? `We sent a reset link to ${email}. Tap it on your phone — it'll drop you back here to set a new password.`
            : 'Enter the email you signed up with. We\'ll send a link to set a new password.'}
        </div>
      </div>

      {!sent && (
        <>
          <div style={{ marginTop: 28 }}>
            <Field label="Email">
              <Input value={email} onChange={setEmail} type="email" autoComplete="email" autoCapitalize="off"/>
            </Field>
          </div>
          {err && <div style={{ marginTop: 14, fontSize: 13, color: 'var(--loss-soft)', background: 'rgba(155,58,46,0.2)', padding: '10px 12px', borderRadius: 12 }}>{err}</div>}
          <div style={{ flex: 1 }}/>
          <Button variant="paper" size="lg" full disabled={!email.trim() || busy} onClick={submit}>
            {busy ? 'Sending…' : 'Send reset link'}
            {!busy && <Icon.ArrowRight size={16}/>}
          </Button>
        </>
      )}

      {sent && (
        <>
          <div style={{ flex: 1 }}/>
          <Button variant="outlineWhite" size="lg" full onClick={onBack}>
            Back to sign in
          </Button>
        </>
      )}
    </form>
  );
}

// ─── Reset password (set new password after clicking email link) ────
// Rendered when the app is opened via a password-recovery link: Supabase
// fires a PASSWORD_RECOVERY auth event, which Root detects via useRecovering.
function ResetPasswordScreen({ onDone }) {
  const [password, setPassword] = React.useState('');
  const [confirm, setConfirm]   = React.useState('');
  const [busy, setBusy]         = React.useState(false);
  const [err, setErr]           = React.useState('');

  const valid = password.length >= 6 && password === confirm;

  async function submit(e) {
    e.preventDefault();
    if (!valid || busy) return;
    setBusy(true); setErr('');
    const { error } = await sbx.auth.updateUser({ password });
    if (error) { setErr(error.message); setBusy(false); return; }
    setBusy(false);
    onDone && onDone();
  }

  return (
    <div style={{
      position: 'absolute', inset: 0,
      background: 'linear-gradient(160deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      color: 'var(--paper)', display: 'flex', flexDirection: 'column',
    }}>
      <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
      <form onSubmit={submit} style={{ position: 'relative', flex: 1, display: 'flex', flexDirection: 'column', padding: '80px 24px 24px' }}>
        <Eyebrow color="var(--paper)">New password</Eyebrow>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 34, lineHeight: 0.95, marginTop: 10, letterSpacing: '-0.02em' }}>
          Set a new<br/>password.
        </div>
        <div className="caption-serif" style={{ fontSize: 15, marginTop: 10, opacity: 0.75 }}>
          You're signed in from the reset link. Pick a new password to keep using the app.
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 28 }}>
          <Field label="New password" hint="At least 6 characters">
            <Input value={password} onChange={setPassword} type="password" autoComplete="new-password"/>
          </Field>
          <Field label="Confirm password">
            <Input value={confirm} onChange={setConfirm} type="password" autoComplete="new-password"/>
          </Field>
        </div>

        {password && confirm && password !== confirm && (
          <div style={{ marginTop: 10, fontSize: 12, color: 'var(--loss-soft)' }}>Passwords don't match.</div>
        )}
        {err && <div style={{ marginTop: 14, fontSize: 13, color: 'var(--loss-soft)', background: 'rgba(155,58,46,0.2)', padding: '10px 12px', borderRadius: 12 }}>{err}</div>}

        <div style={{ flex: 1 }}/>

        <Button variant="paper" size="lg" full disabled={!valid || busy} onClick={submit}>
          {busy ? 'Saving…' : 'Save & continue'}
          {!busy && <Icon.ArrowRight size={16}/>}
        </Button>
      </form>
    </div>
  );
}

// ─── Profile completion (for users signed up elsewhere w/o profile row) ───
function ProfileSetupScreen({ session, onDone }) {
  const [firstName, setFirstName] = React.useState('');
  const [lastName, setLastName]   = React.useState('');
  const [gender, setGender]       = React.useState('');
  const [dob, setDob]             = React.useState('');
  const [busy, setBusy]           = React.useState(false);
  const [err, setErr]             = React.useState('');

  const valid = firstName.trim() && lastName.trim();

  async function submit(e) {
    e.preventDefault();
    if (!valid || busy) return;
    setBusy(true); setErr('');
    // No handle here — DisplayNameScreen picks it up next.
    const { error } = await sbx.from('profiles').insert({
      id: session.user.id,
      first_name: firstName.trim(),
      last_name:  lastName.trim(),
      gender: gender || null,
      dob:    dob    || null,
      sbx: 0,
    });
    if (error) { setErr(error.message); setBusy(false); return; }
    setBusy(false);
    onDone && onDone();
  }

  return (
    <div style={{
      position: 'absolute', inset: 0,
      background: 'linear-gradient(160deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      color: 'var(--paper)', display: 'flex', flexDirection: 'column',
    }}>
      <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
      <form onSubmit={submit} style={{ position: 'relative', flex: 1, display: 'flex', flexDirection: 'column', padding: '80px 24px 24px' }}>
        <Eyebrow color="var(--paper)">One more step</Eyebrow>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 34, lineHeight: 0.95, marginTop: 10, letterSpacing: '-0.02em' }}>
          Finish your<br/>profile.
        </div>
        <div className="caption-serif" style={{ fontSize: 15, marginTop: 10, opacity: 0.75 }}>
          This is how other players will find you on the board.
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 24 }}>
          <Row>
            <Field label="First name" required>
              <Input value={firstName} onChange={setFirstName} autoComplete="given-name"/>
            </Field>
            <Field label="Last name" required>
              <Input value={lastName} onChange={setLastName} autoComplete="family-name"/>
            </Field>
          </Row>
          <Row>
            <Field label="Gender">
              <Select value={gender} onChange={setGender} options={[
                ['',   '—'],
                ['male',   'Male'],
                ['female', 'Female'],
                ['other',  'Other'],
                ['skip',   'Prefer not to say'],
              ]}/>
            </Field>
            <Field label="Date of birth">
              <Input value={dob} onChange={setDob} type="date"/>
            </Field>
          </Row>
        </div>

        {err && <div style={{ marginTop: 14, fontSize: 13, color: 'var(--loss-soft)', background: 'rgba(155,58,46,0.2)', padding: '10px 12px', borderRadius: 12 }}>{err}</div>}

        <div style={{ flex: 1 }}/>

        <Button variant="paper" size="lg" full disabled={!valid || busy} onClick={submit}>
          {busy ? 'Saving…' : 'Continue'}
          {!busy && <Icon.ArrowRight size={16}/>}
        </Button>

        <button type="button" onClick={signOut} style={{
          marginTop: 14, fontSize: 13, fontFamily: 'var(--font-mono)', width: '100%',
          color: 'var(--paper)', opacity: 0.7, textAlign: 'center', letterSpacing: '0.06em',
        }}>
          Not you? <u>Sign out</u>
        </button>
      </form>
    </div>
  );
}

// ─── Small form primitives ───────────────────────────────────
function Row({ children }) {
  return <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>{children}</div>;
}

function Field({ label, required, hint, children }) {
  return (
    <label style={{ display: 'block' }}>
      <div style={{
        fontSize: 10, fontFamily: 'var(--font-mono)',
        letterSpacing: '0.14em', textTransform: 'uppercase',
        fontWeight: 700, opacity: 0.65, marginBottom: 6,
      }}>
        {label} {required && <span style={{ color: 'var(--paper)' }}>*</span>}
      </div>
      {children}
      {hint && <div style={{ fontSize: 11, opacity: 0.55, marginTop: 4 }}>{hint}</div>}
    </label>
  );
}

function Input({ value, onChange, type = 'text', ...rest }) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
      type={type}
      {...rest}
      style={{
        width: '100%', padding: '13px 14px',
        borderRadius: 12,
        background: 'rgba(255,255,255,0.08)',
        border: '1px solid rgba(234,226,206,0.2)',
        color: 'var(--paper)',
        fontSize: 15, fontFamily: 'var(--font-body)',
        outline: 'none',
      }}
    />
  );
}

function Select({ value, onChange, options }) {
  return (
    <select
      value={value}
      onChange={(e) => onChange(e.target.value)}
      style={{
        width: '100%', padding: '13px 14px',
        borderRadius: 12,
        background: 'rgba(255,255,255,0.08)',
        border: '1px solid rgba(234,226,206,0.2)',
        color: 'var(--paper)',
        fontSize: 15, fontFamily: 'var(--font-body)',
        outline: 'none',
        appearance: 'none',
      }}
    >
      {options.map(([v, l]) => <option key={v} value={v} style={{ color: 'var(--ink)' }}>{l}</option>)}
    </select>
  );
}

function BackButton({ onClick }) {
  return (
    <button type="button" onClick={onClick} style={{
      position: 'absolute', top: 20, left: 20, zIndex: 20,
      width: 38, height: 38, borderRadius: 999,
      background: 'rgba(255,255,255,0.08)',
      border: '1px solid rgba(234,226,206,0.2)',
      color: 'var(--paper)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <Icon.ArrowLeft size={16} color="currentColor"/>
    </button>
  );
}

// ─── Display name (handle) step ──────────────────────────────
// Shown right after sign-up for new accounts, or from MatchHub when an
// existing user wants to change their @handle. Validates format + uniqueness
// as the user types, with a 400ms debounce.
//   onDone:   optional — if provided, called after save instead of relying on Root's gate
//   onCancel: optional — shows a back button and calls this when pressed
function DisplayNameScreen({ profile, onDone, onCancel }) {
  const currentClean = profile.handle ? profile.handle.replace(/^@/, '') : '';
  const [raw, setRaw] = React.useState(currentClean);
  const [status, setStatus] = React.useState(currentClean ? 'available' : 'idle'); // idle | checking | available | taken | invalid
  const [busy, setBusy] = React.useState(false);
  const [err, setErr]   = React.useState('');

  const clean = sanitizeHandle(raw);
  const validFormat = isValidHandle(clean);

  // Debounced uniqueness check. If the typed name matches the user's current
  // handle, treat as available (so re-opening the screen with the existing
  // handle doesn't show as "taken by yourself").
  React.useEffect(() => {
    if (!raw) { setStatus('idle'); return; }
    if (!validFormat) { setStatus('invalid'); return; }
    if (clean === currentClean) { setStatus('available'); return; }
    setStatus('checking');
    const t = setTimeout(async () => {
      const { data } = await sbx.from('profiles').select('id').eq('handle', '@' + clean).maybeSingle();
      if (!data || data.id === profile.id) setStatus('available');
      else setStatus('taken');
    }, 400);
    return () => clearTimeout(t);
  }, [raw, clean, validFormat, profile.id, currentClean]);

  const isChange = Boolean(profile.handle);
  const noChangeFromCurrent = clean === currentClean;

  async function submit(e) {
    e.preventDefault();
    if (status !== 'available' || busy) return;
    // No-op save when opening the change screen and pressing continue without edits.
    if (noChangeFromCurrent) { onDone ? onDone() : null; return; }
    setBusy(true); setErr('');
    const { error } = await sbx.from('profiles').update({ handle: '@' + clean }).eq('id', profile.id);
    if (error) {
      if (/duplicate|unique/i.test(error.message)) {
        setStatus('taken');
        setErr('That name was just taken. Try another.');
      } else {
        setErr(error.message);
      }
      setBusy(false);
      return;
    }
    if (window.reloadProfile) await window.reloadProfile();
    setBusy(false);
    if (onDone) onDone();
  }

  return (
    <div style={{
      position: 'absolute', inset: 0,
      background: 'linear-gradient(160deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      color: 'var(--paper)', display: 'flex', flexDirection: 'column',
    }}>
      <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
      <form onSubmit={submit} style={{ position: 'relative', flex: 1, display: 'flex', flexDirection: 'column', padding: '60px 24px 24px' }}>
        {onCancel && <BackButton onClick={onCancel}/>}
        <div style={{ marginTop: onCancel ? 20 : 20 }}>
          <Eyebrow color="var(--paper)">{isChange ? 'Display name' : 'Last step'}</Eyebrow>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 38, lineHeight: 0.95, marginTop: 10, letterSpacing: '-0.02em' }}>
            {isChange ? <>Change your<br/>display name.</> : <>Pick a display<br/>name.</>}
          </div>
          <div className="caption-serif" style={{ fontSize: 16, marginTop: 12, opacity: 0.75, maxWidth: 340 }}>
            This is how other players will find you on the board.
          </div>
        </div>

        <div style={{ marginTop: 32 }}>
          <div style={{
            display: 'flex', alignItems: 'center',
            background: 'rgba(255,255,255,0.08)',
            border: `1px solid ${borderFor(status)}`,
            borderRadius: 14,
            padding: '0 14px',
            transition: 'border-color 0.15s',
          }}>
            <span style={{
              fontFamily: 'var(--font-display)', fontSize: 22,
              color: 'rgba(234,226,206,0.5)', paddingRight: 2,
            }}>@</span>
            <input
              value={raw}
              onChange={e => setRaw(e.target.value)}
              placeholder="yourname"
              autoCapitalize="off"
              autoCorrect="off"
              autoComplete="off"
              spellCheck={false}
              maxLength={20}
              style={{
                flex: 1, padding: '16px 0',
                background: 'transparent', border: 'none',
                color: 'var(--paper)',
                fontSize: 22, fontFamily: 'var(--font-display)',
                letterSpacing: '-0.01em', outline: 'none',
              }}
            />
            <StatusIcon status={status}/>
          </div>

          <div style={{
            marginTop: 8, fontSize: 12, fontFamily: 'var(--font-mono)',
            letterSpacing: '0.04em', opacity: 0.85,
            color: statusTextColor(status),
            minHeight: 18,
          }}>
            {statusText(status, clean)}
          </div>
        </div>

        {err && <div style={{ marginTop: 14, fontSize: 13, color: 'var(--loss-soft)', background: 'rgba(155,58,46,0.2)', padding: '10px 12px', borderRadius: 12 }}>{err}</div>}

        <div style={{ flex: 1 }}/>

        <Button variant="paper" size="lg" full disabled={status !== 'available' || busy} onClick={submit}>
          {busy ? 'Saving…' : 'Continue'}
          {!busy && <Icon.ArrowRight size={16}/>}
        </Button>

        {!onCancel && (
          <button type="button" onClick={signOut} style={{
            marginTop: 14, fontSize: 13, fontFamily: 'var(--font-mono)', width: '100%',
            color: 'var(--paper)', opacity: 0.7, textAlign: 'center', letterSpacing: '0.06em',
          }}>
            Not you? <u>Sign out</u>
          </button>
        )}
      </form>
    </div>
  );
}

// ─── Handle validation + UI helpers ──────────────────────────
// Handle rules: 3–20 chars; lowercase letters, digits, underscore.
// Stripping happens live (uppercase is lowercased, other chars dropped).
function sanitizeHandle(raw) {
  return (raw || '').toLowerCase().replace(/[^a-z0-9_]/g, '').slice(0, 20);
}
function isValidHandle(clean) {
  return /^[a-z0-9_]{3,20}$/.test(clean);
}
function borderFor(status) {
  if (status === 'available') return 'rgba(62,138,87,0.9)';
  if (status === 'taken' || status === 'invalid') return 'rgba(231,184,167,0.6)';
  return 'rgba(234,226,206,0.2)';
}
function statusTextColor(status) {
  if (status === 'available') return 'rgba(201,216,190,1)';
  if (status === 'taken' || status === 'invalid') return 'var(--loss-soft)';
  return 'rgba(234,226,206,0.6)';
}
function statusText(status, clean) {
  switch (status) {
    case 'idle':      return '3–20 characters. Letters, numbers, underscores.';
    case 'checking':  return 'Checking…';
    case 'available': return `✓ @${clean} is yours.`;
    case 'taken':     return `✗ @${clean} is taken.`;
    case 'invalid':   return clean.length < 3
      ? 'Too short — at least 3 characters.'
      : 'Only letters, numbers, and underscores.';
    default: return '';
  }
}
function StatusIcon({ status }) {
  const size = 22;
  if (status === 'available') {
    return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
        <circle cx="12" cy="12" r="10" fill="rgba(62,138,87,0.18)"/>
        <path d="M7 12.5l3.5 3.5L17 9" stroke="#3E8A57" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"/>
      </svg>
    );
  }
  if (status === 'taken' || status === 'invalid') {
    return (
      <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
        <circle cx="12" cy="12" r="10" fill="rgba(155,58,46,0.2)"/>
        <path d="M8 8l8 8M16 8l-8 8" stroke="var(--loss-soft)" strokeWidth="2.4" strokeLinecap="round"/>
      </svg>
    );
  }
  if (status === 'checking') {
    return (
      <div style={{
        width: size, height: size, borderRadius: 999,
        border: '2px solid rgba(234,226,206,0.2)',
        borderTopColor: 'rgba(234,226,206,0.8)',
        animation: 'spin 0.8s linear infinite',
      }}/>
    );
  }
  return <div style={{ width: size, height: size }}/>;
}

Object.assign(window, { AuthScreens, ProfileSetupScreen, DisplayNameScreen, ResetPasswordScreen });
