// app.jsx — Coach alpha shell.

// Feature flag — hide the admin tab so we focus on the coaching flow first.
// Set to true to expose admin requests + allowlist again.
const ADMIN_TAB_VISIBLE = false;

const { useEffect, useState, useCallback, useRef, useMemo } = React;
const { useAuth, signIn, signUp, signOut, authedFetch } = window.CoachAuth;

// ─── Loading screen ────────────────────────────────────────────────────────
// Used at boot + during stale-while-revalidate gaps. Intentionally NOT used
// once we have any cached data — render the shell from cache and let async
// refreshes happen invisibly.
function LoadingScreen({ status = 'loading…' }) {
  return (
    <div className="boot-screen">
      <div className="boot-mark">coach</div>
      <div className="boot-spinner" aria-hidden="true" />
      <div className="boot-status">{status}</div>
    </div>
  );
}

// ─── Bootstrap cache (localStorage, stale-while-revalidate) ────────────────
// Calls /api/bootstrap, but reads from localStorage synchronously so the
// shell renders instantly on subsequent visits. The fresh fetch updates
// state in the background. Cache is namespaced by supabase user id so
// switching accounts doesn't show stale data.
const BOOTSTRAP_CACHE_KEY = 'coach-bootstrap-v1';

function readBootstrapCache(supabaseUserId) {
  try {
    const raw = localStorage.getItem(BOOTSTRAP_CACHE_KEY);
    if (!raw) return null;
    const obj = JSON.parse(raw);
    if (obj && obj.user && obj.user.supabase_user_id === supabaseUserId) {
      return obj;
    }
  } catch (_e) {}
  return null;
}

function writeBootstrapCache(data) {
  try { localStorage.setItem(BOOTSTRAP_CACHE_KEY, JSON.stringify(data)); }
  catch (_e) {}
}

function clearBootstrapCache() {
  try { localStorage.removeItem(BOOTSTRAP_CACHE_KEY); }
  catch (_e) {}
}

function useBootstrap(session) {
  // Hydrate synchronously from cache so the first render is instant.
  const supaId = session?.user?.id;
  const [data, setData] = useState(() => supaId ? readBootstrapCache(supaId) : null);
  const [error, setError] = useState(null);
  const [refreshing, setRefreshing] = useState(false);
  const lastFetchedFor = useRef(null);

  const refresh = useCallback(async () => {
    if (!supaId) return;
    setRefreshing(true);
    try {
      const fresh = await authedFetch('/api/bootstrap');
      setData(fresh);
      setError(null);
      writeBootstrapCache(fresh);
    } catch (e) {
      setError(e);
    } finally {
      setRefreshing(false);
    }
  }, [supaId]);

  // Refetch only when the actual user changes — NOT on token refresh, which
  // also changes the session reference but not the user.
  useEffect(() => {
    if (!supaId) {
      setData(null);
      setError(null);
      lastFetchedFor.current = null;
      return;
    }
    if (lastFetchedFor.current === supaId) return;
    lastFetchedFor.current = supaId;
    refresh();
  }, [supaId, refresh]);

  return { data, error, refreshing, refresh };
}

// ─── Auth screen ───────────────────────────────────────────────────────────

function AuthScreen() {
  const [mode, setMode] = useState('signin'); // 'signin' | 'signup'
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [pending, setPending] = useState(false);
  const [error, setError] = useState(null);
  const [info, setInfo] = useState(null);

  const submit = async (e) => {
    e.preventDefault();
    setError(null);
    setInfo(null);
    setPending(true);
    try {
      if (mode === 'signin') {
        await signIn(email, password);
      } else {
        const { user, session } = await signUp(email, password);
        if (user && !session) {
          setInfo("Check your email to confirm the address, then sign in.");
          setMode('signin');
        }
      }
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setPending(false);
    }
  };

  return (
    <div className="auth-shell">
      <div className="auth-card">
        <div className="auth-mark">coach</div>
        <div className="auth-tabs">
          <button
            className={`auth-tab${mode === 'signin' ? ' is-on' : ''}`}
            onClick={() => { setMode('signin'); setError(null); setInfo(null); }}
            type="button"
          >sign in</button>
          <button
            className={`auth-tab${mode === 'signup' ? ' is-on' : ''}`}
            onClick={() => { setMode('signup'); setError(null); setInfo(null); }}
            type="button"
          >create account</button>
        </div>
        <form onSubmit={submit} className="auth-form">
          <label className="auth-field">
            <span>email</span>
            <input type="email" required autoComplete="email"
              value={email} onChange={(e) => setEmail(e.target.value)} />
          </label>
          <label className="auth-field">
            <span>password</span>
            <input type="password" required minLength={6}
              autoComplete={mode === 'signin' ? 'current-password' : 'new-password'}
              value={password} onChange={(e) => setPassword(e.target.value)} />
          </label>
          {error && <div className="auth-msg auth-err">{error}</div>}
          {info && <div className="auth-msg auth-info">{info}</div>}
          <button className="auth-submit" type="submit" disabled={pending}>
            {pending ? '…' : (mode === 'signin' ? 'sign in' : 'create account')}
          </button>
        </form>
      </div>
    </div>
  );
}

// ─── Garmin link card ──────────────────────────────────────────────────────

function GarminCard({ onDataChanged, onLinked, initialStatus }) {
  // Hydrate from the parent's bootstrap data so we render content immediately
  // instead of showing "…" during a redundant /api/garmin/status fetch.
  const [status, setStatus] = useState(() =>
    initialStatus ? { loading: false, ...initialStatus } : { loading: true, linked: false }
  );
  const [syncStatus, setSyncStatus] = useState({ running: false });
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [pending, setPending] = useState(false);
  const [syncErr, setSyncErr] = useState(null);
  const [error, setError] = useState(null);

  const refresh = useCallback(async () => {
    try {
      const [s, ss] = await Promise.all([
        authedFetch('/api/garmin/status'),
        authedFetch('/api/sync'),
      ]);
      setStatus({ loading: false, ...s });
      setSyncStatus(ss);
    } catch (e) {
      setStatus({ loading: false, linked: false });
      setError(e.message);
    }
  }, []);

  useEffect(() => { refresh(); }, [refresh]);

  // Poll while a sync is running.
  useEffect(() => {
    if (!syncStatus.running) return;
    let cancelled = false;
    const tick = async () => {
      try {
        const ss = await authedFetch('/api/sync');
        if (cancelled) return;
        setSyncStatus(ss);
        if (!ss.running) {
          if (ss.last_error) setSyncErr(ss.last_error);
          await refresh();
          if (onDataChanged) onDataChanged();
        }
      } catch (e) {
        if (!cancelled) setSyncErr(e.message);
      }
    };
    const id = setInterval(tick, 2500);
    return () => { cancelled = true; clearInterval(id); };
  }, [syncStatus.running, refresh, onDataChanged]);

  const link = async (e) => {
    e.preventDefault();
    setError(null);
    setPending(true);
    try {
      await authedFetch('/api/garmin/link', {
        method: 'POST',
        body: JSON.stringify({ username, password }),
      });
      setUsername('');
      setPassword('');
      await refresh();
      if (onLinked) onLinked();
    } catch (e) {
      setError(e.message);
    } finally {
      setPending(false);
    }
  };

  const unlink = async () => {
    if (!confirm('Disconnect Garmin? Your stored credentials will be removed.')) return;
    try {
      await authedFetch('/api/garmin/link', { method: 'DELETE' });
      await refresh();
      if (onLinked) onLinked();
    } catch (e) {
      setError(e.message);
    }
  };

  const sync = async () => {
    setSyncErr(null);
    try {
      const r = await authedFetch('/api/sync?days=14', { method: 'POST' });
      setSyncStatus(r);
    } catch (e) {
      setSyncErr(e.message);
    }
  };

  if (status.loading) return <section className="card"><div className="card-hd">garmin</div><div className="card-body muted">…</div></section>;

  return (
    <section className="card">
      <div className="card-hd">
        <span>garmin</span>
        <span className={`pill pill-${status.linked && status.status === 'ok' ? 'good' : status.linked ? 'mid' : 'off'}`}>
          {status.linked ? (status.status || 'linked') : 'not linked'}
        </span>
      </div>

      {status.linked ? (
        <div className="card-body">
          <div className="kv">
            <div className="kv-row"><span>status</span><span>{status.status}</span></div>
            {status.last_refreshed_at && (
              <div className="kv-row"><span>last refresh</span><span>{new Date(status.last_refreshed_at).toLocaleString()}</span></div>
            )}
            {status.last_error && (
              <div className="kv-row"><span>last error</span><span className="err">{status.last_error}</span></div>
            )}
            {syncStatus.last_summary && (
              <div className="kv-row">
                <span>last sync</span>
                <span>
                  {syncStatus.last_summary.metrics_written} days · {syncStatus.last_summary.new_sessions_actual} new sessions
                </span>
              </div>
            )}
          </div>
          {syncErr && <div className="card-err">{syncErr}</div>}
          <div className="card-actions">
            <button className="btn-ghost" onClick={unlink} disabled={syncStatus.running}>disconnect</button>
            <button className="btn-primary" onClick={sync} disabled={syncStatus.running}>
              {syncStatus.running ? 'syncing…' : 'sync 14 days'}
            </button>
          </div>
        </div>
      ) : (
        <form onSubmit={link} className="card-body">
          <p className="muted small">
            Connect your Garmin Connect account so the coach can read your readiness, sleep, HRV, and activities.
            Credentials are encrypted at rest with a key stored only on this server.
          </p>
          <label className="field">
            <span>email or username</span>
            <input type="text" required autoComplete="off"
              value={username} onChange={(e) => setUsername(e.target.value)} />
          </label>
          <label className="field">
            <span>password</span>
            <input type="password" required autoComplete="off"
              value={password} onChange={(e) => setPassword(e.target.value)} />
          </label>
          {error && <div className="card-err">{error}</div>}
          <div className="card-actions">
            <button className="btn-primary" type="submit" disabled={pending}>
              {pending ? 'verifying…' : 'connect garmin'}
            </button>
          </div>
        </form>
      )}
    </section>
  );
}

// ─── Data card — what we have so far ───────────────────────────────────────

function DataCard({ refreshKey }) {
  const [dash, setDash] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const r = await authedFetch('/api/dashboard');
        if (!cancelled) setDash(r);
      } catch (e) {
        if (!cancelled) setError(e.message);
      }
    })();
    return () => { cancelled = true; };
  }, [refreshKey]);

  if (error) return <section className="card"><div className="card-hd">data</div><div className="card-body card-err">{error}</div></section>;
  if (!dash) return <section className="card"><div className="card-hd">data</div><div className="card-body muted">…</div></section>;

  const s = dash.summary || {};
  // Last day with any metrics — useful for a quick "did the sync land?" check.
  const lastWithData = [...(dash.history || [])].reverse().find(
    (h) => Number.isFinite(h.garmin_readiness) || Number.isFinite(h.sleep_hours)
  );

  return (
    <section className="card">
      <div className="card-hd">
        <span>your data</span>
        <span className="pill pill-off">{s.days_with_metrics || 0} days</span>
      </div>
      <div className="card-body">
        <div className="kv">
          <div className="kv-row"><span>days with metrics</span><span>{s.days_with_metrics || 0}</span></div>
          <div className="kv-row"><span>sessions logged</span><span>{s.sessions_logged || 0}</span></div>
          {s.latest_metrics_date && (
            <div className="kv-row"><span>latest date</span><span>{s.latest_metrics_date}</span></div>
          )}
        </div>
        {lastWithData && (
          <div className="data-snapshot">
            <div className="data-snapshot-hd">{lastWithData.date}</div>
            <div className="data-snapshot-grid">
              <Metric label="readiness" value={lastWithData.garmin_readiness} unit="" />
              <Metric label="body batt." value={lastWithData.garmin_body_battery} unit="" />
              <Metric label="hrv" value={lastWithData.hrv} unit=" ms" />
              <Metric label="rhr" value={lastWithData.rhr} unit=" bpm" />
              <Metric label="sleep" value={lastWithData.sleep_hours} unit=" h" digits={1} />
              <Metric label="weight" value={lastWithData.weight_kg} unit=" kg" digits={1} />
            </div>
          </div>
        )}
        {!lastWithData && (
          <p className="muted small">
            No metrics yet. Connect Garmin and run a sync to backfill the last 14 days.
          </p>
        )}
      </div>
    </section>
  );
}

function Metric({ label, value, unit, digits = 0 }) {
  const has = Number.isFinite(value);
  return (
    <div className="metric">
      <div className="metric-lbl">{label}</div>
      <div className="metric-val">
        {has ? Number(value).toFixed(digits) : '—'}
        {has && <span className="metric-unit">{unit}</span>}
      </div>
    </div>
  );
}

// ─── Goals card ────────────────────────────────────────────────────────────

// Goal taxonomy — drives the visual cards + the per-goal field map.
// IDs are stored as `goals.goal_type`; `details_json` carries the rest.
const GOAL_OPTIONS = [
  { id: 'faster',     icon: '⚡', name: 'get faster',         desc: 'pace at current distances' },
  { id: 'power',      icon: '△',  name: 'power & strides',    desc: 'hills, plyos, economy' },
  { id: 'endurance',  icon: '∿',  name: 'build endurance',    desc: 'extend long sessions' },
  { id: 'consistent', icon: '◷',  name: 'stay consistent',    desc: 'habit, maintenance' },
  { id: 'weight',     icon: '◐',  name: 'lose weight',        desc: 'calorie-aware plan' },
  { id: 'race',       icon: '◆',  name: 'race prep',          desc: 'event-targeted block' },
  { id: 'return',     icon: '↻',  name: 'return from break',  desc: 'deload + ramp' },
  { id: 'muscle',     icon: '◼',  name: 'build muscle',       desc: 'strength + hypertrophy' },
];

const GOAL_FIELD_MAP = {
  faster:     ['targetDistance', 'currentPb'],
  power:      ['hillAccess', 'stridesExperience'],
  endurance:  ['targetLong', 'currentLong'],
  consistent: ['minSessions', 'streakTarget'],
  weight:     ['targetDelta', 'timeframe'],
  race:       ['eventDate', 'eventDistance', 'targetTime', 'terrain'],
  return:     ['breakDuration', 'breakReason'],
  muscle:     ['split', 'gymAccess', 'trainingAge'],
};

const FIELD_DEFS = {
  targetDistance:    { label: 'target distance (km)', type: 'number', placeholder: 'e.g. 10' },
  currentPb:         { label: 'current PB at that distance', type: 'text', placeholder: 'e.g. 52:30' },
  hillAccess:        { label: 'hill access', type: 'select', options: ['yes — regular hills nearby', 'limited', 'flat only'] },
  stridesExperience: { label: 'experience with strides/plyos', type: 'select', options: ['none', 'some', 'regular'] },
  targetLong:        { label: 'target long-session (min)', type: 'number', placeholder: 'e.g. 90' },
  currentLong:       { label: 'current longest (min)', type: 'number', placeholder: 'e.g. 55' },
  minSessions:       { label: 'minimum sessions/week', type: 'number', placeholder: 'e.g. 3' },
  streakTarget:      { label: 'streak target (weeks)', type: 'number', placeholder: 'e.g. 12' },
  targetDelta:       { label: 'target weight change (kg)', type: 'number', placeholder: 'e.g. -4' },
  timeframe:         { label: 'timeframe (weeks)', type: 'number', placeholder: 'e.g. 12' },
  eventDate:         { label: 'event date', type: 'date' },
  eventDistance:     { label: 'event distance (km)', type: 'number', placeholder: 'e.g. 21.1' },
  targetTime:        { label: 'target time (optional)', type: 'text', placeholder: 'e.g. 1:45:00' },
  terrain:           { label: 'terrain', type: 'select', options: ['road', 'trail', 'track', 'mixed'] },
  breakDuration:     { label: 'break length (weeks)', type: 'number', placeholder: 'e.g. 6' },
  breakReason:       { label: 'reason', type: 'select', options: ['injury', 'illness', 'life / travel', 'other'] },
  split:             { label: 'split preference', type: 'select', options: ['let coach decide', 'push-pull-legs', 'upper-lower', 'full body'] },
  gymAccess:         { label: 'equipment tier', type: 'select', options: ['full gym', 'home rack + barbell', 'dumbbells only', 'bodyweight'] },
  trainingAge:       { label: 'lifting experience', type: 'select', options: ['beginner (<1y)', 'intermediate (1-3y)', 'advanced (3y+)'] },
};

const CONSTRAINTS = ['knee', 'back', 'shoulder', 'hip', 'asthma', 'pregnancy', 'returning from injury', 'other'];
const DAY_LABELS = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
const DAY_LONG = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
const MAX_EQUIPMENT = 12;
const MAX_OTHER = 50;

function GoalCard({ onSaved, initialGoal }) {
  // null = still loading, false / object = ready
  const [active, setActive] = useState(
    initialGoal !== undefined ? (initialGoal || false) : null
  );
  const [editing, setEditing] = useState(false);
  const [error, setError] = useState(null);
  const [pending, setPending] = useState(false);

  // Form state — mirrors the design's data model.
  const [goalId, setGoalId] = useState(null);
  const [fields, setFields] = useState({ sessions: '5' });
  const [constraints, setConstraints] = useState(new Set());
  const [otherConstraint, setOtherConstraint] = useState('');
  const [days, setDays] = useState(new Set([0, 1, 2, 3, 4]));
  const [equipment, setEquipment] = useState([]);
  const [equipDraft, setEquipDraft] = useState('');

  // Hydrate the form from an existing goal when entering edit mode (or when
  // first opening the form on a returning user with `details_json`).
  const hydrateFromActive = useCallback((g) => {
    if (!g) return;
    const d = g.details_json || {};
    setGoalId(d.goal || g.goal_type || null);
    setFields({ sessions: '5', ...(d.fields || {}) });
    setConstraints(new Set(d.constraints || []));
    setOtherConstraint(d.otherConstraint || '');
    setDays(new Set(d.days || [0, 1, 2, 3, 4]));
    setEquipment(Array.isArray(d.equipment) ? d.equipment : []);
  }, []);

  const refresh = useCallback(async () => {
    try {
      const r = await authedFetch('/api/goals/active');
      setActive(r.goal || false);
    } catch (e) {
      setError(e.message);
    }
  }, []);
  useEffect(() => { refresh(); }, [refresh]);

  const startEdit = () => {
    hydrateFromActive(active);
    setEditing(true);
  };

  const cancelEdit = () => {
    setEditing(false);
    setError(null);
  };

  const selectGoal = (id) => {
    setGoalId(id);
    setError(null);
  };

  const updateField = (key, value) => setFields((f) => ({ ...f, [key]: value }));

  const toggleConstraint = (c) => {
    setConstraints((prev) => {
      const next = new Set(prev);
      if (next.has(c)) {
        next.delete(c);
        if (c === 'other') setOtherConstraint('');
      } else {
        next.add(c);
      }
      return next;
    });
  };

  const toggleDay = (i) => {
    setDays((prev) => {
      const next = new Set(prev);
      if (next.has(i)) next.delete(i); else next.add(i);
      return next;
    });
  };

  const commitEquip = () => {
    let v = equipDraft.trim().toLowerCase().slice(0, 20);
    v = v.replace(/[^a-z0-9 \-/]/g, '').trim();
    if (!v) { setEquipDraft(''); return; }
    if (equipment.length >= MAX_EQUIPMENT) { setEquipDraft(''); return; }
    if (equipment.includes(v)) { setEquipDraft(''); return; }
    setEquipment([...equipment, v]);
    setEquipDraft('');
  };

  const onEquipKey = (e) => {
    if (e.key === ' ' || e.key === 'Enter' || e.key === ',') {
      e.preventDefault();
      commitEquip();
    } else if (e.key === 'Backspace' && equipDraft === '' && equipment.length > 0) {
      setEquipment(equipment.slice(0, -1));
    }
  };

  const removeEquip = (eq) => setEquipment(equipment.filter((x) => x !== eq));

  // Live preview + validation warnings.
  const preview = useMemoPreview(goalId, fields, constraints, otherConstraint, days, equipment);

  const save = async (e) => {
    e.preventDefault();
    if (!goalId) {
      setError('pick a goal first');
      return;
    }
    setError(null);
    setPending(true);
    try {
      const cleanFields = Object.fromEntries(
        Object.entries(fields).filter(([, v]) => v !== '' && v != null)
      );
      const details = {
        goal: goalId,
        fields: cleanFields,
        constraints: [...constraints],
        otherConstraint: otherConstraint || null,
        days: [...days].sort((a, b) => a - b),
        equipment,
      };
      const body = {
        goal_type: goalId,
        details,
        activate: true,
      };
      await authedFetch('/api/goals', {
        method: 'POST',
        body: JSON.stringify(body),
      });
      setEditing(false);
      await refresh();
      if (onSaved) onSaved();
    } catch (e) {
      setError(e.message);
    } finally {
      setPending(false);
    }
  };

  if (active === null) {
    return <section className="card"><div className="card-hd">goal</div><div className="card-body muted">…</div></section>;
  }

  // ── Read-only summary mode (existing goal, not editing) ─────────────
  if (active && !editing) {
    const d = active.details_json || {};
    const opt = GOAL_OPTIONS.find((g) => g.id === (d.goal || active.goal_type));
    const goalLabel = opt ? opt.name : (active.goal_type || '').replace(/_/g, ' ');
    const dayList = (d.days && d.days.length) ? d.days.map((i) => DAY_LONG[i]).join(', ') : null;
    const constraintList = (d.constraints && d.constraints.length)
      ? d.constraints.map((c) => (c === 'other' && d.otherConstraint) ? `other (${d.otherConstraint})` : c).join(', ')
      : null;
    return (
      <section className="card">
        <div className="card-hd">
          <span>active goal</span>
          <button className="btn-ghost small" onClick={startEdit}>edit</button>
        </div>
        <div className="card-body">
          <div className="kv">
            <div className="kv-row"><span>goal</span><span>{goalLabel}</span></div>
            {Object.entries(d.fields || {}).map(([k, v]) => (
              <div key={k} className="kv-row"><span>{k}</span><span>{String(v)}</span></div>
            ))}
            {dayList && <div className="kv-row"><span>days</span><span>{dayList}</span></div>}
            {constraintList && <div className="kv-row"><span>constraints</span><span>{constraintList}</span></div>}
            {d.equipment && d.equipment.length > 0 && (
              <div className="kv-row"><span>equipment</span><span>{d.equipment.join(', ')}</span></div>
            )}
          </div>
        </div>
      </section>
    );
  }

  // ── Edit / first-set mode ─────────────────────────────────────────────
  const fieldIds = goalId ? (GOAL_FIELD_MAP[goalId] || []) : [];

  return (
    <section className="card goal-set">
      <div className="card-hd">
        <span>set your goal</span>
        {active && <button className="btn-ghost small" type="button" onClick={cancelEdit}>cancel</button>}
      </div>
      <div className="goal-set-body">
        <form onSubmit={save} className="goal-form">
          <span className="label">goal type</span>
          <div className="goal-grid">
            {GOAL_OPTIONS.map((g) => (
              <button
                type="button"
                key={g.id}
                className={`goal-tile${goalId === g.id ? ' is-on' : ''}`}
                onClick={() => selectGoal(g.id)}
              >
                <span className="goal-tile-icon">{g.icon}</span>
                <div className="goal-tile-name">{g.name}</div>
                <div className="goal-tile-desc">{g.desc}</div>
              </button>
            ))}
          </div>

          {goalId && (
            <div className="goal-fields reveal">
              <label className="field">
                <span>weekly sessions</span>
                <input type="number" min="1" max="14" value={fields.sessions || ''}
                       onChange={(e) => updateField('sessions', e.target.value)} />
              </label>
              {(() => {
                const rows = [];
                for (let i = 0; i < fieldIds.length; i += 2) {
                  const pair = [fieldIds[i], fieldIds[i + 1]].filter(Boolean);
                  rows.push(
                    <div key={i} className={pair.length === 2 ? 'field-row' : ''}>
                      {pair.map((fid) => {
                        const def = FIELD_DEFS[fid];
                        return (
                          <label className="field" key={fid}>
                            <span>{def.label}</span>
                            {def.type === 'select' ? (
                              <select value={fields[fid] || ''}
                                      onChange={(e) => updateField(fid, e.target.value)}>
                                <option value="">— choose —</option>
                                {def.options.map((o) => <option key={o} value={o}>{o}</option>)}
                              </select>
                            ) : (
                              <input type={def.type} placeholder={def.placeholder || ''}
                                     value={fields[fid] || ''}
                                     onChange={(e) => updateField(fid, e.target.value)} />
                            )}
                          </label>
                        );
                      })}
                    </div>
                  );
                }
                return rows;
              })()}
            </div>
          )}

          <div className="chip-group">
            <span className="label">constraints <em>tap any that apply</em></span>
            <div className="chips">
              {CONSTRAINTS.map((c) => (
                <button type="button" key={c}
                        className={`chip${constraints.has(c) ? ' is-on' : ''}`}
                        onClick={() => toggleConstraint(c)}>{c}</button>
              ))}
            </div>
            {constraints.has('other') && (
              <div className="other-input-wrap reveal">
                <input type="text" maxLength={MAX_OTHER}
                       placeholder={`describe (max ${MAX_OTHER} chars)`}
                       value={otherConstraint}
                       onChange={(e) => setOtherConstraint(e.target.value.replace(/[\r\n]+/g, ' ').slice(0, MAX_OTHER))} />
                <div className={`hint-counter${otherConstraint.length > MAX_OTHER - 10 ? ' warn' : ''}`}>
                  {otherConstraint.length} / {MAX_OTHER}
                </div>
              </div>
            )}
          </div>

          <div className="chip-group">
            <span className="label">available days</span>
            <div className="chips">
              {DAY_LABELS.map((d, i) => (
                <button type="button" key={i} title={DAY_LONG[i]}
                        className={`chip chip-day${days.has(i) ? ' is-on' : ''}`}
                        onClick={() => toggleDay(i)}>{d}</button>
              ))}
            </div>
          </div>

          <div className="chip-group">
            <span className="label">
              equipment / environment
              <em>type and press space · max 20 chars per item · leave empty to use equipment tier only</em>
            </span>
            <div className="equip-box" onClick={(e) => e.currentTarget.querySelector('input')?.focus()}>
              {equipment.map((eq) => (
                <span key={eq} className="chip is-on">
                  {eq}
                  <button type="button" className="chip-x"
                          onClick={(e) => { e.stopPropagation(); removeEquip(eq); }}>×</button>
                </span>
              ))}
              <input type="text" maxLength={20}
                     placeholder={equipment.length === 0 ? 'e.g. treadmill, gym, pool, home rack…' : ''}
                     value={equipDraft}
                     onChange={(e) => setEquipDraft(e.target.value)}
                     onKeyDown={onEquipKey}
                     onBlur={commitEquip} />
            </div>
            <div className={`hint-counter${equipment.length >= MAX_EQUIPMENT ? ' warn' : ''}`}>
              {equipment.length} / {MAX_EQUIPMENT} items
            </div>
          </div>

          {error && <div className="card-err">{error}</div>}
          <div className="card-actions">
            <button type="submit" className="btn-primary" disabled={pending || !goalId}>
              {pending ? 'saving…' : 'save goal'}
            </button>
          </div>
        </form>

        <aside className="goal-preview">
          <div className="goal-preview-hd">
            <span className="goal-preview-dot" />
            COACH PREVIEW
          </div>
          <div className="goal-preview-body">
            {preview.lines}
          </div>
          {preview.warnings.length > 0 && (
            <div className="goal-preview-warns">
              {preview.warnings.map((w, i) => (
                <div key={i} className="goal-preview-warn">⚠ {w}</div>
              ))}
            </div>
          )}
        </aside>
      </div>
    </section>
  );
}

// Build the live "COACH PREVIEW" panel content. Returns React nodes + any
// validation warnings derived from the current form state.
function useMemoPreview(goalId, fields, constraints, otherConstraint, days, equipment) {
  return useMemo(() => {
    if (!goalId) {
      return {
        lines: <div className="goal-preview-empty">pick a goal to see how the coach will read your inputs.</div>,
        warnings: [],
      };
    }
    const opt = GOAL_OPTIONS.find((g) => g.id === goalId);
    const fieldNodes = Object.entries(fields)
      .filter(([, v]) => v !== '' && v != null)
      .map(([k, v]) => (
        <div key={k}>
          <span className="goal-preview-key">{k}:</span>{' '}
          <span className="goal-preview-val">{String(v)}</span>
        </div>
      ));
    const dayList = [...days].sort((a, b) => a - b).map((i) => DAY_LONG[i]).join(', ') || '—';
    const constraintItems = [...constraints].map((c) =>
      c === 'other' && otherConstraint ? `other (${otherConstraint})` : c
    );
    const constraintList = constraintItems.join(', ') || 'none';
    const tier = fields.gymAccess;
    const equipNode = equipment.length > 0
      ? equipment.join(', ')
      : tier
        ? <em className="goal-preview-empty">— using equipment tier: {tier}</em>
        : <em className="goal-preview-empty">— none specified, coach will assume defaults</em>;

    const lines = (
      <>
        <div>
          <span className="goal-preview-key">goal:</span>{' '}
          <span className="goal-preview-val">{opt?.name || goalId}</span>
        </div>
        {fieldNodes}
        <div className="goal-preview-divider" />
        <div><span className="goal-preview-key">days:</span> <span className="goal-preview-val">{dayList}</span></div>
        <div><span className="goal-preview-key">constraints:</span> <span className="goal-preview-val">{constraintList}</span></div>
        <div><span className="goal-preview-key">equipment:</span> <span className="goal-preview-val">{equipNode}</span></div>
      </>
    );

    const warnings = [];
    if (goalId === 'race' && fields.eventDate) {
      const d = new Date(fields.eventDate);
      const daysOut = (d - new Date()) / (1000 * 60 * 60 * 24);
      if (daysOut < 14) warnings.push('event is less than 2 weeks out — coach will switch to taper-only mode.');
      if (daysOut > 540) warnings.push('event is more than 18 months out — consider a sub-goal in the meantime.');
    }
    if (fields.sessions && Number(fields.sessions) > 7) {
      warnings.push('7+ sessions/week is high — coach will plan double-day blocks carefully.');
    }
    if (constraints.has('returning from injury') && goalId !== 'return') {
      warnings.push('flagged returning from injury — consider "return from break" goal for safer ramp.');
    }

    return { lines, warnings };
  }, [goalId, fields, constraints, otherConstraint, days, equipment]);
}

// ─── Share card — let friends/family follow your plan ─────────────────────

function ShareCard() {
  const [status, setStatus] = useState(null);
  const [followers, setFollowers] = useState([]);
  const [pending, setPending] = useState(false);
  const [error, setError] = useState(null);
  const [copied, setCopied] = useState(false);

  const refresh = useCallback(async () => {
    try {
      const [s, f] = await Promise.all([
        authedFetch('/api/share/status'),
        authedFetch('/api/share/followers'),
      ]);
      setStatus(s);
      setFollowers(f.followers || []);
    } catch (e) {
      setError(e.message);
    }
  }, []);

  useEffect(() => { refresh(); }, [refresh]);

  const enable = async () => {
    setError(null);
    setPending(true);
    try {
      await authedFetch('/api/share/enable', { method: 'POST' });
      await refresh();
    } catch (e) {
      setError(e.message);
    } finally {
      setPending(false);
    }
  };

  const disable = async () => {
    if (!confirm('Disable sharing? Your share link will 404 until you re-enable. Existing follower rows are kept.')) return;
    setError(null);
    setPending(true);
    try {
      await authedFetch('/api/share/disable', { method: 'POST' });
      await refresh();
    } catch (e) {
      setError(e.message);
    } finally {
      setPending(false);
    }
  };

  const removeFollower = async (id) => {
    if (!confirm('Remove this follower? They lose access immediately.')) return;
    try {
      await authedFetch('/api/share/followers/' + id, { method: 'DELETE' });
      await refresh();
    } catch (e) {
      setError(e.message);
    }
  };

  const shareUrl = status?.share_slug
    ? `${window.location.origin}/p/${status.share_slug}`
    : null;

  const copyLink = async () => {
    if (!shareUrl) return;
    try {
      await navigator.clipboard.writeText(shareUrl);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch (_e) {
      // clipboard write can fail in older browsers — fall back to selection prompt
      window.prompt('Copy this link:', shareUrl);
    }
  };

  if (!status) return <section className="card"><div className="card-hd">share my plan</div><div className="card-body muted">…</div></section>;

  const remaining = status.max_followers - status.active_count - status.pending_count;
  const atCap = remaining <= 0;

  return (
    <section className="card">
      <div className="card-hd">
        <span>share my plan</span>
        <span className={`pill ${status.enabled ? 'pill-good' : 'pill-off'}`}>
          {status.enabled ? 'shared' : 'private'}
        </span>
      </div>
      <div className="card-body">
        {!status.enabled && (
          <>
            <p className="muted small">
              Generate a public link friends and family can follow. They submit their email,
              confirm via a link, and get a Monday digest of your week. Up to {status.max_followers} followers.
              No biometrics or workout structure leaks — just commitment + totals.
            </p>
            <div className="card-actions">
              <button className="btn-primary" onClick={enable} disabled={pending}>
                {pending ? '…' : 'enable share link'}
              </button>
            </div>
          </>
        )}

        {status.enabled && (
          <>
            <div className="share-link">
              <input className="share-link-input" type="text" readOnly value={shareUrl || ''} />
              <button className="btn-ghost small" onClick={copyLink}>{copied ? 'copied' : 'copy'}</button>
            </div>
            <div className="kv share-counts">
              <div className="kv-row"><span>active followers</span>
                <span>{status.active_count} / {status.max_followers}</span>
              </div>
              {status.pending_count > 0 && (
                <div className="kv-row"><span>pending confirm</span>
                  <span>{status.pending_count}</span>
                </div>
              )}
              {atCap && (
                <div className="kv-row"><span>capacity</span>
                  <span className="err">at cap — remove a follower to free a slot</span>
                </div>
              )}
            </div>

            {followers.length > 0 && (
              <ul className="follower-list">
                {followers.map((f) => (
                  <li key={f.id} className={`follower-row follower-${f.status}`}>
                    <span className="follower-email">{f.email_masked}</span>
                    <span className="follower-status">{f.status}</span>
                    {f.status !== 'active' && (
                      <span className="follower-meta muted">
                        {f.confirmed_at ? `confirmed ${f.confirmed_at.slice(0,10)}` :
                         f.unsubscribed_at ? `unsub ${f.unsubscribed_at.slice(0,10)}` :
                         `joined ${f.created_at.slice(0,10)}`}
                      </span>
                    )}
                    <button className="btn-ghost small" onClick={() => removeFollower(f.id)}>remove</button>
                  </li>
                ))}
              </ul>
            )}

            <div className="card-actions">
              <button className="btn-ghost" onClick={disable} disabled={pending}>disable</button>
            </div>
          </>
        )}

        {error && <div className="card-err">{error}</div>}
      </div>
    </section>
  );
}

// ─── Main shell ────────────────────────────────────────────────────────────

function Header({ user, view, setView, isAdmin }) {
  const showAdminTab = isAdmin && ADMIN_TAB_VISIBLE;
  return (
    <header className="topbar">
      <div className="topbar-mark">coach</div>
      {showAdminTab && (
        <nav className="topbar-nav">
          <button className={`topbar-tab${view === 'app' ? ' is-on' : ''}`}
                  onClick={() => setView('app')}>training</button>
          <button className={`topbar-tab${view === 'admin' ? ' is-on' : ''}`}
                  onClick={() => setView('admin')}>admin</button>
        </nav>
      )}
      <div className="topbar-right">
        <span className="topbar-email">{user?.email}</span>
        <button className="btn-ghost small" onClick={signOut}>sign out</button>
      </div>
    </header>
  );
}

// Compact "your setup" strip shown when both Garmin + Goal are configured.
// Click "manage setup" to expand the GarminCard + GoalCard inline for editing.
function StatusStrip({ garmin, goal, expanded, onToggle }) {
  const goalLabel = (goal.goal_type || 'goal').replace(/_/g, ' ');
  const phaseLabel = goal.current_phase ? goal.current_phase.replace(/_/g, ' ') : null;
  return (
    <section className="status-strip">
      <span className="status-strip-pill status-strip-pill-good">garmin: {garmin.status || 'ok'}</span>
      <span className="status-strip-text">{goalLabel}</span>
      {phaseLabel && <>
        <span className="status-strip-divider">·</span>
        <span className="status-strip-text">phase: {phaseLabel}</span>
      </>}
      <span className="status-strip-spacer" />
      <button className="btn-ghost small" onClick={onToggle}>
        {expanded ? 'hide setup' : 'manage setup'}
      </button>
    </section>
  );
}

function MainScreen({ session, bootstrap, refreshBootstrap }) {
  // Both views stay MOUNTED — toggling is CSS only — so switching admin ↔
  // training doesn't refetch every card.
  const [view, setView] = useState('app');

  // DataCard refresh key — bumped after a successful sync.
  const [dataKey, setDataKey] = useState(0);

  // Whether the GarminCard + GoalCard are visible in the ready stage.
  const [showSettings, setShowSettings] = useState(false);

  const isAdmin = !!bootstrap.user?.is_admin;
  const garmin = bootstrap.garmin || { linked: false };
  const goal = bootstrap.goal;

  // After a mutation that changes garmin or goal state, refresh the
  // bootstrap so all stages re-evaluate. After a Garmin sync we ALSO bump
  // dataKey so the DataCard re-fetches /api/dashboard.
  const onDataChanged = useCallback(() => {
    setDataKey((k) => k + 1);
    refreshBootstrap();
  }, [refreshBootstrap]);

  const garminOk = garmin.linked && garmin.status === 'ok';
  const stage = !garminOk ? 'garmin' : !goal ? 'goal' : 'ready';

  return (
    <div className="alpha-shell">
      <Header user={session.user} view={view} setView={setView} isAdmin={isAdmin} />

      {/* Admin view — only mounted when the feature flag is on, so the
          /api/admin/* calls don't fire on page load while we're hidden. */}
      {ADMIN_TAB_VISIBLE && (
        <div className="view-pane" style={{ display: view === 'admin' ? 'block' : 'none' }}>
          {isAdmin && <AdminPage />}
        </div>
      )}

      {/* Training view — three stages. */}
      <div className="view-pane" style={{ display: view === 'app' ? 'block' : 'none' }}>
        {stage === 'garmin' && (
          <main className="alpha-main">
            <div className="alpha-banner">
              <strong>Step 1 — connect your Garmin.</strong>
              <br />
              The coach reads your readiness, sleep, HRV, and activities directly from your watch.
              We can't help you train until this is connected.
            </div>
            <GarminCard
              initialStatus={garmin}
              onLinked={refreshBootstrap}
              onDataChanged={onDataChanged}
            />
          </main>
        )}

        {stage === 'goal' && (
          <main className="alpha-main alpha-main-wide">
            <div className="alpha-banner">
              <strong>Step 2 — what are you training for?</strong>
              <span className="alpha-mini-status"> · garmin connected ✓</span>
              <br />
              Pick a goal so the coach knows what to optimize for. You can change it any time.
            </div>
            <GoalCard initialGoal={goal} onSaved={refreshBootstrap} />
          </main>
        )}

        {stage === 'ready' && (
          <main className="alpha-main">
            <StatusStrip
              garmin={garmin}
              goal={goal}
              expanded={showSettings}
              onToggle={() => setShowSettings((v) => !v)}
            />
            {showSettings && (
              <>
                <GarminCard
                  initialStatus={garmin}
                  onLinked={refreshBootstrap}
                  onDataChanged={onDataChanged}
                />
                <GoalCard initialGoal={goal} onSaved={refreshBootstrap} />
              </>
            )}
            <DataCard refreshKey={dataKey} />
            {/* ShareCard temporarily hidden — focus on the core
                Garmin-→-goal-→-chat loop first. Re-enable when ready. */}
            {/* <ShareCard /> */}
            <window.CoachChat.ChatCard />
          </main>
        )}
      </div>
    </div>
  );
}

function WaitlistScreen({ email }) {
  const [status, setStatus] = useState({ loading: true, request: null });
  const [note, setNote] = useState('');
  const [pending, setPending] = useState(false);
  const [error, setError] = useState(null);

  const refresh = useCallback(async () => {
    try {
      const r = await authedFetch('/api/access/my-request');
      setStatus({ loading: false, request: r.request });
    } catch (e) {
      setStatus({ loading: false, request: null });
      setError(e.message);
    }
  }, []);

  useEffect(() => { refresh(); }, [refresh]);

  const submit = async (e) => {
    e.preventDefault();
    setError(null);
    setPending(true);
    try {
      await authedFetch('/api/access/request', {
        method: 'POST',
        body: JSON.stringify({ note: note || null }),
      });
      await refresh();
    } catch (e) {
      setError(e.message);
    } finally {
      setPending(false);
    }
  };

  if (status.loading) return <div className="boot-screen">…</div>;

  const req = status.request;

  return (
    <div className="auth-shell">
      <div className="auth-card">
        <div className="auth-mark">coach</div>
        <h2 className="waitlist-title">you're on the waitlist</h2>

        {req?.status === 'pending' && (
          <>
            <p className="muted small waitlist-body">
              Your request to use Coach as <strong>{email}</strong> is pending review.
              An admin will approve or deny it shortly. Come back to this page after they decide.
            </p>
            {req.request_note && (
              <p className="muted small waitlist-body">
                <span style={{ color: 'var(--fg-muted)' }}>Your note:</span> {req.request_note}
              </p>
            )}
          </>
        )}

        {req?.status === 'denied' && (
          <p className="muted small waitlist-body">
            Your previous request was declined{req.decision_note ? `: ${req.decision_note}` : '.'}
            {' '}You can submit a new request below.
          </p>
        )}

        {(!req || req.status === 'denied') && (
          <>
            <p className="muted small waitlist-body">
              Coach is invite-only right now. You're signed in as <strong>{email}</strong> —
              ask the admin to add you, or submit a request below.
            </p>
            <form onSubmit={submit} className="auth-form">
              <label className="auth-field">
                <span>note (optional)</span>
                <textarea
                  rows="3"
                  placeholder="who you are, why you want to try Coach…"
                  value={note}
                  onChange={(e) => setNote(e.target.value)}
                  maxLength={500}
                />
              </label>
              {error && <div className="auth-msg auth-err">{error}</div>}
              <button className="auth-submit" type="submit" disabled={pending}>
                {pending ? '…' : 'request access'}
              </button>
            </form>
          </>
        )}

        <div className="card-actions" style={{ marginTop: 18 }}>
          <button className="btn-ghost" onClick={signOut}>sign out</button>
        </div>
      </div>
    </div>
  );
}

// ─── Admin page ────────────────────────────────────────────────────────────

function AdminPage() {
  const [requests, setRequests] = useState(null);
  const [allowlist, setAllowlist] = useState(null);
  const [error, setError] = useState(null);
  const [addEmail, setAddEmail] = useState('');
  const [addNote, setAddNote] = useState('');
  const [pending, setPending] = useState(false);

  const refresh = useCallback(async () => {
    try {
      const [r, a] = await Promise.all([
        authedFetch('/api/admin/requests?status=pending'),
        authedFetch('/api/admin/allowlist'),
      ]);
      setRequests(r.requests || []);
      setAllowlist(a.entries || []);
    } catch (e) {
      setError(e.message);
    }
  }, []);

  useEffect(() => { refresh(); }, [refresh]);

  const decide = async (id, action) => {
    let note = null;
    if (action === 'deny') {
      note = window.prompt('Optional reason (visible to the requester):') || null;
    }
    try {
      await authedFetch(`/api/admin/requests/${id}/${action}`, {
        method: 'POST',
        body: JSON.stringify({ decision_note: note }),
      });
      await refresh();
    } catch (e) {
      setError(e.message);
    }
  };

  const addAllowlistEntry = async (e) => {
    e.preventDefault();
    setError(null);
    setPending(true);
    try {
      await authedFetch('/api/admin/allowlist', {
        method: 'POST',
        body: JSON.stringify({ email: addEmail, note: addNote || null }),
      });
      setAddEmail('');
      setAddNote('');
      await refresh();
    } catch (e) {
      setError(e.message);
    } finally {
      setPending(false);
    }
  };

  const removeAllowlistEntry = async (email) => {
    if (!confirm(`Remove ${email} from the allowlist? They lose access on next request.`)) return;
    try {
      await authedFetch('/api/admin/allowlist/' + encodeURIComponent(email), { method: 'DELETE' });
      await refresh();
    } catch (e) {
      setError(e.message);
    }
  };

  return (
    <div className="alpha-main">
      <div className="alpha-banner">
        <strong>Admin.</strong> Manage who can use Coach as a creator.
        Requests come in from the waitlist screen — approve to add their email to the allowlist.
      </div>

      {error && <div className="card-err" style={{ margin: '0 0 8px' }}>{error}</div>}

      <section className="card">
        <div className="card-hd">
          <span>pending requests</span>
          <span className="pill pill-off">{requests ? requests.length : '…'}</span>
        </div>
        <div className="card-body">
          {requests === null && <div className="muted">…</div>}
          {requests && requests.length === 0 && (
            <p className="muted small">No pending requests right now.</p>
          )}
          {requests && requests.length > 0 && (
            <ul className="follower-list">
              {requests.map((r) => (
                <li key={r.id} className="follower-row">
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
                    <span className="follower-email">{r.email}</span>
                    {r.request_note && (
                      <span className="follower-meta muted">"{r.request_note}"</span>
                    )}
                    <span className="follower-meta muted">
                      requested {r.created_at?.slice(0, 10)}
                    </span>
                  </div>
                  <span className="follower-status">{r.status}</span>
                  <button className="btn-ghost small" onClick={() => decide(r.id, 'deny')}>deny</button>
                  <button className="btn-primary" onClick={() => decide(r.id, 'approve')} style={{ padding: '4px 12px', fontSize: 11 }}>approve</button>
                </li>
              ))}
            </ul>
          )}
        </div>
      </section>

      <section className="card">
        <div className="card-hd">
          <span>creator allowlist</span>
          <span className="pill pill-good">{allowlist ? allowlist.length : '…'}</span>
        </div>
        <div className="card-body">
          <form onSubmit={addAllowlistEntry} className="field-row" style={{ marginBottom: 12 }}>
            <label className="field" style={{ flex: 2 }}>
              <span>add email directly</span>
              <input type="email" required value={addEmail}
                onChange={(e) => setAddEmail(e.target.value)} placeholder="friend@example.com" />
            </label>
            <label className="field" style={{ flex: 2 }}>
              <span>note <em>(optional)</em></span>
              <input type="text" value={addNote}
                onChange={(e) => setAddNote(e.target.value)} placeholder="how you know them" />
            </label>
            <div style={{ display: 'flex', alignItems: 'flex-end' }}>
              <button className="btn-primary" type="submit" disabled={pending || !addEmail}>add</button>
            </div>
          </form>

          {allowlist && allowlist.length === 0 && (
            <p className="muted small">No one on the allowlist yet. Approve a request or add an email above.</p>
          )}
          {allowlist && allowlist.length > 0 && (
            <ul className="follower-list">
              {allowlist.map((e) => (
                <li key={e.email} className="follower-row">
                  <span className="follower-email">{e.email}</span>
                  <span className="follower-meta muted">
                    {e.added_at?.slice(0, 10)} {e.added_by ? `by ${e.added_by}` : ''}
                  </span>
                  <button className="btn-ghost small" onClick={() => removeAllowlistEntry(e.email)}>remove</button>
                </li>
              ))}
            </ul>
          )}
        </div>
      </section>
    </div>
  );
}

function App() {
  const { loading: authLoading, session } = useAuth();
  const boot = useBootstrap(session);

  if (authLoading) return <LoadingScreen status="connecting…" />;
  if (!session) return <AuthScreen />;

  // Not on the invite allowlist — Supabase signed them up, our backend says no.
  if (boot.error?.body?.detail?.error === 'not_invited') {
    return <WaitlistScreen email={session.user?.email || boot.error.body.detail.email} />;
  }

  // Hard error AND no cached fallback — show retry/sign-out.
  if (boot.error && !boot.data) {
    return (
      <div className="auth-shell">
        <div className="auth-card">
          <div className="auth-mark">coach</div>
          <div className="auth-msg auth-err">
            Could not load your profile: {boot.error.message}
          </div>
          <div className="card-actions">
            <button className="btn-ghost" onClick={() => boot.refresh()}>retry</button>
            <button className="btn-ghost" onClick={signOut}>sign out</button>
          </div>
        </div>
      </div>
    );
  }

  // No cache + no data yet → first-ever load for this user. Brief spinner.
  // Subsequent visits hydrate from cache and skip this entirely.
  if (!boot.data) return <LoadingScreen status="loading your data…" />;

  return <MainScreen
    session={session}
    bootstrap={boot.data}
    refreshBootstrap={boot.refresh}
  />;
}

// Boot the supabase client first, then mount React.
window.CoachAuth.boot()
  .then(() => {
    ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  })
  .catch((e) => {
    document.getElementById('root').innerHTML =
      '<pre style="color:#f88;padding:24px;font-family:monospace">' +
      'Boot failed: ' + (e.message || e) +
      '\n\nCheck that the FastAPI backend is running and /api/config returns SUPABASE_URL + SUPABASE_PUBLISHABLE_KEY.' +
      '</pre>';
  });
