// X Autopilot dashboard. Three views (Dashboard / Analytics / Settings) behind a
// login when the deploy sets APP_PASSWORD. Slack + Claude Code (MCP) are still
// first-class ways to act; this is the web home base.
// API base defaults to same-origin (works under `vercel dev` / deployed); override
// with ?api=https://your-deploy for a static (serve.py) preview.

const { useState, useEffect } = React;

// API base defaults to same-origin. The ?api= override is a LOCAL preview
// convenience only (static serve.py pointing at a deploy) — honoring it on the
// public origin would let a crafted link (?api=https://evil.com) redirect every
// request, including the ones carrying a freshly-typed Anthropic/X secret, to an
// attacker. So it is ignored unless we're on localhost.
const IS_LOCAL = /^(localhost|127\.0\.0\.1|\[::1\])$/.test(location.hostname);
const API = IS_LOCAL ? (new URLSearchParams(location.search).get('api') || '') : '';
// The API answers expected failures (x_not_connected, quota_exceeded,
// already_published…) as 4xx with a { error, note } body explaining what to do.
// Throwing on !r.ok without reading that body is what turned every one of them
// into a bare "error 400" — so parse it first and carry the reason on the Error.
async function fail(r, path) {
  let body = null;
  try { body = await r.json(); } catch (_) {}
  const reason = body && (body.note || body.error);
  const e = new Error(reason || path + ' → ' + r.status);
  e.status = r.status; e.reason = body && body.error; e.note = body && body.note;
  // Upstream's own error body (e.g. X's JSON). Carried so explain() can show what
  // the provider actually said instead of our opaque code — the whole point of
  // plumbing `detail` through the API in the first place.
  e.detail = body && body.detail;
  return e;
}
async function get(path) {
  const r = await fetch(API + path, { credentials: 'include', headers: { 'sec-fetch-site': 'same-origin' } });
  if (r.status === 401) throw new Error('unauthorized');
  if (!r.ok) throw await fail(r, path);
  return r.json();
}
async function post(path, body) {
  const r = await fetch(API + path, { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json', 'sec-fetch-site': 'same-origin' }, body: JSON.stringify(body || {}) });
  if (r.status === 401) throw new Error('unauthorized');
  if (!r.ok) throw await fail(r, path);
  return r.json();
}
const pct = (n) => Math.round((n || 0) * 100) + '%';

// Only let through hrefs we trust to be inert: http(s), mailto, and in-app
// relative/anchor links. Tweet URLs, trend items (HN/Google News/Reddit) and
// setup links are data we don't fully control, so a poisoned `javascript:`/`data:`
// URL must never become a clickable link. Returns undefined for anything else
// (React then renders a non-navigating anchor).
const safeHref = (url) => {
  const u = String(url || '').trim();
  return /^(https?:\/\/|mailto:|\/|#)/i.test(u) ? u : undefined;
};

// Turn an API failure into something a user can act on. The X API's own 4xx codes
// are the ones that actually strand people: 401/403 almost always mean the stored
// OAuth token is missing the write scope or was revoked, which reads as "publish
// is broken" unless we say so.
const REASONS = {
  x_not_connected: 'X isn\'t connected. Go to Settings → Connections and sign in with X.',
  x_api_http_401: 'X rejected the token. Reconnect X in Settings → Connections.',
  x_api_http_402: 'X has no API credits left. Since X moved to pay-per-use, publishing draws on a prepaid balance — top it up in the X developer console (developer.x.com → Billing).',
  x_api_http_403: 'X refused the post. Usually the connection is missing write permission (reconnect X in Settings to re-grant "tweet.write") — or the app has no active API access.',
  x_api_http_429: 'X is rate-limiting the account. Wait a few minutes and try again.',
  x_api_unreachable: 'Could not reach X. Try again in a moment.',
  already_published: 'This post already went out.',
  quota_exceeded: 'You have hit your weekly post cap. Upgrade in Settings → Plan.',
};

// Pull the human sentence out of X's error body. X returns {detail, title} (and
// sometimes {errors:[{message}]}), and that text is far more useful than any code
// we could map — especially for failures we haven't seen yet.
function upstreamMessage(detail) {
  if (!detail) return null;
  if (typeof detail === 'string') return detail;
  if (detail.detail) return String(detail.detail);
  if (detail.title) return String(detail.title);
  if (Array.isArray(detail.errors) && detail.errors[0]) {
    return String(detail.errors[0].message || detail.errors[0].detail || '') || null;
  }
  return null;
}

function explain(e) {
  if (!e) return 'Something went wrong.';
  const key = e.reason || e.message;
  const upstream = upstreamMessage(e.detail);
  // A mapped reason is the friendliest, but still append what the provider said —
  // otherwise an unanticipated cause hides behind our best guess.
  if (REASONS[key]) return upstream ? `${REASONS[key]} (X said: ${upstream})` : REASONS[key];
  if (upstream) return `X refused this: ${upstream}`;
  if (e.note) return e.note;
  const m = String(e.message || e);
  return /→\s*\d+$/.test(m) ? 'Request failed (' + e.status + '). Try again.' : m;
}

// Logo — a bold X whose rising blade has broken away and launched: the autopilot
// is the part that moves on its own. Heavy strokes (not a thin line-icon), one
// accent blade. Inherits currentColor so it works on both themes.
// The AX monogram: a peaked "A" in the surface's own ink (currentColor, so it
// works on the light app and the dark landing alike) crossed by a tapered blue
// blade that completes the "X". `accent` overrides the blade — pass a flat colour
// where the gradient can't be referenced. Each instance needs a unique gradient
// id, otherwise multiple logos on one page collide.
let _logoSeq = 0;
function Logo({ size, accent }) {
  const s = size || 22;
  const [gid] = useState(() => 'axg' + (++_logoSeq));
  return (
    <svg className="logo" width={s} height={s} viewBox="0 0 48 48" fill="none" aria-hidden="true">
      {!accent && (
        <defs>
          <linearGradient id={gid} x1="14" y1="42" x2="46" y2="4" gradientUnits="userSpaceOnUse">
            <stop offset="0" stopColor="#2563EB" /><stop offset="1" stopColor="#60A5FA" />
          </linearGradient>
        </defs>
      )}
      <path d="M4 42 L21 6 L30 25" stroke="currentColor" strokeWidth="6" strokeLinejoin="miter" strokeLinecap="square" fill="none" />
      <path d="M12 31 L27 31" stroke="currentColor" strokeWidth="6" strokeLinecap="square" />
      <path d="M46 4 L34 4 L14 42 L27 42 Z" fill={accent || `url(#${gid})`} />
    </svg>
  );
}

function Tile({ n, l }) { return <div className="tile"><div className="n">{n}</div><div className="l">{l}</div></div>; }

// A post in the queue. When `onChange` is passed the card is actionable —
// approve / edit / ask-AI-to-revise / reject, and publish-to-X once approved.
function Post({ p, onChange }) {
  const [mode, setMode] = useState(null); // null | 'edit' | 'revise'
  const [text, setText] = useState(p.text);
  const [instruction, setInstruction] = useState('');
  const [busy, setBusy] = useState(null);
  const [err, setErr] = useState(null);

  const act = async (action, extra) => {
    setBusy(action); setErr(null);
    try {
      const r = await post('/api/v1/posts', { action, id: p.id, ...extra });
      if (!r.ok) { setErr(r.note || r.error || 'failed'); return; }
      setMode(null);
      if (onChange) await onChange();
    } catch (e) {
      // The API returns 4xx with a reason for expected failures (no X connected, quota).
      setErr(explain(e));
    } finally { setBusy(null); }
  };

  const actionable = !!onChange && (p.status === 'draft' || p.status === 'approved');

  return (
    <div className="post">
      {mode === 'edit'
        ? <textarea className="inp" rows={5} value={text} onChange={(e) => setText(e.target.value)} />
        : <div className="txt">{p.text}</div>}

      {mode === 'revise' && (
        <input className="inp" value={instruction} onChange={(e) => setInstruction(e.target.value)}
          placeholder="How should it change? e.g. 'shorter, drop the last line'"
          onKeyDown={(e) => { if (e.key === 'Enter' && instruction.trim()) act('revise', { instruction: instruction.trim() }); }} />
      )}

      <div className="meta">
        <span className={'badge b-' + p.status}>{p.status}</span>
        <span className="pill">{p.type}</span>
        <code>{p.id}</code>
        <span>· {p.topic}</span>
        {p.url && <a href={safeHref(p.url)} target="_blank" rel="noreferrer">view on X ↗</a>}
        {p.text && p.text.length > 280 && <span className="over">⚠ {p.text.length}/280</span>}
        {/* A link makes X charge 13× to publish AND throttles the post's reach.
            Worth surfacing at the moment of approval, when it can still be cut. */}
        {p.hasUrl && (
          <span className="over" title="X throttles the reach of posts with links, and charges 13× to publish them. Cut the link unless the post needs it.">
            🔗 link — lower reach
          </span>
        )}
        {p.review && p.review.scrollStopping != null && (
          <span className="pill" title={`Editor pass — scroll-stopping ${p.review.scrollStopping}/10, voice match ${p.review.voiceMatch}/10${p.review.defectCount ? `, ${p.review.defectCount} defect(s) fixed` : ''}`}>
            ✓ reviewed {p.review.scrollStopping}/10
          </span>
        )}
      </div>

      {actionable && (
        <div className="acts">
          {mode === 'edit' ? (
            <>
              <button className="btn sm" onClick={() => act('edit', { text })} disabled={busy || !text.trim()}>{busy === 'edit' ? 'Saving…' : 'Save & approve'}</button>
              <button className="btn sm ghost" onClick={() => { setMode(null); setText(p.text); }} disabled={busy}>Cancel</button>
            </>
          ) : mode === 'revise' ? (
            <>
              <button className="btn sm" onClick={() => act('revise', { instruction: instruction.trim() })} disabled={busy || !instruction.trim()}>{busy === 'revise' ? 'Revising…' : 'Revise'}</button>
              <button className="btn sm ghost" onClick={() => setMode(null)} disabled={busy}>Cancel</button>
            </>
          ) : (
            <>
              {p.status === 'draft' && <button className="btn sm" onClick={() => act('approve')} disabled={busy}>{busy === 'approve' ? 'Approving…' : 'Post as-is'}</button>}
              <button className="btn sm ghost" onClick={() => { setText(p.text); setMode('edit'); }} disabled={busy}>Edit</button>
              <button className="btn sm ghost" onClick={() => setMode('revise')} disabled={busy}>Ask AI to revise</button>
              <button className="btn sm ghost" onClick={() => act('publish_now')} disabled={busy} title="Post this to X now">{busy === 'publish_now' ? 'Publishing…' : 'Publish to X'}</button>
              <button className="btn sm ghost danger" onClick={() => act('reject')} disabled={busy}>Reject</button>
            </>
          )}
        </div>
      )}
      {err && <p className="err" style={{ margin: '8px 0 0' }}>{err}</p>}
    </div>
  );
}

function Inspiration() {
  const [items, setItems] = useState(null);
  const [text, setText] = useState('');
  const [note, setNote] = useState('');
  const [busy, setBusy] = useState(false);

  const load = () => get('/api/v1/inspiration').then((d) => setItems(d.inspiration)).catch(() => setItems([]));
  useEffect(() => { load(); }, []);

  const save = async () => {
    if (!text.trim()) return;
    setBusy(true);
    try { await post('/api/v1/inspiration', { text: text.trim(), note: note.trim() }); setText(''); setNote(''); await load(); }
    finally { setBusy(false); }
  };
  const remove = async (id) => { await post('/api/v1/inspiration', { action: 'remove', id }); load(); };

  return (
    <div className="section">
      <h2>Inspiration — drop posts you like</h2>
      <div className="post">
        <textarea className="inp" rows={3} value={text} onChange={(e) => setText(e.target.value)} placeholder="Paste a post you love — it becomes style copy + inspiration for future drafts…" />
        <input className="inp" value={note} onChange={(e) => setNote(e.target.value)} placeholder="Optional: why you like it / what to emulate" />
        <button className="btn" onClick={save} disabled={busy || !text.trim()}>{busy ? 'Saving…' : 'Save inspiration'}</button>
      </div>
      {items && items.length > 0 && items.map((it) => (
        <div className="post" key={it.id}>
          <div className="txt">{it.text}</div>
          <div className="meta">
            <code>{it.id}</code>{it.note && <span>· {it.note}</span>}{it.source && <span className="pill">{it.source}</span>}
            <a onClick={() => remove(it.id)} style={{ cursor: 'pointer', marginLeft: 'auto' }}>remove ✕</a>
          </div>
        </div>
      ))}
      {items && items.length === 0 && <p className="empty">Nothing saved yet — paste your first above, or use <code>/x like</code> in Slack.</p>}
    </div>
  );
}

// First run — a conversational guide. It explains what this is, infers their
// topics from a plain answer about what they're building, learns their voice
// from a pasted sample, and hands them their first drafts. All on the free
// trial, so nothing is asked for up front.
function OnboardAgent({ onDone, preview }) {
  const [msgs, setMsgs] = useState([]);
  const [input, setInput] = useState('');
  const [busy, setBusy] = useState(true);
  const [done, setDone] = useState(false);
  const [applied, setApplied] = useState({});
  const [importing, setImporting] = useState(0); // >0 = reading N posts from X
  const endRef = React.useRef(null);

  const push = (role, content) => setMsgs((m) => m.concat([{ role, content }]));

  const send = async (text) => {
    if (text) push('user', text);
    setInput(''); setBusy(true);
    try {
      const r = await post('/api/v1/onboard', { message: text || '' });
      push('assistant', r.reply);
      if (r.applied) setApplied((a) => ({ ...a, ...r.applied }));
      if (r.done) setDone(true);
    } catch (e) {
      push('assistant', "I couldn't reach the server just then. You can also set everything up in Settings.");
    } finally { setBusy(false); }
  };

  // Import their existing X posts BEFORE the agent's opening turn. The agent's
  // state block reports whether a voice profile exists, so seeding it first means
  // it never asks them to paste writing samples for an account we can already
  // read. Best-effort: any failure falls straight through to the normal flow.
  useEffect(() => {
    let cancelled = false;
    (async () => {
      if (preview) { send(''); return; } // reviewing the flow on an existing account
      try {
        const st = await get('/api/v1/import');
        if (!st.imported) {
          setImporting(st.max || 0);
          const r = await post('/api/v1/import', {});
          if (!cancelled && r.ok && r.imported) {
            push('assistant', `I read your last ${r.imported} posts on X to learn how you write`
              + (r.topics && r.topics.length ? `, and it looks like you're known for ${r.topics.slice(0, 3).join(', ')}.` : '.'));
          }
        }
      } catch (_) { /* fall through to the normal onboarding */ }
      if (!cancelled) { setImporting(0); send(''); }
    })();
    return () => { cancelled = true; };
  }, []);
  useEffect(() => { if (endRef.current) endRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, [msgs, busy]);

  const finish = async () => { try { await post('/api/v1/onboard', { action: 'finish' }); } catch (_) {} await onDone(); };

  return (
    <div className="section">
      {preview && (
        <div className="notice trial" style={{ marginBottom: 14 }}>
          <b>Preview mode.</b> You're running the first-run guide on an account that already has data.
          It's the real agent, so if you let it set topics or learn your voice it <b>will overwrite</b> what's
          in Settings today. Read the conversation, then leave without answering to change nothing.
        </div>
      )}
      <div className="chat">
        <div className="chat-log">
          {msgs.map((m, i) => (
            <div className={'msg ' + m.role} key={i}>{m.content}</div>
          ))}
          {/* The import is the slowest step in the whole first run — say what is
              happening rather than showing a bare typing dot for ~10 seconds. */}
          {importing > 0 && <div className="msg assistant">Reading your last {importing} posts on X to learn your voice…</div>}
          {busy && <div className="msg assistant typing"><span /><span /><span /></div>}
          <div ref={endRef} />
        </div>

        {(applied.topics || applied.voice || applied.drafts) && (
          <div className="chat-applied">
            {applied.topics && <span className="tag">✓ {applied.topics.length} topics set</span>}
            {applied.voice && <span className="tag">✓ voice learned</span>}
            {applied.drafts && <span className="tag">✓ {applied.drafts} drafts written</span>}
          </div>
        )}

        {done ? (
          <div className="row" style={{ padding: '12px 14px' }}>
            <button className="btn" onClick={finish}>Go to my queue →</button>
          </div>
        ) : (
          <form className="chat-input" onSubmit={(e) => { e.preventDefault(); if (input.trim() && !busy) send(input.trim()); }}>
            <input className="inp" style={{ marginBottom: 0 }} value={input} onChange={(e) => setInput(e.target.value)}
              placeholder={busy ? 'One moment…' : 'Tell it what you\'re building…'} disabled={busy} autoFocus />
            <button className="btn" type="submit" disabled={busy || !input.trim()}>Send</button>
          </form>
        )}
      </div>
      <p className="empty" style={{ textAlign: 'center', marginTop: 10 }}>
        <a onClick={finish} style={{ cursor: 'pointer' }}>Skip — take me to the app</a>
      </p>
    </div>
  );
}

// Live "what's wired" panel. Reads the real connection state — it never claims
// something works when it doesn't.
function SetupPanel({ onGoto }) {
  const [d, setD] = useState(null);
  useEffect(() => { get('/api/v1/setup').then(setD).catch(() => setD(null)); }, []);
  if (!d) return null;

  return (
    <div className="section">
      <h2>Setup {d.ready ? <span className="saved">✓ ready</span> : <span className="waiting">{d.remaining} left</span>}</h2>
      <div className="post">
        {d.items.map((it) => (
          <div className="setup-row" key={it.key}>
            <span className={'dot ' + (it.ok ? 'on' : 'off')} />
            <div className="setup-body">
              <div className="setup-head">
                <b>{it.label}</b>
                {!it.required && <span className="pill">optional</span>}
                {it.ok ? <span className="saved">connected</span> : it.required ? <span className="waiting">needed</span> : null}
              </div>
              <div className="empty">{it.detail}</div>
              <div className="empty" style={{ marginTop: 2 }}><i>Unlocks: {it.unlocks}</i></div>
              {it.fix && (it.fix.href
                ? <a href={safeHref(it.fix.href)} className="setup-fix">{it.fix.label} →</a>
                : <a className="setup-fix" style={{ cursor: 'pointer' }} onClick={() => onGoto && onGoto(it.fix.view)}>{it.fix.label} →</a>)}
            </div>
          </div>
        ))}
      </div>

      <h2 style={{ marginTop: 22 }}>Where you can use it</h2>
      <div className="post">
        {d.surfaces.map((sf) => (
          <div className="setup-row" key={sf.key}>
            <span className={'dot ' + (sf.ok === false ? 'off' : 'on')} />
            <div className="setup-body">
              <div className="setup-head"><b>{sf.label}</b></div>
              <div className="empty">{sf.detail}</div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// (legacy step wizard — kept as the non-agent fallback path)
function Onboarding({ config, onDone }) {
  const [step, setStep] = useState(0);
  const [topics, setTopics] = useState((config && config.topics) || []);
  const [newTopic, setNewTopic] = useState('');
  const [samples, setSamples] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  const saveTopics = async () => {
    setBusy(true); setErr(null);
    try { await post('/api/v1/config', { topics }); setStep(1); }
    catch (e) { setErr(String(e.message || e)); } finally { setBusy(false); }
  };
  const learn = async () => {
    setBusy(true); setErr(null);
    try {
      const r = await post('/api/v1/profile', { action: 'learn', samples });
      if (!r.ok) { setErr(r.note || r.error); return; }
      setStep(2);
    } catch (e) { setErr(String(e.message || e)); } finally { setBusy(false); }
  };
  const firstDrafts = async () => {
    setBusy(true); setErr(null);
    try {
      const r = await post('/api/v1/posts', { action: 'draft', count: 2 });
      if (!r.ok) { setErr(r.note || r.message || r.error); return; }
      await onDone();
    } catch (e) { setErr(String(e.message || e)); } finally { setBusy(false); }
  };

  return (
    <div className="section">
      <div className="post onb">
        <div className="onb-steps">
          {['Your topics', 'Your voice', 'First drafts'].map((label, i) => (
            <span key={i} className={'onb-step' + (i === step ? ' on' : '') + (i < step ? ' done' : '')}>
              {i < step ? '✓' : i + 1} {label}
            </span>
          ))}
        </div>

        {step === 0 && (
          <div>
            <h3 className="onb-h">What do you want to be known for?</h3>
            <p className="empty">These are the themes it writes about — and what it watches for trends each week.</p>
            <div className="row" style={{ margin: '12px 0' }}>
              {topics.map((t, i) => <span className="tag" key={i}>{t}<a onClick={() => setTopics(topics.filter((_, j) => j !== i))}>✕</a></span>)}
            </div>
            <div className="row">
              <input className="inp" style={{ flex: 1, marginBottom: 0 }} value={newTopic} onChange={(e) => setNewTopic(e.target.value)}
                placeholder="e.g. building a dev-tools startup solo"
                onKeyDown={(e) => { if (e.key === 'Enter' && newTopic.trim()) { setTopics([...topics, newTopic.trim()]); setNewTopic(''); } }} />
              <button className="btn ghost" onClick={() => { if (newTopic.trim()) { setTopics([...topics, newTopic.trim()]); setNewTopic(''); } }}>Add</button>
            </div>
            <button className="btn" style={{ marginTop: 14 }} onClick={saveTopics} disabled={busy || !topics.length}>{busy ? 'Saving…' : 'Next — teach it your voice'}</button>
          </div>
        )}

        {step === 1 && (
          <div>
            <h3 className="onb-h">Paste something you wrote.</h3>
            <p className="empty">Old posts, your site copy, a note to your team — anything that sounds like you. It distils a style profile from this, then learns more from every edit you make.</p>
            <textarea className="inp" rows={6} style={{ marginTop: 12 }} value={samples} onChange={(e) => setSamples(e.target.value)}
              placeholder="Paste a few paragraphs in your own words…" />
            <div className="row">
              <button className="btn" onClick={learn} disabled={busy || samples.trim().length < 40}>{busy ? 'Learning your voice…' : 'Learn my voice'}</button>
              <button className="btn ghost" onClick={() => setStep(2)} disabled={busy}>Skip for now</button>
            </div>
          </div>
        )}

        {step === 2 && (
          <div>
            <h3 className="onb-h">Let's write your first posts.</h3>
            <p className="empty">Two drafts, in your voice, on your topics. Nothing publishes — you approve, edit or reject each one.</p>
            <button className="btn" style={{ marginTop: 14 }} onClick={firstDrafts} disabled={busy}>{busy ? 'Writing…' : 'Write my first 2 posts'}</button>
          </div>
        )}

        {err && <p className="err" style={{ marginTop: 12 }}>{err}</p>}
      </div>
    </div>
  );
}

function Dashboard() {
  const [data, setData] = useState(null);
  const [err, setErr] = useState(null);
  const [drafting, setDrafting] = useState(false);

  const load = () => Promise.all([get('/api/v1/stats'), get('/api/v1/queue'), get('/api/v1/config'), get('/api/v1/profile'), get('/api/v1/plan'), get('/api/v1/onboard')])
    .then(([stats, queue, config, profile, plan, onb]) => setData({ stats, posts: queue.posts, config: config.config, profile: profile.profile, plan, onboardDone: onb.done }))
    .catch((e) => setErr(e.message));
  useEffect(() => { load(); }, []);

  const draftNow = async () => {
    setDrafting(true);
    try { await post('/api/v1/posts', { action: 'draft', count: 1 }); await load(); }
    catch (e) { setErr(String(e.message || e)); }
    finally { setDrafting(false); }
  };

  if (err) return <p className="empty">Couldn't reach the API ({err}). Run <code>vercel dev</code>, or pass <code>?api=&lt;deploy-url&gt;</code>.</p>;
  if (!data) return <p className="empty">Loading…</p>;

  const { stats, posts, config, profile, plan } = data;
  const pending = posts.filter((p) => p.status === 'draft' || p.status === 'approved');
  const published = posts.filter((p) => p.status === 'published');
  // A fresh account: nothing written yet and no voice learned. Walk them in
  // rather than dropping them on an empty dashboard whose first click fails.
  const trial = plan && plan.trial;

  // First run: hand them to the agent instead of an empty dashboard whose first
  // click would fail. `onboardDone` is set once they finish or skip.
  // ?onboard=1 forces it on an existing account so the flow can be reviewed.
  const forceOnboard = new URLSearchParams(location.search).get('onboard') === '1';
  if (forceOnboard || (!data.onboardDone && !posts.length && !profile)) {
    return <OnboardAgent onDone={load} preview={forceOnboard && (posts.length > 0 || !!profile)} />;
  }

  return (
    <div>
      <p className="sub">Mode <b>{stats.mode}</b> · cadence {config.cadence.perDay}/day ({config.cadence.tz}) · drafts in your voice, you approve, it learns.</p>

      {trial && trial.active && (
        <div className="notice trial">
          <b>{trial.remaining} of {trial.total} free posts left</b> — running on our AI while you try it.
          When they're gone, add your own Anthropic key (free, unlimited) or pick a plan in <b>Settings</b>.
        </div>
      )}
      {trial && !trial.active && plan && !plan.ownKey && plan.plan === 'byok' && (
        <div className="notice trial">
          <b>Your free posts are used up.</b> Add your Anthropic key in <b>Settings</b> (free, unlimited) — or pick a plan and we'll run the AI for you.
        </div>
      )}

      <div className="tiles">
        <Tile n={pct(stats.approvalRate)} l={`approval rate (last ${stats.sampled})`} />
        <Tile n={pct(stats.avgEditDistance)} l="avg edit size" />
        <Tile n={stats.published} l="published" />
        <Tile n={stats.pending} l="pending" />
      </div>

      <div className="section">
        <h2>Graduation</h2>
        <div className="post">
          <span className={stats.graduationReady ? 'ready' : 'waiting'}>
            {stats.graduationReady ? '✅ Ready for auto mode' : '⏳ Still learning'}
          </span>
          <div className="meta"><span>{stats.recommendation}</span></div>
        </div>
      </div>

      <div className="section">
        <div className="row" style={{ justifyContent: 'space-between', alignItems: 'baseline' }}>
          <h2 style={{ margin: 0 }}>Queue ({pending.length})</h2>
          <button className="btn sm" onClick={draftNow} disabled={drafting}>{drafting ? 'Writing…' : '+ Draft a post'}</button>
        </div>
        <div style={{ marginTop: 10 }}>
          {pending.length ? pending.map((p) => <Post key={p.id} p={p} onChange={load} />)
            : <p className="empty">Nothing pending. Drafts arrive on your cadence — or write one now.</p>}
        </div>
      </div>

      <Inspiration />

      <div className="section">
        <h2>Topics</h2>
        <div className="post"><div className="meta">{config.topics.map((t, i) => <span key={i} className="pill">{t}</span>)}</div></div>
      </div>

      {published.length > 0 && (
        <div className="section">
          <h2>Published ({published.length})</h2>
          {published.slice(0, 10).map((p) => <Post key={p.id} p={p} />)}
        </div>
      )}
    </div>
  );
}

// Magnitude bars — one hue (single measure across categories). `stacked` puts the
// (possibly long) label on its own line above a full-width bar so it stays fully
// readable; the default inline layout suits short labels (e.g. the funnel).
function Bars({ rows, kind, stacked }) {
  const max = Math.max(1, ...rows.map((r) => r.count));
  if (stacked) {
    return (
      <div className="bars stacked">
        {rows.map((r) => (
          <div className="bar-stack" key={r.key || r.label}>
            <div className="bar-head"><span className="lbl2">{r.label}</span><span className="val">{r.count}</span></div>
            <div className="track"><span className="fill" style={{ width: (r.count / max * 100) + '%' }} /></div>
          </div>
        ))}
      </div>
    );
  }
  return (
    <div className={'bars' + (kind ? ' ' + kind : '')}>
      {rows.map((r) => (
        <div className="bar-row" key={r.key || r.label} title={`${r.label}: ${r.count}`}>
          <span className="lbl">{r.label}</span>
          <span className="track"><span className="fill" style={{ width: (r.count / max * 100) + '%' }} /></span>
          <span className="val">{r.count}</span>
        </div>
      ))}
    </div>
  );
}

// Topics arrive as long sentences ("why most sales tooling fails revenue leaders"),
// which are unreadable as chart labels. Derive a short category handle from the
// significant words; the full topic + its posts live behind a click.
const CAT_STOP = new Set('the a an and or of to in on for with that your you my me i is are was not just it its as at by from about why how what most more less very really actually do does did be been being this these those they them we our us so if then than into out up down can will get got make made'.split(' '));
function categoryLabel(topic) {
  const words = String(topic || '').split(/[^A-Za-z0-9'’/&+-]+/).filter(Boolean);
  const sig = words.filter((w) => !CAT_STOP.has(w.toLowerCase()));
  const pool = sig.length ? sig : words;
  let pick = pool.slice(0, 2);
  if (pick.join(' ').length < 9 && pool[2]) pick = pool.slice(0, 3);
  let label = pick.join(' ');
  if (label.length > 28) label = label.slice(0, 27).trimEnd() + '…';
  return label ? label.charAt(0).toUpperCase() + label.slice(1) : 'Untitled';
}

// Clickable category rows — short handle + bar; click to reveal the full topic
// and the posts written under it.
function CategoryBars({ rows }) {
  const [open, setOpen] = useState(null);
  const max = Math.max(1, ...rows.map((r) => r.count));
  return (
    <div className="cats">
      {rows.map((r) => {
        const isOpen = open === r.key;
        return (
          <div className={'cat-row' + (isOpen ? ' open' : '')} key={r.key}>
            <button className="cat-head" onClick={() => setOpen(isOpen ? null : r.key)} aria-expanded={isOpen}>
              <span className="cat-chev" aria-hidden="true">{isOpen ? '▾' : '▸'}</span>
              <span className="cat-name">{categoryLabel(r.label)}</span>
              <span className="cat-track"><span className="cat-fill" style={{ width: (r.count / max * 100) + '%' }} /></span>
              <span className="cat-count">{r.count}</span>
            </button>
            {isOpen && (
              <div className="cat-detail">
                <p className="cat-full">{r.label}</p>
                {(r.sample || []).map((p) => (
                  <div className="cat-post" key={p.id}>
                    <div className="cat-post-txt">{p.text}</div>
                    <div className="meta">
                      <span className={'badge b-' + p.status}>{p.status}</span>
                      <span className="pill">{p.type}</span>
                      {p.url && <a href={safeHref(p.url)} target="_blank" rel="noreferrer">view on X ↗</a>}
                    </div>
                  </div>
                ))}
                {r.count > (r.sample || []).length && <p className="empty" style={{ margin: '6px 0 0' }}>+{r.count - (r.sample || []).length} more</p>}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

// Single-series sparkline. values: number[]. Title names it (no legend).
function Sparkline({ values, color }) {
  if (!values || values.length < 2) return <p className="empty">Not enough data yet.</p>;
  const W = 300, H = 44, pad = 3;
  const min = Math.min(...values), max = Math.max(...values);
  const span = max - min || 1;
  const pts = values.map((v, i) => {
    const x = pad + (i / (values.length - 1)) * (W - 2 * pad);
    const y = H - pad - ((v - min) / span) * (H - 2 * pad);
    return `${x.toFixed(1)},${y.toFixed(1)}`;
  }).join(' ');
  return (
    <svg className="spark" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none">
      <polyline points={pts} fill="none" stroke={color || 'var(--accent)'} strokeWidth="2" vectorEffect="non-scaling-stroke" />
    </svg>
  );
}

function EngagementBlock({ e, onRefresh, refreshing }) {
  if (!e || !e.connected) {
    return (
      <div className="section">
        <h2>X engagement</h2>
        <div className="notice">Connect your X account in <b>Settings → Connections</b> to pull likes, reposts, impressions and follower growth for your published posts.</div>
      </div>
    );
  }
  const followerSeries = (e.followerSeries || []).map((f) => f.count);
  return (
    <div className="section">
      <h2>X engagement {e.fetchedAt && <span className="sub" style={{ textTransform: 'none', letterSpacing: 0 }}>· updated {new Date(e.fetchedAt).toLocaleString()}</span>}</h2>
      <div className="tiles">
        <Tile n={e.totals.like} l="likes" />
        <Tile n={e.totals.retweet} l="reposts" />
        <Tile n={e.totals.reply} l="replies" />
        {e.userContext && <Tile n={e.totals.impression} l="impressions" />}
        {e.followers != null && <Tile n={e.followers} l="followers" />}
      </div>
      <div className="grid2">
        {e.top && e.top.length > 0 && (
          <div className="chart">
            <h3>Top posts</h3>
            {e.top.map((p) => (
              <div className="bar-row" key={p.id} title={p.text}>
                <span className="lbl" style={{ width: 'auto', flex: 1 }}>{p.text.slice(0, 60)}{p.text.length > 60 ? '…' : ''}</span>
                <span className="val" style={{ width: 'auto' }}>♥ {p.metrics.like} · ↻ {p.metrics.retweet}</span>
              </div>
            ))}
          </div>
        )}
        {followerSeries.length > 1 && (
          <div className="chart">
            <h3>Follower growth</h3>
            <Sparkline values={followerSeries} />
          </div>
        )}
      </div>
      <div className="row" style={{ marginTop: 10 }}>
        <button className="btn ghost" onClick={onRefresh} disabled={refreshing}>{refreshing ? 'Refreshing…' : 'Refresh engagement'}</button>
      </div>
    </div>
  );
}

function Analytics() {
  const [d, setD] = useState(null);
  const [err, setErr] = useState(null);
  const [refreshing, setRefreshing] = useState(false);

  const load = (refresh) => {
    if (refresh) setRefreshing(true);
    return get('/api/v1/analytics' + (refresh ? '?refresh=1' : ''))
      .then((res) => setD(res))
      .catch((e) => setErr(e.message))
      .finally(() => setRefreshing(false));
  };
  useEffect(() => { load(false); }, []);

  if (err) return <p className="empty">Couldn't load analytics ({err}).</p>;
  if (!d) return <p className="empty">Loading…</p>;

  const perf = d.performance;
  const editValues = perf.editTrend.map((x) => x.editDistance);

  return (
    <div>
      <div className="tiles">
        <Tile n={perf.counts.published} l="published" />
        <Tile n={pct(perf.graduation.approvalRate)} l={`approval rate (last ${perf.graduation.sampled})`} />
        <Tile n={pct(perf.graduation.avgEditDistance)} l="avg edit size" />
        <Tile n={pct(perf.cadence.adherence)} l="cadence (14d)" />
      </div>

      <div className="section">
        <h2>Drafting → publishing funnel</h2>
        <div className="chart"><Bars rows={perf.funnel} kind="funnel" /></div>
      </div>

      <div className="grid2">
        <div className="chart">
          <h3>Edit size over time (voice converging)</h3>
          <Sparkline values={editValues} color="var(--warn)" />
          <p className="empty" style={{ marginTop: 6 }}>Lower is better — smaller edits mean the drafts already sound like you.</p>
        </div>
        <div className="chart">
          <h3>Published / day (last 14)</h3>
          <Sparkline values={perf.cadence.series.map((s) => s.count)} color="var(--ok)" />
          <p className="empty" style={{ marginTop: 6 }}>{perf.cadence.actual} of {perf.cadence.target} target posts.</p>
        </div>
      </div>

      <div className="grid2" style={{ marginTop: 12 }}>
        <div className="chart"><h3>Topics</h3>{perf.topics.length ? <CategoryBars rows={perf.topics} /> : <p className="empty">No data yet.</p>}</div>
        <div className="chart"><h3>Post types</h3>{perf.types.length ? <Bars rows={perf.types} stacked /> : <p className="empty">No data yet.</p>}</div>
      </div>

      <div className="section" style={{ marginTop: 24 }}>
        <h2>Graduation</h2>
        <div className="post">
          <span className={perf.graduation.ready ? 'ready' : 'waiting'}>{perf.graduation.ready ? '✅ Ready for auto mode' : '⏳ Still learning'}</span>
          <div className="meta"><span>{perf.graduation.recommendation}</span></div>
        </div>
      </div>

      <EngagementBlock e={d.engagement} onRefresh={() => load(true)} refreshing={refreshing} />
    </div>
  );
}

function TrendsPanel() {
  const [d, setD] = useState(null);
  const [busy, setBusy] = useState(false);
  const load = () => get('/api/v1/trends').then(setD).catch(() => setD({ topics: [] }));
  useEffect(() => { load(); }, []);
  const refresh = async () => { setBusy(true); try { const r = await post('/api/v1/trends', { refresh: 1 }); setD(r); } finally { setBusy(false); } };

  return (
    <div className="section" style={{ marginTop: 24 }}>
      <h2>Weekly trends {d && d.weekOf && <span className="sub" style={{ textTransform: 'none', letterSpacing: 0 }}>· week of {d.weekOf}</span>}</h2>
      {(!d || !d.topics || !d.topics.length) ? (
        <div className="notice">No trends yet — they build weekly from your topics. <a onClick={refresh} style={{ cursor: 'pointer' }}>{busy ? 'Building…' : 'Build now'}</a></div>
      ) : (
        <div>
          <div className="grid2">
            {d.topics.map((t, i) => (
              <div className="chart" key={i}>
                <h3>{t.topic}</h3>
                {(t.bullets || []).length ? <ul style={{ margin: 0, paddingLeft: 18 }}>{t.bullets.map((b, j) => <li key={j} style={{ marginBottom: 4 }}>{b}</li>)}</ul> : <p className="empty">No signal this week.</p>}
                {(t.items || []).length > 0 && <div className="meta" style={{ marginTop: 8 }}>{t.items.slice(0, 3).map((it, j) => <a key={j} href={safeHref(it.url)} target="_blank" rel="noreferrer" className="pill">{it.source}</a>)}</div>}
              </div>
            ))}
          </div>
          <div className="row" style={{ marginTop: 10 }}>
            <button className="btn ghost" onClick={refresh} disabled={busy}>{busy ? 'Refreshing…' : 'Refresh trends'}</button>
            <span className="empty">{d.suggested || 0} suggested post(s) added to your queue.</span>
          </div>
        </div>
      )}
    </div>
  );
}

function SavedFlag({ at }) {
  return at ? <span className="saved">✓ saved</span> : null;
}

function Settings() {
  const [config, setConfig] = useState(null);
  const [conns, setConns] = useState(null);
  const [profile, setProfile] = useState(null);
  const [err, setErr] = useState(null);
  // per-section transient "saved" flags
  const [saved, setSaved] = useState({});
  const flag = (k) => setSaved((s) => ({ ...s, [k]: Date.now() }));

  const loadAll = () => Promise.all([
    get('/api/v1/config'), get('/api/v1/connections'), get('/api/v1/profile'),
  ]).then(([c, cn, pr]) => { setConfig(c.config); setConns(cn); setProfile(pr.profile); })
    .catch((e) => setErr(e.message));
  useEffect(() => { loadAll(); }, []);

  const patchConfig = async (patch, key) => {
    const r = await post('/api/v1/config', patch);
    setConfig(r.config); if (key) flag(key);
  };

  if (err) return <p className="empty">Couldn't load settings ({err}).</p>;
  if (!config || !conns) return <p className="empty">Loading…</p>;

  return (
    <div>
      <SetupPanel />
      <PlanSettings conns={conns} onReload={loadAll} />
      <PostingSettings config={config} onPatch={patchConfig} saved={saved} />
      <VoiceSettings profile={profile} conns={conns} onReload={loadAll} />
      <ConnectionsSettings conns={conns} onReload={loadAll} />
      <McpSettings />
    </div>
  );
}

// Claude Code (MCP) — a per-account token pointed at the hosted /api/mcp endpoint.
// The plaintext token is returned exactly once, at creation, so the UI holds it in
// state for copying and never asks the server for it again.
function McpSettings() {
  const [d, setD] = useState(null);
  const [fresh, setFresh] = useState(null); // the just-minted token, shown once
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const load = () => get('/api/v1/mcp').then(setD).catch(() => setD(null));
  useEffect(() => { load(); }, []);

  const create = async () => {
    setBusy(true); setErr(null);
    try {
      const r = await post('/api/v1/mcp', { action: 'create' });
      if (r.ok) { setFresh(r.token); await load(); } else setErr(r.message || r.error);
    } catch (e) { setErr(explain(e)); } finally { setBusy(false); }
  };
  const revoke = async () => {
    setBusy(true); setErr(null);
    try { await post('/api/v1/mcp', { action: 'revoke' }); setFresh(null); await load(); }
    catch (e) { setErr(explain(e)); } finally { setBusy(false); }
  };

  if (!d) return null;
  const cmd = `claude mcp add --transport http x-autopilot ${d.url} \\\n  --header "Authorization: Bearer ${fresh || 'YOUR_TOKEN'}"`;

  return (
    <div className="section">
      <h2>Claude Code</h2>
      <div className="post">
        {!d.enabled ? (
          <p className="empty" style={{ margin: 0 }}>
            Driving X Autopilot from Claude Code is part of <b>Pro</b>. Upgrade and you can draft,
            triage and publish from your terminal — scoped to your account.
          </p>
        ) : (
          <>
            <p className="empty" style={{ margin: '0 0 12px' }}>
              Connect Claude Code to your own queue. The token below authenticates as <b>your account only</b>.
            </p>

            {fresh && (
              <div className="notice trial" style={{ marginBottom: 12 }}>
                <b>Copy this now — it won't be shown again.</b>
                <div style={{ marginTop: 8 }}><code style={{ wordBreak: 'break-all' }}>{fresh}</code></div>
              </div>
            )}

            {d.token.exists ? (
              <div className="row" style={{ justifyContent: 'space-between', marginBottom: 12 }}>
                <span className="empty">
                  Token ending <code>…{d.token.hint}</code> · created {new Date(d.token.createdAt).toLocaleDateString()}
                  {d.token.lastUsedAt ? ` · last used ${new Date(d.token.lastUsedAt).toLocaleDateString()}` : ' · never used'}
                </span>
                <div className="row">
                  <button className="btn ghost sm" onClick={create} disabled={busy}>Regenerate</button>
                  <button className="btn sm danger" onClick={revoke} disabled={busy}>Revoke</button>
                </div>
              </div>
            ) : (
              <button className="btn" onClick={create} disabled={busy}>{busy ? 'Creating…' : 'Create a token'}</button>
            )}

            {(d.token.exists || fresh) && (
              <>
                <div className="field" style={{ marginTop: 6 }}><label>Add it to Claude Code</label></div>
                <pre className="cmd">{cmd}</pre>
              </>
            )}
          </>
        )}
        {err && <p className="err" style={{ margin: '10px 0 0' }}>{err}</p>}
      </div>
    </div>
  );
}

function PlanSettings({ conns, onReload }) {
  const [d, setD] = useState(null);
  const [byok, setByok] = useState('');
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState(null);
  const load = () => get('/api/v1/plan').then(setD).catch(() => setD(null));
  useEffect(() => { load(); }, []);

  const upgrade = async (planId) => {
    setBusy(true); setMsg(null);
    try { const r = await post('/api/billing/checkout', { plan: planId }); if (r.url) location.href = r.url; else setMsg(r.error || 'could not start checkout'); }
    catch (e) { setMsg('Error: ' + e.message); } finally { setBusy(false); }
  };
  const manage = async () => {
    setBusy(true);
    try { const r = await post('/api/billing/portal', {}); if (r.url) location.href = r.url; else setMsg(r.error || 'no subscription'); }
    catch (e) { setMsg('Error: ' + e.message); } finally { setBusy(false); }
  };
  const saveByok = async () => {
    if (!byok.trim()) return;
    setBusy(true); setMsg(null);
    try { const r = await post('/api/v1/connections', { action: 'saveByokKey', key: byok.trim() }); if (r.ok) { setByok(''); setMsg('Your Anthropic key is saved.'); await onReload(); await load(); } else setMsg(r.note || r.error); }
    catch (e) { setMsg('Error: ' + e.message); } finally { setBusy(false); }
  };

  if (!d) return null;
  const current = d.plan;
  const usage = d.usage || {};
  const byokKey = conns && conns.connections && conns.connections.byokKey;

  return (
    <div className="section">
      <h2>Plan &amp; usage</h2>
      <div className="post">
        <div className="row" style={{ justifyContent: 'space-between' }}>
          <span>Current plan: <b>{current}</b></span>
          {usage.cap != null ? <span className="empty">{usage.used}/{usage.cap} posts this week</span> : <span className="empty">unlimited (you bring the key)</span>}
        </div>
        {usage.cap != null && (
          <div className="bar-row" style={{ marginTop: 10 }}>
            <span className="track"><span className="fill" style={{ width: Math.min(100, (usage.used / usage.cap) * 100) + '%' }} /></span>
          </div>
        )}
      </div>

      <div className="grid2">
        {d.catalog.map((p) => (
          <div className={'chart' + (p.premium ? ' premium' : '')} key={p.id}>
            <h3>{p.label} {p.premium && <span className="tag" style={{ padding: '1px 8px' }}>★ best</span>}</h3>
            <div style={{ fontSize: 20, fontWeight: 650, margin: '2px 0 2px' }}>{p.priceEur > 0 ? `€${p.priceEur.toFixed(2)}` : 'Free'}<span className="empty" style={{ fontSize: 12, fontWeight: 400 }}>{p.priceEur > 0 ? '/mo' : ''}</span></div>
            {p.modelLabel && <div className="empty" style={{ marginBottom: 8 }}>{p.modelLabel}{p.weeklyCap != null ? ` · ${p.weeklyCap}/wk` : ' · unlimited'}</div>}
            {(p.features || []).length > 0 && <ul style={{ margin: '0 0 12px', paddingLeft: 18, fontSize: 13 }}>{p.features.map((f, i) => <li key={i} style={{ marginBottom: 3 }}>{f}</li>)}</ul>}
            {p.id === current ? (
              <span className="tag">current plan</span>
            ) : p.managed ? (
              <button className="btn" onClick={() => upgrade(p.id)} disabled={busy || !d.billingEnabled}>{d.billingEnabled ? `Upgrade to ${p.label}` : 'Billing not configured'}</button>
            ) : (
              <span className="empty">Add your Anthropic key below.</span>
            )}
          </div>
        ))}
      </div>

      <div className="post" style={{ marginTop: 12 }}>
        <div className="field"><label>Bring your own Anthropic key (BYOK — unlimited, your usage)</label>
          <p className="empty" style={{ margin: '0 0 8px' }}>Stored encrypted. When set and you're on the BYOK plan, drafts run on your key with no weekly cap.</p>
          {conns && conns.canSaveToken ? (
            <div className="row">
              <input className="inp" style={{ flex: 1, marginBottom: 0 }} type="password" value={byok} onChange={(e) => setByok(e.target.value)} placeholder={byokKey ? '•••••• (a key is saved)' : 'sk-ant-…'} />
              <button className="btn" onClick={saveByok} disabled={busy || !byok.trim()}>Save key</button>
            </div>
          ) : <div className="notice">Set <code>SECRET_ENCRYPTION_KEY</code> on the server to store a key.</div>}
        </div>
        {current !== 'byok' && <button className="btn ghost" onClick={manage} disabled={busy}>Manage / cancel subscription</button>}
        {msg && <p className="empty" style={{ marginTop: 8 }}>{msg}</p>}
      </div>
    </div>
  );
}

function PostingSettings({ config, onPatch, saved }) {
  const [perDay, setPerDay] = useState(config.cadence.perDay);
  const [tz, setTz] = useState(config.cadence.tz);
  const [slots, setSlots] = useState((config.cadence.slots || []).join(', '));
  const [topics, setTopics] = useState(config.topics || []);
  const [newTopic, setNewTopic] = useState('');
  const [grad, setGrad] = useState(config.graduation);

  const saveCadence = () => onPatch({ cadence: { perDay: Number(perDay) || 1, tz, slots: slots.split(',').map((s) => s.trim()).filter(Boolean) } }, 'cadence');
  const saveTopics = (next) => { setTopics(next); onPatch({ topics: next }, 'topics'); };
  const addTopic = () => { if (!newTopic.trim()) return; saveTopics([...topics, newTopic.trim()]); setNewTopic(''); };
  const saveGrad = () => onPatch({ graduation: { window: Number(grad.window), maxAvgEditDistance: Number(grad.maxAvgEditDistance), minApprovalRate: Number(grad.minApprovalRate) } }, 'grad');

  return (
    <div className="section">
      <h2>Posting</h2>

      <div className="post">
        <div className="field">
          <label>Mode <SavedFlag at={saved.mode} /></label>
          <div className="seg">
            <button className={config.mode === 'loop' ? 'on' : ''} onClick={() => onPatch({ mode: 'loop' }, 'mode')}>Loop (you approve)</button>
            <button className={config.mode === 'auto' ? 'on' : ''} onClick={() => onPatch({ mode: 'auto' }, 'mode')}>Auto (X API)</button>
          </div>
        </div>
      </div>

      <div className="post">
        <div className="field"><label>Cadence <SavedFlag at={saved.cadence} /></label>
          <div className="row">
            <input className="inp" style={{ width: 70 }} type="number" min="1" max="5" value={perDay} onChange={(e) => setPerDay(e.target.value)} />
            <span className="empty">posts/day</span>
          </div>
        </div>
        <div className="field"><label>Time slots (comma-separated, e.g. 13:00, 18:00)</label>
          <input className="inp" value={slots} onChange={(e) => setSlots(e.target.value)} />
        </div>
        <div className="field"><label>Timezone</label>
          <input className="inp" value={tz} onChange={(e) => setTz(e.target.value)} />
        </div>
        <button className="btn" onClick={saveCadence}>Save cadence</button>
      </div>

      <div className="post">
        <div className="field"><label>Topics <SavedFlag at={saved.topics} /></label>
          <div className="row" style={{ marginBottom: 10 }}>
            {topics.map((t, i) => (
              <span className="tag" key={i}>{t}<a onClick={() => saveTopics(topics.filter((_, j) => j !== i))}>✕</a></span>
            ))}
            {!topics.length && <span className="empty">No topics yet.</span>}
          </div>
          <div className="row">
            <input className="inp" style={{ flex: 1, marginBottom: 0 }} value={newTopic} onChange={(e) => setNewTopic(e.target.value)} placeholder="Add a topic…" onKeyDown={(e) => { if (e.key === 'Enter') addTopic(); }} />
            <button className="btn" onClick={addTopic} disabled={!newTopic.trim()}>Add</button>
          </div>
        </div>
      </div>

      <div className="post">
        <div className="field"><label>Graduation thresholds (when to offer auto mode) <SavedFlag at={saved.grad} /></label>
          <div className="row">
            <div><span className="empty">window</span><br /><input className="inp" style={{ width: 80 }} type="number" min="1" value={grad.window} onChange={(e) => setGrad({ ...grad, window: e.target.value })} /></div>
            <div><span className="empty">max avg edit</span><br /><input className="inp" style={{ width: 90 }} type="number" step="0.01" min="0" max="1" value={grad.maxAvgEditDistance} onChange={(e) => setGrad({ ...grad, maxAvgEditDistance: e.target.value })} /></div>
            <div><span className="empty">min approval</span><br /><input className="inp" style={{ width: 90 }} type="number" step="0.05" min="0" max="1" value={grad.minApprovalRate} onChange={(e) => setGrad({ ...grad, minApprovalRate: e.target.value })} /></div>
          </div>
          <button className="btn" style={{ marginTop: 10 }} onClick={saveGrad}>Save thresholds</button>
        </div>
      </div>
    </div>
  );
}

function VoiceSettings({ profile, conns, onReload }) {
  const [samples, setSamples] = useState('');
  const [busy, setBusy] = useState(null);
  const [msg, setMsg] = useState(null);
  const hasKey = conns.connections.anthropic;

  const run = async (action, extra) => {
    setBusy(action); setMsg(null);
    try {
      const r = await post('/api/v1/profile', { action, ...extra });
      if (r.skipped) setMsg(r.skipped);
      else { setMsg(action === 'learn' ? 'Learned your voice from those samples.' : 'Profile refreshed from recent edits.'); if (action === 'learn') setSamples(''); }
      await onReload();
    } catch (e) { setMsg('Error: ' + e.message); }
    finally { setBusy(null); }
  };

  return (
    <div className="section">
      <h2>Voice</h2>
      {!hasKey && <div className="notice" style={{ marginBottom: 10 }}>Set <code>ANTHROPIC_API_KEY</code> to manage the style profile.</div>}
      <div className="post">
        {profile ? (
          <div>
            <div className="txt" style={{ fontSize: 14 }}>{profile.voice}</div>
            <div className="meta">
              {(profile.tone || []).map((t, i) => <span className="pill" key={i}>{t}</span>)}
              {profile.source && <span className="pill">source: {profile.source}</span>}
              {profile.updatedAt && <span>· updated {new Date(profile.updatedAt).toLocaleDateString()}</span>}
            </div>
          </div>
        ) : <p className="empty">No profile yet — paste samples below to learn your voice.</p>}
      </div>
      <div className="post">
        <div className="field"><label>Learn from samples — paste posts/writing that sound like you</label>
          <textarea className="inp" rows={4} value={samples} onChange={(e) => setSamples(e.target.value)} placeholder="Paste your writing, one idea per line…" disabled={!hasKey} />
        </div>
        <div className="row">
          <button className="btn" onClick={() => run('learn', { samples })} disabled={!hasKey || busy || samples.trim().length < 40}>{busy === 'learn' ? 'Learning…' : 'Learn from samples'}</button>
          <button className="btn ghost" onClick={() => run('refresh')} disabled={!hasKey || busy}>{busy === 'refresh' ? 'Refreshing…' : 'Refresh from recent edits'}</button>
        </div>
        {msg && <p className="empty" style={{ marginTop: 8 }}>{msg}</p>}
      </div>
    </div>
  );
}

const SLACK_RESULT = {
  connected: 'Slack connected.',
  denied: 'Slack install cancelled — nothing was changed.',
  expired: 'That Slack link expired. Try “Add to Slack” again.',
  login: 'Sign in first, then connect Slack.',
  error: 'Slack connection failed. Try again, or paste a webhook instead.',
};

// "connected" is uninformative once it works — say which channel.
function slackWhere(meta) {
  if (!meta) return 'Connected.';
  if (meta.via === 'manual') return 'Connected via a pasted webhook.';
  const chan = meta.channel ? (meta.channel.startsWith('#') ? meta.channel : '#' + meta.channel) : 'your channel';
  return meta.teamName ? `Posting to ${chan} in ${meta.teamName}.` : `Posting to ${chan}.`;
}

function ConnectionsSettings({ conns, onReload }) {
  const c = conns.connections;
  const [token, setToken] = useState('');
  const [slackUrl, setSlackUrl] = useState('');
  const [manual, setManual] = useState(false);
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState(null);

  // Returning from the "Add to Slack" round trip — report the outcome once.
  useEffect(() => {
    const q = new URLSearchParams(window.location.search).get('slack');
    if (!q) return;
    setMsg(SLACK_RESULT[q] || null);
    window.history.replaceState({}, '', window.location.pathname);
    if (q === 'connected') onReload();
  }, []);

  const save = async () => {
    if (!token.trim()) return;
    setBusy(true); setMsg(null);
    try { const r = await post('/api/v1/connections', { action: 'saveXToken', token: token.trim() }); if (r.ok) { setToken(''); setMsg('X read token saved.'); await onReload(); } else setMsg(r.note || r.error); }
    catch (e) { setMsg('Error: ' + e.message); } finally { setBusy(false); }
  };
  const clear = async () => { setBusy(true); try { await post('/api/v1/connections', { action: 'clearXToken' }); await onReload(); } finally { setBusy(false); } };
  const saveSlack = async () => {
    const val = slackUrl.trim();
    if (!val) return;
    setBusy(true); setMsg(null);
    try { const r = await post('/api/v1/connections', { action: 'saveSlackWebhook', url: val }); if (r.ok) { setSlackUrl(''); setManual(false); setMsg('Slack connected.'); await onReload(); } else setMsg(r.note || r.error); }
    catch (e) { setMsg('Error: ' + e.message); } finally { setBusy(false); }
  };
  const disconnectSlack = async () => {
    setBusy(true); setMsg(null);
    try { await post('/api/v1/connections', { action: 'disconnectSlack' }); setMsg('Slack disconnected.'); await onReload(); }
    catch (e) { setMsg('Error: ' + e.message); } finally { setBusy(false); }
  };

  const Status = ({ on, label, note }) => (
    <div className="row" style={{ justifyContent: 'space-between', padding: '8px 0', borderBottom: '1px solid var(--line)' }}>
      <span><span className={'dot ' + (on ? 'on' : 'off')} /> &nbsp;{label}</span>
      <span className="empty">{on ? (note || 'connected') : 'not configured'}</span>
    </div>
  );

  return (
    <div className="section">
      <h2>Connections</h2>
      <div className="post">
        <Status on={c.anthropic} label="Anthropic (drafting)" />
        <Status on={c.slack} label="Slack (drafts &amp; weekly trends)" note={slackWhere(c.slackMeta).replace(/\.$/, '')} />
        <Status on={c.x.write} label="X — publishing (auto mode)" note="OAuth token set" />
        <Status on={c.x.read} label="X — engagement analytics" note={c.x.userContext ? 'read + impressions' : 'public metrics'} />
        <Status on={c.kv} label="Storage (KV / Upstash)" note="persistent" />
      </div>

      <div className="post">
        <div className="field"><label>Connect X for engagement analytics</label>
          <p className="empty" style={{ margin: '0 0 8px' }}>Paste an X API read token (Bearer or user-context OAuth). A user-context token also unlocks impressions + follower count. Stored encrypted.</p>
          {conns.canSaveToken ? (
            <div className="row">
              <input className="inp" style={{ flex: 1, marginBottom: 0 }} type="password" value={token} onChange={(e) => setToken(e.target.value)} placeholder={c.storedXToken ? '•••••• (a token is saved)' : 'X read token'} />
              <button className="btn" onClick={save} disabled={busy || !token.trim()}>{busy ? 'Saving…' : 'Save'}</button>
              {c.storedXToken && <button className="btn ghost" onClick={clear} disabled={busy}>Remove</button>}
            </div>
          ) : (
            <div className="notice">Set <code>SECRET_ENCRYPTION_KEY</code> to save a token here, or configure <code>X_BEARER_TOKEN</code> / <code>X_OAUTH_TOKEN</code> as environment variables.</div>
          )}
        </div>
      </div>

      <div className="post">
        <div className="field"><label>Connect Slack (optional) — get drafts &amp; weekly trends in Slack</label>
          {c.slackAccount ? (
            <div className="row" style={{ justifyContent: 'space-between' }}>
              <span className="empty">{slackWhere(c.slackMeta)}</span>
              <button className="btn ghost" onClick={disconnectSlack} disabled={busy}>Disconnect</button>
            </div>
          ) : c.slackEnv ? (
            // Operator-wide Slack, not this account's install — there is nothing
            // here for the user to disconnect, so don't offer a button that lies.
            <p className="empty" style={{ margin: 0 }}>
              Delivered through the workspace Slack app configured for this deployment.
              Connect your own workspace below to send drafts somewhere else instead.
            </p>
          ) : conns.slackOAuth ? (
            <>
              <p className="empty" style={{ margin: '0 0 8px' }}>One click — Slack asks which channel to post to, and that’s it. No setup, no copying URLs.</p>
              <a className="btn" href="/api/auth/slack/start">Add to Slack</a>
              <p className="empty" style={{ marginTop: 10 }}>
                Prefer to do it by hand? <a href="#" onClick={(e) => { e.preventDefault(); setManual(!manual); }}>Paste a webhook instead</a>.
              </p>
            </>
          ) : (
            <p className="empty" style={{ margin: '0 0 8px' }}>Paste an <b>Incoming Webhook</b> URL from your Slack workspace (Slack → Apps → Incoming Webhooks → Add to a channel).</p>
          )}
          {!c.slackAccount && (manual || !conns.slackOAuth || c.slackEnv) && (
            <div className="row" style={{ marginTop: 8 }}>
              <input className="inp" style={{ flex: 1, marginBottom: 0 }} value={slackUrl} onChange={(e) => setSlackUrl(e.target.value)} placeholder="https://hooks.slack.com/services/…" />
              <button className="btn" onClick={() => saveSlack()} disabled={busy || !slackUrl.trim()}>{busy ? 'Saving…' : 'Connect'}</button>
            </div>
          )}
        </div>
        {msg && <p className="empty" style={{ marginTop: 8 }}>{msg}</p>}
      </div>
    </div>
  );
}

// ── Signed-out landing ───────────────────────────────────────────────────────
// Mono-dark one-pager for the ICP: a very early-stage founder building a brand
// and credibility. All styles are scoped under `.lp` so nothing leaks into the
// app's light theme. Tier copy mirrors api/_lib/plans.js — keep them in sync.
// Tiers differ on CAPABILITY, not volume. Same four plans, names and prices as
// before (they are what's wired to Stripe) — what changed is what each contains.
// Keep in sync with api/_lib/plans.js.
const TIERS = [
  {
    id: 'byok', name: 'Bring your own key', price: 'Free', tagline: 'Prove it sounds like you',
    blurb: 'Your own Anthropic key. Three posts a week, you approve every one.',
    lines: ['Your Anthropic key', '3 posts / week', 'You approve everything', 'Weekly trends'],
    cta: 'Start free',
  },
  {
    id: 'starter', name: 'Starter', price: '€9.90', tagline: 'It writes for you',
    blurb: 'Every post drafted, then reviewed by an editor before it reaches you.',
    lines: ['We run the AI', '7 posts / week', 'Every post editor-reviewed', 'Drafts delivered to Slack'],
    cta: 'Get Starter',
  },
  {
    id: 'pro', name: 'Pro', price: '€15.90', tagline: 'It works where you work',
    blurb: 'Run the whole thing from Slack or Claude Code, and see what actually landed.',
    lines: ['Everything in Starter', 'Full Slack agent + buttons', 'Claude Code (MCP)', 'Engagement analytics'],
    cta: 'Get Pro',
  },
  {
    id: 'premium', name: 'Premium', price: '€25.90', tagline: 'It runs without you',
    blurb: 'Once your edits are consistently small, it publishes on its own.',
    lines: ['Everything in Pro', 'Publishes unattended', '10 posts / week', 'Priority weekly trends'],
    best: true, cta: 'Get Premium',
  },
];

// Stripe Checkout needs an account, so a signed-out tier click can't go straight
// to Stripe. Park the chosen plan, send them through X sign-in, and pick it back
// up on the other side (see PENDING_PLAN in App).
const PENDING_PLAN = 'xa_pending_plan';
function choosePlan(id) {
  try { if (id !== 'byok') sessionStorage.setItem(PENDING_PLAN, id); } catch (_) {}
  window.location.href = '/api/auth/x/start';
}

const SUPPORT_FAQS = [
  { q: 'Is it really my voice?', a: 'It starts by distilling a style profile from writing you already have — your site, your posts, anything you paste in. Then every edit you make is measured and folded back in, so the drafts drift toward how you actually write instead of away from it.' },
  { q: 'Will it post without me?', a: 'Only if you tell it to. You start in loop mode: nothing goes out until you approve it. Once your edits have been consistently small, it offers to switch to auto — and you can switch back any time.' },
  { q: 'Do I need an API key?', a: 'On the free plan, yes — you bring your own Anthropic key and there is no posting limit. On the paid plans we run the AI for you and you bring nothing.' },
  { q: 'What if a draft is wrong?', a: 'Edit it, ask the AI to revise it with a one-line instruction, or reject it. All three are signals: rejects and edits both teach the profile what you do not sound like.' },
  { q: 'Can I cancel?', a: 'Yes — self-serve, any time, from the billing portal. You drop back to the free bring-your-own-key plan and keep your voice profile and history.' },
];

const PRIVACY_SECTIONS = [
  {
    h: 'What we collect',
    p: 'We collect the account details needed to run X Autopilot: your X profile connection, settings, topics, drafts, approvals, edits, rejects, usage data, billing status, and optional Slack connection details. If you use bring-your-own-key, your Anthropic key is stored encrypted.'
  },
  {
    h: 'How we use it',
    p: 'We use your data to draft in your voice, publish only according to your settings, deliver approvals and trends, manage billing, protect the service, and answer support requests.'
  },
  {
    h: 'Who processes it',
    p: 'X Autopilot uses service providers for hosting, storage, AI generation, X authentication and publishing, Slack delivery, and Stripe billing. We do not sell your personal data.'
  },
  {
    h: 'Your controls',
    p: 'You can disconnect X or Slack, remove your Anthropic key, cancel billing, or ask us to delete your account data. Some logs and billing records may be kept where required for security, debugging, tax, or legal reasons.'
  },
  {
    h: 'Contact',
    p: 'For privacy or account requests, email support@autopilot-x.com.'
  }
];

function Reveal({ children, delay }) {
  const ref = React.useRef(null);
  const [seen, setSeen] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el || typeof IntersectionObserver === 'undefined') { setSeen(true); return; }
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => { if (e.isIntersecting) { setSeen(true); io.disconnect(); } });
    }, { threshold: 0.15, rootMargin: '0px 0px -10% 0px' });
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return <div ref={ref} className={'rv' + (seen ? ' rv-in' : '')} style={{ transitionDelay: (delay || 0) + 'ms' }}>{children}</div>;
}

function Eyebrow({ children }) {
  return <div className="lp-eyebrow"><span className="lp-rule" aria-hidden="true" /><span>{children}</span></div>;
}

// A visitor who has signed in on this device before is a returning user: send
// them straight through OAuth. A first-timer gets "Sign up" and is sent to pick
// a plan first — dropping a stranger into an X consent screen before they know
// what they're buying is how you lose them. Plan choice then carries through the
// OAuth round-trip via choosePlan().
const RETURNING = 'xa_returning';
function isReturning() {
  try { return localStorage.getItem(RETURNING) === '1'; } catch (_) { return false; }
}

function SignInX({ big }) {
  const cls = 'lp-cta' + (big ? ' lp-cta-big' : '');
  return isReturning()
    ? <a className={cls} href="/api/auth/x/start">𝕏&nbsp;&nbsp;Sign in with X</a>
    : <a className={cls} href="#pricing">𝕏&nbsp;&nbsp;Sign up with X</a>;
}

function LandingFooter() {
  return (
    <footer className="lp-foot">
      <div className="lp-foot-brand">
        <span className="lp-foot-mark"><Logo size={15} /> X Autopilot</span>
        <span className="lp-foot-dim">Built for early-stage founders.</span>
      </div>
      <nav className="lp-foot-links" aria-label="Footer">
        <div className="lp-foot-group">
          <h4>Support</h4>
          <a href="/support">Support</a>
        </div>
        <div className="lp-foot-group">
          <h4>Privacy</h4>
          <a href="/privacy">Privacy policy</a>
        </div>
      </nav>
    </footer>
  );
}

function SupportPage() {
  const [faq, setFaq] = useState(0);
  useEffect(() => { document.title = 'Support — X Autopilot'; }, []);

  return (
    <div className="lp">
      <header className="lp-nav">
        <a className="lp-mark" href="/"><Logo size={22} /> X Autopilot</a>
        <a className="lp-nav-cta" href="/">Home</a>
      </header>

      <main className="lp-sec lp-page">
        <Eyebrow>Support</Eyebrow>
        <h1>Answers for running X Autopilot.</h1>
        <p className="lp-deck">
          Start here for setup, billing, voice quality, and publishing questions. For account help,
          email <a href="mailto:support@autopilot-x.com">support@autopilot-x.com</a>.
        </p>
        <div className="lp-faq" id="faq">
          {SUPPORT_FAQS.map((f, i) => (
            <div className={'lp-faq-item' + (faq === i ? ' open' : '')} key={i}>
              <button onClick={() => setFaq(faq === i ? -1 : i)} aria-expanded={faq === i}>
                <span>{f.q}</span><span className="lp-faq-chev" aria-hidden="true">{faq === i ? '−' : '+'}</span>
              </button>
              {faq === i && <p>{f.a}</p>}
            </div>
          ))}
        </div>
      </main>

      <LandingFooter />
    </div>
  );
}

function PrivacyPage() {
  useEffect(() => { document.title = 'Privacy Policy — X Autopilot'; }, []);

  return (
    <div className="lp">
      <header className="lp-nav">
        <a className="lp-mark" href="/"><Logo size={22} /> X Autopilot</a>
        <a className="lp-nav-cta" href="/">Home</a>
      </header>

      <main className="lp-sec lp-page">
        <Eyebrow>Privacy policy</Eyebrow>
        <h1>Privacy Policy</h1>
        <p className="lp-deck">
          Last updated July 19, 2026. This policy explains what X Autopilot collects,
          how it is used, and how to contact us about your data.
        </p>
        <div className="lp-policy">
          {PRIVACY_SECTIONS.map((section) => (
            <section className="lp-policy-section" key={section.h}>
              <h2>{section.h}</h2>
              <p>{section.p}</p>
            </section>
          ))}
        </div>
      </main>

      <LandingFooter />
    </div>
  );
}

function Landing({ xLogin, onPassword }) {
  const [password, setPassword] = useState('');
  const [err, setErr] = useState(null);
  const [busy, setBusy] = useState(false);

  const submit = async (e) => {
    if (e) e.preventDefault();
    if (!password) return;
    setBusy(true); setErr(null);
    try { const r = await post('/api/auth/login', { password }); if (r.authed) onPassword(); else setErr('Wrong password.'); }
    catch (_) { setErr('Wrong password.'); } finally { setBusy(false); }
  };

  return (
    <div className="lp">
      <header className="lp-nav">
        <span className="lp-mark"><Logo size={22} /> X Autopilot</span>
        {xLogin && (isReturning()
          ? <a className="lp-nav-cta" href="/api/auth/x/start">Sign in with X</a>
          : <a className="lp-nav-cta" href="#pricing">Sign up with X</a>)}
      </header>

      {/* Hero */}
      <section className="lp-hero">
        <Eyebrow>For founders who are building, not posting</Eyebrow>
        <h1>Nobody buys from a founder they've <span className="accent">never heard of.</span></h1>
        <p className="lp-deck">
          X Autopilot writes your posts in your voice, you approve them in one click, and it learns
          from every edit. You get a credible presence in your market without spending your week on it.
        </p>
        <div className="lp-actions">
          {xLogin
            ? <SignInX big />
            : <form onSubmit={submit} className="lp-pw">
                <input className="lp-input" type="password" autoFocus value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
                <button className="lp-cta" type="submit" disabled={busy || !password}>{busy ? 'Signing in…' : 'Sign in'}</button>
                {err && <p className="lp-err">{err}</p>}
              </form>}
          <span className="lp-note">Free to start — bring your own key. No credit card.</span>
        </div>
      </section>

      {/* Problem */}
      <section className="lp-sec">
        <Reveal>
          <Eyebrow>The problem</Eyebrow>
          <h2>You're heads-down building. Meanwhile your market forgets you exist.</h2>
          <div className="lp-cols">
            <div><h4>Posting is the first thing to slip</h4><p>You know consistency compounds. You also have a product to ship, and writing a good post costs you the one hour you didn't have.</p></div>
            <div><h4>Generic AI posts cost you credibility</h4><p>Hashtag soup and "🧵 a thread on why X is dead" reads as a bot. For an early founder, sounding fake is worse than saying nothing.</p></div>
            <div><h4>Silence looks like no traction</h4><p>Buyers, candidates and investors search you before they reply. An empty feed answers a question you didn't want asked.</p></div>
          </div>
        </Reveal>
      </section>

      {/* How it works */}
      <section className="lp-sec">
        <Reveal>
          <Eyebrow>How it works</Eyebrow>
          <h2>Three steps. Then it runs.</h2>
          <div className="lp-steps">
            <div><span className="lp-num">01</span><h4>Sign in with X</h4><p>One connection. It's your identity, your publishing, and your stats — nothing else to wire up.</p></div>
            <div><span className="lp-num">02</span><h4>It learns your voice</h4><p>It distills a style profile from writing you already have, and picks the topics you want to be known for.</p></div>
            <div><span className="lp-num">03</span><h4>You approve. It posts.</h4><p>Drafts land in your queue. Approve, edit, or reject. Every decision teaches it — and it publishes for you.</p></div>
          </div>
        </Reveal>
      </section>

      {/* Sample post — the inverted panel */}
      <section className="lp-sec lp-invert">
        <Reveal>
          <Eyebrow>What it writes</Eyebrow>
          <h2>A post, not content.</h2>
          <div className="lp-sample">
            <div className="lp-sample-post">
              Most "AI agents" don't do work. They do activity theater with a chat window.{'\n\n'}
              A real agent doesn't draft you a nice email to send. It detects the buyer went quiet,
              forces the next step, and verifies it happened.{'\n\n'}
              Detect. Force. Verify.
            </div>
            <ul className="lp-rules">
              <li><b>No hashtags.</b> No "🧵 thread" theatrics. At most one emoji.</li>
              <li><b>Never invents</b> fake metrics, fake customers, or things that didn't happen.</li>
              <li><b>Under 280</b> characters, in one of five shapes — a sharp take, a build-in-public note, a contrarian claim, a real question, or a thread opener.</li>
              <li><b>Written as you</b>, from your own words — not a generic "content creator" voice.</li>
            </ul>
          </div>
          <p className="lp-fine">That's the actual drafting standard the model is held to — not marketing copy.</p>
        </Reveal>
      </section>

      {/* Trends */}
      <section className="lp-sec">
        <Reveal>
          <Eyebrow>Weekly trends</Eyebrow>
          <h2>Never wonder what to post about.</h2>
          <p className="lp-lead">
            Every week it scans Hacker News, Google News and Reddit for what's actually moving in
            <i> your </i>topics, tells you what's happening in a few lines — and drops ready-to-post
            suggestions straight into your queue. You show up on the conversation while it's live.
          </p>
        </Reveal>
      </section>

      {/* Learning */}
      <section className="lp-sec">
        <Reveal>
          <Eyebrow>It compounds</Eyebrow>
          <h2>Every edit makes the next draft better.</h2>
          <div className="lp-cols">
            <div><h4>It measures how much you changed</h4><p>Each approval, edit and reject is scored. Cut the hashtags every time? It stops adding them.</p></div>
            <div><h4>Your approval rate goes up</h4><p>You watch the edit size shrink week over week. That's the profile converging on how you write.</p></div>
            <div><h4>You decide when it earns autonomy</h4><p>Once your edits stay small and consistent, it offers to run unattended. Until then, nothing ships without you.</p></div>
          </div>
        </Reveal>
      </section>

      {/* Pricing */}
      <section className="lp-sec" id="pricing">
        <Reveal>
          <Eyebrow>Pricing</Eyebrow>
          <h2>Start free. Upgrade when it's earning its keep.</h2>
          <div className="lp-tiers">
            {TIERS.map((t) => (
              <div className={'lp-tier' + (t.best ? ' best' : '')} key={t.name}>
                {/* Not "Best AI" — the model ladder is no longer what the top
                    tier sells, so the badge would be advertising the wrong thing. */}
                {t.best && <span className="lp-badge">Recommended</span>}
                <h4>{t.name}</h4>
                <div className="lp-price">{t.price}{t.price !== 'Free' && <span>/mo</span>}</div>
                <div className="lp-tier-meta">{t.tagline}</div>
                <p>{t.blurb}</p>
                <ul className="lp-tier-lines">
                  {t.lines.map((l) => <li key={l}>{l}</li>)}
                </ul>
                {xLogin && (
                  <button className={'lp-tier-cta' + (t.best ? ' best' : '')} onClick={() => choosePlan(t.id)}>
                    {t.cta}
                  </button>
                )}
              </div>
            ))}
          </div>
          <p className="lp-fine">
            {xLogin
              ? 'You sign in with X first, then land straight on checkout. No credit card for the free plan. Cancel any time.'
              : 'No credit card to start. Cancel any time.'}
          </p>
        </Reveal>
      </section>

      {/* Final CTA */}
      <section className="lp-final">
        <Reveal>
          <h2>Start sounding like the founder you are.</h2>
          <p className="lp-deck">Free to start. Your first drafts in a couple of minutes.</p>
          {xLogin && <div className="lp-actions"><SignInX big /></div>}
        </Reveal>
      </section>

      <LandingFooter />
    </div>
  );
}

// Trends is its own view. It used to render dead last inside Analytics, below
// the funnel, sparkline, topic mix, graduation and engagement blocks — which is
// why it read as missing from the product entirely.
const VIEWS = [
  { id: 'dashboard', label: 'Dashboard' },
  { id: 'trends', label: 'Trends' },
  { id: 'analytics', label: 'Analytics' },
  { id: 'settings', label: 'Settings' },
];

// Stripe sends the user back to /?billing=success|cancel. Without this the return
// from a real payment looked like nothing happened — the plan does update, but
// only via the webhook, so the page has to say so.
const BILLING_RESULT = {
  success: { tone: 'ok', text: 'Payment received — your plan is being activated. It appears here within a few seconds.' },
  cancel: { tone: 'muted', text: 'Checkout cancelled. Nothing was charged.' },
};

function App() {
  const [sess, setSess] = useState(undefined); // undefined=checking, null=signed out, obj=signed in
  const [view, setView] = useState('dashboard');
  const [billing, setBilling] = useState(null);
  const path = window.location.pathname.replace(/\/+$/, '') || '/';
  const publicPage = path === '/support' || path === '/privacy';

  const checkSession = () => get('/api/auth/session')
    .then((d) => {
      // Once they've authed here, this device is no longer a first-time visitor —
      // future signed-out visits say "Sign in", not "Sign up" (see isReturning).
      if (d.authed) { try { localStorage.setItem(RETURNING, '1'); } catch (_) {} }
      setSess(d.authed ? d : { authed: false, xLogin: d.xLogin });
    })
    .catch(() => setSess({ authed: false, xLogin: false }));
  useEffect(() => { if (!publicPage) checkSession(); }, [publicPage]);

  // Returning from Stripe Checkout: acknowledge it, land on Settings so the plan
  // is visible, and re-check the session shortly after — the plan flips when
  // Stripe's webhook lands, which can trail the redirect by a second or two.
  useEffect(() => {
    const q = new URLSearchParams(window.location.search).get('billing');
    if (!q || !BILLING_RESULT[q]) return;
    setBilling(BILLING_RESULT[q]);
    setView('settings');
    window.history.replaceState({}, '', window.location.pathname);
    if (q === 'success') {
      const t1 = setTimeout(checkSession, 2500);
      const t2 = setTimeout(checkSession, 6000);
      return () => { clearTimeout(t1); clearTimeout(t2); };
    }
  }, []);

  // Came in from a landing pricing CTA: the plan they picked survived the X
  // OAuth round-trip in sessionStorage. Send them to Stripe Checkout for it —
  // clearing the key first, so a failed checkout can't put them in a redirect
  // loop. Nothing is charged here; Stripe still asks them to confirm.
  useEffect(() => {
    if (!sess || !sess.authed) return;
    let plan;
    try { plan = sessionStorage.getItem(PENDING_PLAN); sessionStorage.removeItem(PENDING_PLAN); } catch (_) {}
    if (!plan) return;
    setView('settings');
    post('/api/billing/checkout', { plan })
      .then((r) => { if (r && r.url) window.location.href = r.url; })
      .catch(() => {});
  }, [sess && sess.authed]);

  const logout = async () => { try { await post('/api/auth/logout'); } catch (_) {} checkSession(); };

  if (path === '/support') return <SupportPage />;
  if (path === '/privacy') return <PrivacyPage />;
  if (sess === undefined) return <div className="wrap"><h1>X Autopilot</h1><p className="empty">Loading…</p></div>;
  if (!sess.authed) return <Landing xLogin={sess.xLogin} onPassword={checkSession} />;

  return (
    <div className="wrap">
      <header className="topbar">
        <h1 className="brand"><Logo size={20} /> X Autopilot</h1>
        <nav className="nav">
          {VIEWS.map((v) => (
            <button key={v.id} className={'tab' + (view === v.id ? ' active' : '')} onClick={() => setView(v.id)}>{v.label}</button>
          ))}
          {sess.handle && <span className="tab" style={{ color: 'var(--muted)' }}>@{sess.handle}{sess.plan ? ` · ${sess.plan}` : ''}</span>}
          <button className="tab logout" onClick={logout} title="Log out">Log out</button>
        </nav>
      </header>

      {billing && (
        <div className="notice" style={{ marginBottom: 16, borderColor: billing.tone === 'ok' ? 'var(--ok)' : 'var(--line)' }}>
          <span style={{ color: billing.tone === 'ok' ? 'var(--ok)' : 'var(--muted)' }}>{billing.tone === 'ok' ? '✓ ' : ''}</span>
          {billing.text}
          <button className="btn sm ghost" style={{ marginLeft: 10 }} onClick={() => setBilling(null)}>Dismiss</button>
        </div>
      )}

      {view === 'dashboard' && <Dashboard />}
      {view === 'trends' && <TrendsPanel />}
      {view === 'analytics' && <Analytics />}
      {view === 'settings' && <Settings />}
    </div>
  );
}

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