// Vanta App — home screen: interactive cards, checkable moves, habits sheet,
// plant-a-thought composer, horizon pills. Navigation is handled by the shell.

// Canonical card order. 'habits' places the habits tile on desktop; the
// mobile stack filters it out (it has its own dedicated card there).
const V_DEFAULT_ORDER = ['learning', 'work', 'habits', 'home', 'hobbies', 'sleep', 'thoughts', 'professional', 'finance', 'health'];

// Resolve the user's saved order, folding in any keys added since it was saved.
function vantaCardOrder(saved) {
  const stored = Array.isArray(saved) ? saved.filter(k => V_DEFAULT_ORDER.includes(k)) : [];
  return [...stored, ...V_DEFAULT_ORDER.filter(k => !stored.includes(k))];
}

// Arrange mode — mission-control style. Name-only chips jiggle; drag a chip
// up or down to reorder; done commits the order.
function VArrange({ P, order, labels, onDone, onCancel }) {
  const S = vStyles(P);
  const [items, setItems] = React.useState(order);
  const [dragKey, setDragKey] = React.useState(null);
  const drag = React.useRef(null);
  const ROW = 50;

  const down = (i) => (e) => {
    drag.current = { from: i, cur: i, startY: e.clientY, base: items };
    setDragKey(items[i]);
    e.currentTarget.setPointerCapture(e.pointerId);
  };
  const move = (e) => {
    const d = drag.current;
    if (!d) return;
    const delta = Math.round((e.clientY - d.startY) / ROW);
    const to = Math.max(0, Math.min(d.base.length - 1, d.from + delta));
    if (to !== d.cur) {
      d.cur = to;
      const next = [...d.base];
      const [m] = next.splice(d.from, 1);
      next.splice(to, 0, m);
      setItems(next);
    }
  };
  const up = () => { drag.current = null; setDragKey(null); };

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 40,
      background: `radial-gradient(600px 500px at 30% -5%, ${P.bg2}ee, ${P.bg}ee 60%)`,
      display: 'flex', flexDirection: 'column', alignItems: 'center',
      overflowY: 'auto', padding: '28px 20px 40px', boxSizing: 'border-box',
      fontFamily: '"Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif',
    }} onPointerMove={move} onPointerUp={up}>
      <div style={{ width: '100%', maxWidth: 380 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 16 }}>
          <span style={{ ...S.serif, fontSize: 24, color: P.ink }}>Arrange the garden</span>
          <span style={{ ...S.caps, color: P.ink3 }}>drag to reorder</span>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {items.map((k, i) => (
            <div key={k}
              onPointerDown={down(i)}
              className={dragKey === k ? '' : 'vanta-jiggle'}
              style={{
                height: 42, borderRadius: 14, background: V_TILE_BG(P, k),
                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                padding: '0 16px', cursor: 'grab', userSelect: 'none', touchAction: 'none',
                boxShadow: dragKey === k ? `0 6px 18px rgba(28,26,20,0.18)` : 'none',
                transform: dragKey === k ? 'scale(1.04)' : 'none',
                transition: 'box-shadow .15s',
              }}>
              <span style={{ ...S.caps, color: V_RING(P, k), fontSize: 11 }}>{labels[k] || k}</span>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke={P.ink4} strokeWidth="2" strokeLinecap="round">
                <path d="M4 9h16M4 15h16" />
              </svg>
            </div>
          ))}
        </div>
        <div style={{ display: 'flex', gap: 10, marginTop: 20, justifyContent: 'flex-end' }}>
          <button onClick={onCancel} style={{
            appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit',
            padding: '10px 18px', borderRadius: 999, background: 'transparent',
            boxShadow: `inset 0 0 0 1px ${P.ink3}`, color: P.ink2, fontSize: 13, minHeight: 40,
          }}>cancel</button>
          <button onClick={() => onDone(items)} style={{
            appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit',
            padding: '10px 22px', borderRadius: 999, background: P.ink, color: P.bg,
            fontSize: 13, fontWeight: 500, minHeight: 40,
          }}>done</button>
        </div>
      </div>
    </div>
  );
}

function VHomeHeader({ P, palette, onCyclePalette, onSources, onArrange }) {
  const S = vStyles(P);
  return (
    <header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, rowGap: 8, flexWrap: 'wrap', padding: '10px 20px 2px' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <VantaM P={P} />
        <span style={{ ...S.serif, fontSize: 21, color: P.ink }}>vanta</span>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end', minWidth: 0 }}>
        <button onClick={onArrange} aria-label="arrange cards" style={{
          appearance: 'none', border: 0, cursor: 'pointer',
          padding: '6px 9px', borderRadius: 999, minHeight: 28,
          background: P.chip, fontFamily: 'inherit',
          display: 'grid', placeItems: 'center', color: P.ink3,
        }}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
            <rect x="3" y="4" width="8" height="7" rx="1.5" />
            <rect x="14" y="4" width="7" height="10" rx="1.5" />
            <rect x="3" y="14" width="8" height="6" rx="1.5" />
          </svg>
        </button>
        <button onClick={() => { window.location.href = '/vantage'; }} style={{
          appearance: 'none', border: 0, cursor: 'pointer',
          padding: '5px 9px', borderRadius: 999,
          background: P.chip, fontFamily: 'inherit',
          ...S.caps, color: P.ink3, minHeight: 28,
        }}>vantage</button>
        <button onClick={() => { window.location.href = '/agents'; }} style={{
          appearance: 'none', border: 0, cursor: 'pointer',
          padding: '5px 9px', borderRadius: 999,
          background: P.chip, fontFamily: 'inherit',
          ...S.caps, color: P.ink3, minHeight: 28,
        }}>agents</button>
        <button onClick={onSources} style={{
          appearance: 'none', border: 0, cursor: 'pointer',
          padding: '5px 9px', borderRadius: 999,
          background: P.chip, fontFamily: 'inherit',
          ...S.caps, color: P.ink3, minHeight: 28,
        }}>sources</button>
        <button onClick={onCyclePalette} aria-label={`palette: ${palette} — tap to change`} title={`palette · ${palette}`} style={{
          appearance: 'none', border: 0, cursor: 'pointer',
          display: 'flex', alignItems: 'center', gap: 3,
          padding: '7px 9px', borderRadius: 999, minHeight: 28,
          background: P.chip, fontFamily: 'inherit',
        }}>
          {[P.accent, P.rust, P.sky].map((c, i) => (
            <span key={i} style={{ width: 10, height: 10, borderRadius: '50%', background: c }} />
          ))}
        </button>
      </div>
    </header>
  );
}

// Level 0 — the day in one honest line, with a tone dot (attention/good/calm).
// Reads the server-computed glance and re-renders when anything changes.
function VGlance({ P, style }) {
  const [, bump] = React.useReducer(x => x + 1, 0);
  React.useEffect(() => {
    const on = () => bump();
    window.addEventListener('vanta:changed', on);
    return () => window.removeEventListener('vanta:changed', on);
  }, []);
  const g = (window.VANTA_LIVE && window.VANTA_LIVE.glance) || null;
  if (!g || !g.line) return null;
  const tone = g.tone === 'attention' ? P.rust : g.tone === 'good' ? P.accent : P.ink3;
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8, ...style }}>
      <span style={{ width: 7, height: 7, borderRadius: '50%', background: tone, flex: '0 0 auto' }} />
      <span style={{ fontSize: 13, color: P.ink2, lineHeight: 1.3 }}>{g.line}</span>
    </div>
  );
}

// Client mirror of gardenAreaNotable (src/vanta-glance.js, unit-tested) — an
// area opens on its own when a metric is materially behind or it was touched in
// the last 6h; otherwise it rests. Kept in sync with the pure version.
function vGardenAreaNotable(area, lastTouchMs) {
  if (!area || area.connect) return false;
  const details = Array.isArray(area.details) ? area.details : [];
  const behind = details.some(d => typeof d.pct === 'number' && d.pct < 0.7);
  const touched = lastTouchMs > 0 && (Date.now() - lastTouchMs) < 6 * 3600 * 1000;
  return behind || touched;
}

// Split area keys into the ones that stay full (the first `bigCount`, plus
// anything notable today) and the quiet rest. Shared by both shells so the
// smart-open behavior is identical. depth='open' keeps everything full.
function vPartitionAreas(keys, areaFor, lastTouchOf, depth, bigCount = 2) {
  if (depth === 'open') return { notable: keys.slice(), resting: [] };
  const notable = keys.filter((k, i) => i < bigCount || vGardenAreaNotable(areaFor(k), lastTouchOf(k) || 0));
  return { notable, resting: keys.filter(k => !notable.includes(k)) };
}

function VReflection({ data, P }) {
  const S = vStyles(P);
  return (
    <section style={{ padding: '10px 20px 0' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' }}>
        <h1 style={{ ...S.serif, fontSize: 23, lineHeight: 1.1, margin: 0, color: P.ink, whiteSpace: 'nowrap' }}>
          {data.greeting.salutation}
        </h1>
        <span style={{ ...S.serif, fontSize: 14.5, lineHeight: 1.25, color: P.ink2, flex: 1, minWidth: 140, textWrap: 'pretty' }}>
          “{data.reflections[new Date().getDate() % data.reflections.length]}”
        </span>
        <span style={{ alignSelf: 'center' }}><VMoodFace P={P} /></span>
      </div>
      <div style={{ ...S.caps, color: P.rust, marginTop: 5 }}>
        {fmtDate(new Date())} · {data.greeting.weather.temp}° {data.greeting.weather.sky}
      </div>
      <VGlance P={P} style={{ marginTop: 7 }} />
    </section>
  );
}

function VHorizon({ P, value, onChange }) {
  return (
    <div style={{ display: 'flex', padding: 3, gap: 2, margin: '16px 20px 0', background: P.chip, borderRadius: 999, fontSize: 12 }}>
      {['today', 'week', 'month', 'year'].map((h) => (
        <button key={h} onClick={() => onChange(h)} style={{
          appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12,
          flex: 1, textAlign: 'center', padding: '8px 0', borderRadius: 999, minHeight: 30,
          background: value === h ? P.tile : 'transparent',
          color: value === h ? P.ink : P.ink3, fontWeight: value === h ? 500 : 400,
          transition: 'background .15s',
        }}>{h}</button>
      ))}
    </div>
  );
}

// Horizon panel — what's ahead in the chosen window plus consistency
// (this window vs the one before it) for everything reasonably measured.
// Rendered when the horizon control leaves 'today'.
function VHorizonPanel({ P, horizon, flush }) {
  const S = vStyles(P);
  const days = { week: 7, month: 30, year: 365 }[horizon] || 7;
  window.VANTA_HORIZONS = window.VANTA_HORIZONS || {};
  const [data, setData] = React.useState(window.VANTA_HORIZONS[days] || null);
  React.useEffect(() => {
    let alive = true;
    if (window.VANTA_HORIZONS[days]) { setData(window.VANTA_HORIZONS[days]); return; }
    fetch(`/vanta/horizon?days=${days}`)
      .then(r => (r.ok ? r.json() : null))
      .then(d => { if (d) window.VANTA_HORIZONS[days] = d; if (alive) setData(d); })
      .catch(() => { if (alive) setData(undefined); });
    return () => { alive = false; };
  }, [days]);

  const ahead = [];
  if (data) {
    for (const d of (data.deadlines || [])) ahead.push({ k: d.title || 'deadline', note: d.course || null, v: vantaDueLabel(d.due_date) });
    for (const e of (data.events || [])) ahead.push({ k: e.title, note: e.location || null, v: vantaDueLabel(e.event_date) });
    for (const c of (data.choresDue || [])) ahead.push({ k: c.label, note: 'chore', v: c.dueNow ? 'due now' : 'coming due' });
  }
  const shown = ahead.slice(0, 6);
  const cons = data?.consistency;
  const pct = v => `${Math.round(v * 100)}%`;
  const delta = cons?.overall ? cons.overall.cur - cons.overall.prev : 0;

  return (
    <section style={{ margin: flush ? 0 : '16px 20px 0', background: P.tile, borderRadius: 22, padding: '16px 18px' }}>
      <div style={{ ...S.caps, color: P.rust, marginBottom: 8 }}>the {horizon} ahead</div>
      {!data && data !== undefined && <div style={{ fontSize: 12.5, color: P.ink3 }}>listening for what's coming…</div>}
      {(data === undefined || (data && !shown.length)) && (
        <div style={{ fontSize: 12.5, color: P.ink3 }}>nothing scheduled — the garden rests.</div>
      )}
      {shown.map((r, i) => (
        <div key={i} style={{
          display: 'grid', gridTemplateColumns: '1fr auto', gap: 12, alignItems: 'baseline',
          padding: '8px 0', borderBottom: i < shown.length - 1 ? `1px solid ${P.rule}` : 'none',
        }}>
          <span style={{ minWidth: 0, display: 'block' }}>
            <span style={{ fontSize: 13.5, color: P.ink, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{r.k}</span>
            {r.note && <span style={{ fontSize: 11, color: P.ink4, display: 'block' }}>{r.note}</span>}
          </span>
          <span style={{ ...S.num, fontSize: 11.5, color: P.ink3 }}>{r.v}</span>
        </div>
      ))}
      {ahead.length > 6 && <div style={{ ...S.caps, color: P.ink4, marginTop: 6 }}>+ {ahead.length - 6} more</div>}

      {cons && cons.overall && (
        <React.Fragment>
          <div style={{ height: 1, background: P.rule, margin: '14px 0' }} />
          <div style={{ ...S.caps, color: P.accent, marginBottom: 6 }}>consistency</div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
            <span style={{ ...S.serif, fontSize: 24, color: P.ink }}>{pct(cons.overall.cur)} tended</span>
            <span style={{ ...S.num, fontSize: 12, color: delta >= 0 ? P.accent : P.rust }}>
              {delta >= 0 ? '↑' : '↓'} {pct(Math.abs(delta))} vs the {horizon} before
            </span>
          </div>
          {cons.daily && cons.daily.length > 1 && (
            <div style={{ marginTop: 10 }}>
              <Spark data={cons.daily} w={290} h={30} stroke={P.accent} fill={P.accent + '22'} strokeWidth={1.3} />
            </div>
          )}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 12 }}>
            {cons.habits.map(h => {
              const d = h.cur - h.prev;
              const dir = Math.abs(d) < 0.01 ? '·' : d > 0 ? '↑' : '↓';
              const col = Math.abs(d) < 0.01 ? P.ink4 : d > 0 ? P.accent : P.rust;
              return (
                <div key={h.key} style={{ display: 'grid', gridTemplateColumns: '1fr auto auto', gap: 10, alignItems: 'baseline', padding: '4px 0' }}>
                  <span style={{ fontSize: 12.5, color: P.ink2 }}>{h.label}</span>
                  <span style={{ ...S.num, fontSize: 12, color: P.ink }}>{pct(h.cur)}</span>
                  <span style={{ ...S.num, fontSize: 12, color: col, minWidth: 14, textAlign: 'right' }}>{dir}</span>
                </div>
              );
            })}
          </div>
        </React.Fragment>
      )}
      {data && !cons && <div style={{ fontSize: 12, color: P.ink4, marginTop: 8 }}>no consistency data yet — tend a few days first.</div>}
    </section>
  );
}

const V_MOVES = [
  { t: 'send Atlas v2 review notes', area: 'work' },
  { t: 'run · 4 mi easy', area: 'hobbies' },
  { t: 'follow up · Figma internship', area: 'professional' },
];

function vCloneTask(t) { return { ...t, steps: (t.steps || []).map(s => ({ ...s })) }; }

// Shared check circle for task + step rows.
function VCheck({ P, done, color, size = 20 }) {
  return (
    <span style={{
      width: size, height: size, borderRadius: '50%', flex: '0 0 auto',
      border: `1.2px solid ${done ? color : P.ink3}`,
      background: done ? color : 'transparent',
      display: 'grid', placeItems: 'center', transition: 'background .15s',
    }}>
      {done && <svg width={size * 0.55} height={size * 0.55} viewBox="0 0 24 24" fill="none" stroke={P.bg} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" /></svg>}
    </span>
  );
}

// The AI (or manual) breakdown panel for one task. mode='ai' auto-drafts steps
// + up to 3 follow-up questions (answering refines once); mode='manual' is a
// plain step editor. An unreachable model degrades to manual entry with an
// honest note — never a fabricated breakdown.
function VBreakdown({ P, task, mode, cacheRef, onSaved, onClose }) {
  const S = vStyles(P);
  const seed = (task.steps || []).map(s => s.text);
  const c = (cacheRef && cacheRef.current) || null;   // preserved across collapse/reopen
  const [loading, setLoading] = React.useState(false);
  const [steps, setSteps] = React.useState(c ? c.steps : seed);
  const [questions, setQuestions] = React.useState(c ? c.questions : []);
  const [answers, setAnswers] = React.useState(c ? c.answers : {});
  const [note, setNote] = React.useState(c ? c.note : '');
  const [degraded, setDegraded] = React.useState(c ? c.degraded : false);
  const [manual, setManual] = React.useState(c ? c.manual : (mode === 'manual'));
  const [manualText, setManualText] = React.useState(c ? c.manualText : seed.join('\n'));
  const [remember, setRemember] = React.useState(c ? c.remember : false);
  const drafted = React.useRef(c ? c.drafted : false);

  // Persist the working state so collapsing/reopening the row keeps edits (and
  // doesn't re-call the model).
  React.useEffect(() => {
    if (cacheRef) cacheRef.current = { steps, questions, answers, note, degraded, manual, manualText, remember, drafted: drafted.current };
  });

  const call = (qa) => {
    setLoading(true); setNote(''); drafted.current = true;
    fetch('/vanta/task/breakdown', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: task.text, qa: qa || undefined }),
    }).then(r => (r.ok ? r.json() : null)).then(d => {
      if (!d || d.degraded) {
        setDegraded(true); setManual(true);
        setNote((d && d.note) || 'Couldn’t reach the assistant — add steps yourself below.');
        return;
      }
      setNote(d.note || '');
      if (d.steps && d.steps.length) setSteps(d.steps);
      setQuestions(d.questions || []);
    }).catch(() => { setDegraded(true); setManual(true); setNote('Couldn’t reach the assistant — add steps yourself below.'); })
      .finally(() => setLoading(false));
  };
  React.useEffect(() => { if (mode === 'ai' && !drafted.current) call(null); }, []);   // auto-start once
  const refine = () => call(questions.map(q => ({ id: q.id, q: q.q, a: answers[q.id] || '' })));

  const collect = () => (manual
    ? manualText.split('\n').map(s => s.trim()).filter(Boolean)
    : steps.map(s => String(s).trim()).filter(Boolean));

  const save = () => {
    const s = collect();
    if (!s.length) return;
    fetch(`/vanta/task/${task.id}/steps`, {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ steps: s }),
    }).then(r => (r.ok ? r.json() : null)).then(d => {
      if (d && d.steps) onSaved(d.steps);
      if (remember) vantaPost('/vanta/framework', { name: task.text, steps: s });
    }).catch(() => {});
  };

  const box = { background: P.tile, borderRadius: 10, padding: '5px 10px', border: 0, outline: 'none', fontFamily: 'inherit', fontSize: 13, color: P.ink, width: '100%', boxSizing: 'border-box' };
  const chip = (label, onClick, primary) => (
    <button onClick={onClick} style={{
      appearance: 'none', border: primary ? 0 : `0.5px solid ${P.ink3}`, cursor: 'pointer', fontFamily: 'inherit',
      padding: '5px 11px', borderRadius: 999, fontSize: 11.5, fontWeight: 500,
      background: primary ? P.ink : 'transparent', color: primary ? P.bg : P.ink2,
    }}>{label}</button>
  );
  const showSave = !loading && (manual || steps.length > 0);

  return (
    <div style={{ marginTop: 8, padding: '10px 12px', background: P.bg2 || P.tile, borderRadius: 12 }}>
      {loading && <div style={{ fontSize: 12.5, color: P.ink3 }}>thinking it through…</div>}
      {note && <div style={{ fontSize: 11.5, color: degraded ? P.rust : P.ink3, marginBottom: 8 }}>{note}</div>}

      {!loading && !manual && questions.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 10 }}>
          <div style={{ ...S.caps, color: P.sky }}>a couple of questions</div>
          {questions.map(q => (
            <div key={q.id}>
              <div style={{ fontSize: 12.5, color: P.ink2, marginBottom: 3 }}>{q.q}</div>
              {q.kind === 'choice' && Array.isArray(q.choices) ? (
                <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                  {q.choices.map(c => (
                    <button key={c} onClick={() => setAnswers(a => ({ ...a, [q.id]: c }))} style={{
                      appearance: 'none', border: `0.5px solid ${answers[q.id] === c ? P.sky : P.ink3}`, cursor: 'pointer',
                      padding: '4px 9px', borderRadius: 999, fontSize: 11.5, fontFamily: 'inherit',
                      background: answers[q.id] === c ? P.sky : 'transparent', color: answers[q.id] === c ? P.bg : P.ink2,
                    }}>{c}</button>
                  ))}
                </div>
              ) : (
                <input value={answers[q.id] || ''} onChange={e => setAnswers(a => ({ ...a, [q.id]: e.target.value }))}
                  placeholder="your answer (optional)" style={box} />
              )}
            </div>
          ))}
          <div>{chip('refine the steps', refine, true)}</div>
        </div>
      )}

      {!loading && !manual && steps.length > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 5, marginBottom: 8 }}>
          {steps.map((s, i) => (
            <div key={i} style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 6, alignItems: 'center' }}>
              <input value={s} onChange={e => setSteps(st => st.map((x, j) => j === i ? e.target.value : x))} style={box} />
              <button onClick={() => setSteps(st => st.filter((_, j) => j !== i))} aria-label="remove step" style={{ appearance: 'none', border: 0, background: 'transparent', color: P.ink4, cursor: 'pointer', fontSize: 15 }}>×</button>
            </div>
          ))}
          {chip('+ step', () => setSteps(st => [...st, '']))}
        </div>
      )}

      {!loading && manual && (
        <textarea value={manualText} onChange={e => setManualText(e.target.value)} rows={4}
          placeholder="one step per line…" style={{ ...box, resize: 'vertical', lineHeight: 1.5, marginBottom: 8 }} />
      )}

      {showSave && (
        <React.Fragment>
          <label style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 11.5, color: P.ink3, margin: '2px 0 8px' }}>
            <input type="checkbox" checked={remember} onChange={e => setRemember(e.target.checked)} />
            remember these as a framework for next time
          </label>
          <div style={{ display: 'flex', gap: 8 }}>
            {chip('save steps', save, true)}
            {chip('cancel', onClose)}
          </div>
        </React.Fragment>
      )}
    </div>
  );
}

// One task row: check circle, text, carried marker, step progress, expand for
// the breakdown. Optimistic; the server reconciles via vanta:changed refetch.
function VTaskRow({ P, task, expanded, onExpand, offer, onUseFramework, onDismissOffer, patch, remove }) {
  const S = vStyles(P);
  const [showBd, setShowBd] = React.useState(false);
  const [bdMode, setBdMode] = React.useState('ai');
  const bdRef = React.useRef(null);   // preserves breakdown edits across collapse/reopen
  const steps = task.steps || [];
  const doneSteps = steps.filter(s => s.done).length;

  const toggleTask = () => {
    const next = !task.done;
    patch(task.id, t => ({ ...t, done: next, steps: (t.steps || []).map(s => ({ ...s, done: next })) }));
    vantaPost('/vanta/task/toggle', { id: task.id, done: next });
  };
  const toggleStep = (s) => {
    // Optimistic framework/breakdown steps carry temp ids until the server
    // returns real ones; a toggle against 'tmp0' would 400. Ignore until the
    // save resolves (a beat later) and real ids arrive.
    if (String(s.id).startsWith('tmp')) return;
    const nd = !s.done;
    patch(task.id, t => {
      const ns = (t.steps || []).map(x => x.id === s.id ? { ...x, done: nd } : x);
      const allDone = ns.length > 0 && ns.every(x => x.done);
      return { ...t, steps: ns, done: allDone };
    });
    vantaPost('/vanta/task/step', { id: s.id, done: nd });
  };
  const del = () => { remove(task.id); vantaFetchDelete(`/vanta/task/${task.id}`); };

  return (
    <div style={{ background: expanded ? (P.bg2 || P.tile) : 'transparent', borderRadius: 12, padding: expanded ? '4px 8px 8px' : 0, transition: 'background .15s' }}>
      <div style={{ display: 'grid', gridTemplateColumns: '22px 1fr auto', alignItems: 'center', gap: 10, minHeight: 34 }}>
        <button onClick={toggleTask} aria-label={task.done ? 'mark undone' : 'complete'} style={{ appearance: 'none', border: 0, background: 'transparent', cursor: 'pointer', padding: 0 }}>
          <VCheck P={P} done={task.done} color={P.sky} />
        </button>
        <button onClick={onExpand} style={{ appearance: 'none', border: 0, background: 'transparent', cursor: 'pointer', textAlign: 'left', padding: 0, minWidth: 0 }}>
          <span style={{ fontSize: 14.5, color: task.done ? P.ink4 : P.ink, textDecoration: task.done ? 'line-through' : 'none', display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{task.text}</span>
          <span style={{ display: 'flex', gap: 8, alignItems: 'center', marginTop: 1 }}>
            {task.carried && <span style={{ ...S.caps, fontSize: 8.5, color: P.rust }}>carried over</span>}
            {steps.length > 0 && <span style={{ ...S.num, fontSize: 10.5, color: P.ink3 }}>{doneSteps}/{steps.length} steps</span>}
          </span>
        </button>
        <span style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
          <button onClick={onExpand} aria-label="expand" style={{ appearance: 'none', border: 0, background: 'transparent', color: P.ink4, cursor: 'pointer', padding: 3, transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform .15s' }}>
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><path d="M9 6l6 6-6 6" /></svg>
          </button>
          <button onClick={del} aria-label="delete task" style={{ appearance: 'none', border: 0, background: 'transparent', color: P.ink4, cursor: 'pointer', padding: 3, fontSize: 15, lineHeight: 1 }}>×</button>
        </span>
      </div>

      {expanded && (
        <div style={{ paddingLeft: 32 }}>
          {steps.length > 0 && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 3, marginTop: 4 }}>
              {steps.map(s => (
                <button key={s.id} onClick={() => toggleStep(s)} style={{ appearance: 'none', border: 0, background: 'transparent', cursor: 'pointer', display: 'grid', gridTemplateColumns: '16px 1fr', gap: 8, alignItems: 'center', textAlign: 'left', padding: '2px 0', width: '100%' }}>
                  <VCheck P={P} done={s.done} color={P.accent} size={15} />
                  <span style={{ fontSize: 13, color: s.done ? P.ink4 : P.ink2, textDecoration: s.done ? 'line-through' : 'none' }}>{s.text}</span>
                </button>
              ))}
            </div>
          )}
          {offer && (
            <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', margin: '8px 0 2px' }}>
              <button onClick={() => onUseFramework(offer)} style={{ appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit', padding: '5px 11px', borderRadius: 999, fontSize: 11.5, fontWeight: 500, background: P.accent, color: P.bg }}>
                use {offer.seeded ? 'starter' : 'framework'}: {offer.name} ({offer.steps.length} steps)
              </button>
              <button onClick={onDismissOffer} aria-label="dismiss" style={{ appearance: 'none', border: 0, background: 'transparent', color: P.ink4, cursor: 'pointer', fontSize: 12 }}>skip</button>
            </div>
          )}
          {!showBd && steps.length === 0 && (
            <div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
              <button onClick={() => { setBdMode('ai'); setShowBd(true); }} style={{ appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit', padding: '5px 11px', borderRadius: 999, fontSize: 11.5, fontWeight: 500, color: P.bg, background: P.ink }}>break it down</button>
              <button onClick={() => { setBdMode('manual'); setShowBd(true); }} style={{ appearance: 'none', border: `0.5px solid ${P.ink3}`, cursor: 'pointer', fontFamily: 'inherit', padding: '5px 11px', borderRadius: 999, fontSize: 11.5, color: P.ink2, background: 'transparent' }}>add steps</button>
            </div>
          )}
          {!showBd && steps.length > 0 && (
            <button onClick={() => { setBdMode('manual'); setShowBd(true); }} style={{ appearance: 'none', border: 0, background: 'transparent', color: P.ink4, cursor: 'pointer', fontSize: 11.5, marginTop: 6, padding: 0 }}>edit steps</button>
          )}
          {showBd && (
            <VBreakdown P={P} task={task} mode={bdMode} cacheRef={bdRef}
              onClose={() => { setShowBd(false); bdRef.current = null; }}
              onSaved={fresh => { patch(task.id, t => ({ ...t, steps: fresh, done: false })); setShowBd(false); bdRef.current = null; }} />
          )}
        </div>
      )}
    </div>
  );
}

// The real daily task list (server mode): add-input + task rows. Owns local
// React state so taps re-render immediately, and reconciles to server truth on
// vanta:changed (rollover, step→parent coupling, framework steps).
function VTaskList({ P, tasks, setTasks }) {
  const [text, setText] = React.useState('');
  const [expandedId, setExpandedId] = React.useState(null);
  const [offer, setOffer] = React.useState(null);
  const inputRef = React.useRef(null);
  const list = tasks || [];

  const patch = (id, fn) => setTasks(prev => (prev || []).map(t => t.id === id ? fn(t) : t));
  const remove = (id) => setTasks(prev => (prev || []).filter(t => t.id !== id));

  const add = () => {
    const t = text.trim();
    if (!t) return;
    setText('');
    fetch('/vanta/task', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: t }) })
      .then(r => (r.ok ? r.json() : null)).then(d => {
        if (!d || !d.task) return;
        setTasks(prev => (prev || []).some(x => x.id === d.task.id) ? prev : [...(prev || []), vCloneTask(d.task)]);
        if (d.framework) { setOffer({ taskId: d.task.id, framework: d.framework }); setExpandedId(d.task.id); }
      }).catch(() => {}).finally(() => { if (inputRef.current) inputRef.current.focus(); });
  };

  const useFramework = (taskId, fw) => {
    patch(taskId, t => ({ ...t, steps: fw.steps.map((s, i) => ({ id: `tmp${i}`, text: s, done: false })), done: false }));
    setOffer(null);
    fetch(`/vanta/task/${taskId}/steps`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ steps: fw.steps, frameworkId: fw.id }) })
      .then(r => (r.ok ? r.json() : null)).then(d => { if (d && d.steps) patch(taskId, t => ({ ...t, steps: d.steps })); }).catch(() => {});
  };

  return (
    <div>
      <div style={{ display: 'flex', gap: 8, marginBottom: list.length ? 10 : 6 }}>
        <input ref={inputRef} value={text} onChange={e => setText(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter') add(); if (e.key === 'Escape') setText(''); }}
          placeholder="add a task for today…"
          style={{ flex: 1, minWidth: 0, appearance: 'none', border: 0, outline: 'none', background: P.tile, borderRadius: 10, padding: '9px 12px', fontFamily: 'inherit', fontSize: 14, color: P.ink }} />
        <button onClick={add} aria-label="add task" style={{ appearance: 'none', border: 0, cursor: 'pointer', width: 36, borderRadius: 10, background: P.ink, color: P.bg, display: 'grid', placeItems: 'center' }}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>
        </button>
      </div>
      {list.length === 0 && <div style={{ fontSize: 13, color: P.ink3, padding: '2px 0 6px' }}>nothing yet — add today’s first task above.</div>}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 340, overflowY: 'auto' }}>
        {list.map(t => (
          <VTaskRow key={t.id} P={P} task={t}
            expanded={expandedId === t.id} onExpand={() => setExpandedId(expandedId === t.id ? null : t.id)}
            offer={offer && offer.taskId === t.id ? offer.framework : null}
            onUseFramework={fw => useFramework(t.id, fw)} onDismissOffer={() => setOffer(null)}
            patch={patch} remove={remove} />
        ))}
      </div>
    </div>
  );
}

// The today card, tabbed: main (tasks) · chores · school · magenta — every tab
// is an INPUT surface, not a report. When the DB is reachable the main tab is
// the real task list (VTaskList); with no DB it falls back to demo moves.
function VMoves({ P, store, update, inRow }) {
  const S = vStyles(P);
  const [tab, setTab] = React.useState('main');
  const [choresGone, setChoresGone] = React.useState({});
  const L = window.VANTA_LIVE || {};
  const serverMode = Array.isArray(L.moves);
  const [tasks, setTasks] = React.useState(() => (serverMode ? L.moves.map(vCloneTask) : null));
  // Reconcile to server truth after any task/habit/chore write. A generation
  // guard drops out-of-order responses so a slow earlier refetch can't clobber
  // a newer one (rapid taps fire several vanta:changed events).
  const genRef = React.useRef(0);
  React.useEffect(() => {
    const onChange = () => {
      if (!window.vantaRefetchMoves) return;
      const g = ++genRef.current;
      window.vantaRefetchMoves().then(m => { if (Array.isArray(m) && g === genRef.current) setTasks(m.map(vCloneTask)); });
    };
    window.addEventListener('vanta:changed', onChange);
    return () => window.removeEventListener('vanta:changed', onChange);
  }, []);
  const MOVES = (L.moves?.length ? L.moves : V_MOVES);
  const moveKey = (m, i) => (m.id != null ? 'o' + m.id : i);
  const isDone = (m, i) => {
    const k = moveKey(m, i);
    return store.moves[k] !== undefined ? !!store.moves[k] : !!m.done;
  };
  const doneCount = serverMode ? (tasks || []).filter(t => t.done).length : MOVES.filter(isDone).length;
  const mainTotal = serverMode ? (tasks || []).length : MOVES.length;
  const choresDue = (L.choresDue || []).filter(c => !choresGone[c.id]);

  const circle = (done, color) => (
    <span style={{
      width: 20, height: 20, borderRadius: '50%',
      border: `1.2px solid ${done ? color : P.ink3}`,
      background: done ? color : 'transparent',
      display: 'grid', placeItems: 'center', transition: 'background .15s',
    }}>
      {done && <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={P.bg} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" /></svg>}
    </span>
  );
  const rowBtn = {
    appearance: 'none', border: 0, background: 'transparent', cursor: 'pointer',
    display: 'grid', gridTemplateColumns: '22px 1fr', alignItems: 'center', gap: 10,
    minHeight: 32, padding: 0, textAlign: 'left', fontFamily: 'inherit', width: '100%',
  };
  // A checkbox habit rendered as a row inside a tab (assignments → school,
  // magenta → magenta) — same store override path as the habits sheet.
  const habitRow = (hk, text, color) => {
    const list = vHabitList(window.VANTA_DATA, store);
    const h = list.find(x => x.hk === hk);
    if (!h) return null;
    return (
      <button key={hk} onClick={() => {
        vantaPost('/vanta/habit', { habit: hk, completed: !h.done });
        update(s => ({ habits: { ...s.habits, [h.k]: !h.done } }));
      }} style={rowBtn}>
        {circle(h.done, color)}
        <span style={{ fontSize: 14.5, color: h.done ? P.ink4 : P.ink, textDecoration: h.done ? 'line-through' : 'none' }}>{text}</span>
      </button>
    );
  };
  const empty = t => <div style={{ fontSize: 12.5, color: P.ink3, padding: '6px 0' }}>{t}</div>;

  const headerCount = tab === 'main' ? `${doneCount} / ${mainTotal}`
    : tab === 'chores' ? `${choresDue.length} due`
    : tab === 'school' ? `${(L.deadlines || []).length} ahead`
    : `${(L.magentaThoughts || []).length} notes`;

  return (
    <section style={{
      margin: inRow ? 0 : '16px 20px 0', height: inRow ? '100%' : 'auto', boxSizing: 'border-box',
      background: P.tileSky, borderRadius: 22, padding: '14px 18px 16px',
    }}>
      <div style={{ display: 'flex', gap: 5, alignItems: 'baseline', marginBottom: 10, flexWrap: 'wrap' }}>
        {['main', 'chores', 'school', 'magenta'].map(t => (
          <button key={t} onClick={() => setTab(t)} style={{
            appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit',
            padding: '4px 10px', borderRadius: 999, fontSize: 10.5, letterSpacing: '0.08em',
            textTransform: 'uppercase', fontWeight: 600, minHeight: 24,
            background: tab === t ? P.tile : 'transparent',
            color: tab === t ? (t === 'magenta' ? '#c026d3' : P.sky) : P.ink4,
          }}>{t}</button>
        ))}
        <span style={{ flex: 1 }} />
        <span style={{ ...S.caps, color: P.ink4 }}>{headerCount}</span>
      </div>

      {tab === 'main' && serverMode && (
        <VTaskList P={P} tasks={tasks} setTasks={setTasks} />
      )}

      {tab === 'main' && !serverMode && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {MOVES.map((m, i) => {
            const done = isDone(m, i);
            return (
              <button key={moveKey(m, i)} onClick={() => {
                if (m.id != null && !done) vantaPost('/vanta/move', { id: m.id });
                update(s => ({
                  moves: { ...s.moves, [moveKey(m, i)]: !done },
                  lastTouch: m.area ? { ...s.lastTouch, [m.area]: Date.now() } : s.lastTouch,
                }));
              }} style={rowBtn}>
                {circle(done, P.sky)}
                <span style={{
                  fontSize: 14.5, color: done ? P.ink4 : P.ink,
                  textDecoration: done ? 'line-through' : 'none', transition: 'color .15s',
                }}>{m.t}</span>
              </button>
            );
          })}
        </div>
      )}

      {tab === 'chores' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {choresDue.length === 0 && empty('nothing due — the home rests.')}
          {choresDue.map(c => (
            <button key={c.id} onClick={() => {
              vantaPost('/vanta/chore/done', { id: c.id });
              setChoresGone(g => ({ ...g, [c.id]: true }));
              update(s => ({ lastTouch: { ...s.lastTouch, home: Date.now() } }));
            }} style={rowBtn}>
              {circle(false, P.sky)}
              <span style={{ fontSize: 14.5, color: P.ink }}>{c.label}</span>
            </button>
          ))}
        </div>
      )}

      {tab === 'school' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {habitRow('assignments', 'assignments block', P.sky)}
          {habitRow('study', 'study block', P.sky)}
          {(L.deadlines || []).length === 0 && empty('no deadlines on the horizon.')}
          {(L.deadlines || []).slice(0, 4).map((d, i) => (
            <a key={i} href={d.url || undefined} target={d.url ? '_blank' : undefined} rel="noreferrer" style={{
              display: 'grid', gridTemplateColumns: '1fr auto', gap: 10, alignItems: 'baseline',
              textDecoration: 'none', minHeight: 26,
            }}>
              <span style={{ minWidth: 0, display: 'block' }}>
                <span style={{ fontSize: 13.5, color: P.ink, display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{d.title}</span>
                {d.course && <span style={{ fontSize: 11, color: P.ink4 }}>{d.course}</span>}
              </span>
              <span style={{ ...S.num, fontSize: 11.5, color: P.ink3 }}>{d.due}</span>
            </a>
          ))}
          {(L.deadlines || []).length > 4 && (
            <div style={{ ...S.caps, color: P.ink4, marginTop: 2 }}>+ {(L.deadlines || []).length - 4} more</div>
          )}
        </div>
      )}

      {tab === 'magenta' && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {habitRow('magenta', 'magenta project · today’s push', '#c026d3')}
          {(L.magentaThoughts || []).map((t, i) => (
            <div key={i} style={{ fontSize: 12.5, color: P.ink2, fontStyle: 'italic', lineHeight: 1.45 }}>
              “{t.text}”
            </div>
          ))}
          {(L.magentaThoughts || []).length === 0 && empty('no magenta notes yet — plant one below and tag it magenta.')}
        </div>
      )}
    </section>
  );
}

// Habit list with seed data merged against store overrides.
function vHabitList(data, store) {
  return data.areas.habits.details.map(h => ({
    ...h,
    done: store.habits[h.k] !== undefined ? store.habits[h.k] : h.done,
  }));
}

function VHabitsCard({ P, data, store, onOpen, vertical }) {
  const S = vStyles(P);
  const list = vHabitList(data, store);
  const done = list.filter(h => h.done).length;
  if (vertical) {
    return (
      <button onClick={onOpen} style={{
        appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left',
        margin: 0, height: '100%', width: '100%', boxSizing: 'border-box',
        background: P.tileMoss, borderRadius: 22, padding: '16px 16px',
        display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 10,
      }}>
        <div style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'baseline' }}>
          <span style={{ ...S.caps, color: P.accent }}>habits</span>
          <span style={{ ...S.caps, color: P.ink4 }}>log ↗</span>
        </div>
        <Ring pct={done / list.length} size={52} stroke={5} color={P.accent} track={P.ring}>
          <span style={{ ...S.num, fontSize: 14, fontWeight: 500, color: P.ink }}>{done}<span style={{ color: P.ink3, fontSize: 11 }}>/{list.length}</span></span>
        </Ring>
        <div style={{ marginTop: 'auto' }}>
          <div style={{ ...S.serif, fontSize: 16, color: P.ink, lineHeight: 1.15 }}>{window.VANTA_LIVE?.streakText || 'eighteen-day streak'}</div>
          <div style={{ fontSize: 11.5, color: P.ink3, marginTop: 2 }}>
            {list.length - done === 0 ? 'all tended today' : `${list.length - done} left today`}
          </div>
        </div>
      </button>
    );
  }
  return (
    <button onClick={onOpen} style={{
      appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left',
      margin: '12px 20px 0', background: P.tileMoss, borderRadius: 22, padding: '14px 18px',
      display: 'grid', gridTemplateColumns: 'auto 1fr auto', gap: 14, alignItems: 'center',
    }}>
      <Ring pct={done / list.length} size={52} stroke={5} color={P.accent} track={P.ring}>
        <span style={{ ...S.num, fontSize: 14, fontWeight: 500, color: P.ink }}>{done}<span style={{ color: P.ink3, fontSize: 11 }}>/{list.length}</span></span>
      </Ring>
      <div>
        <div style={{ ...S.caps, color: P.accent }}>habits</div>
        <div style={{ ...S.serif, fontSize: 19, color: P.ink, lineHeight: 1.1, marginTop: 2 }}>{window.VANTA_LIVE?.streakText || 'eighteen-day streak'}</div>
        <div style={{ fontSize: 11.5, color: P.ink3, marginTop: 1 }}>
          {list.length - done === 0 ? 'all tended today' : `${list.length - done} left today`}
        </div>
      </div>
      <span style={{ ...S.caps, color: P.ink4 }}>log ↗</span>
    </button>
  );
}

// Bottom-sheet habit logger.
function VHabitsSheet({ P, data, store, update, onClose }) {
  const S = vStyles(P);
  const list = vHabitList(data, store);
  return (
    <div onClick={onClose} style={{
      position: 'absolute', inset: 0, zIndex: 30,
      background: P.scrim, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end',
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: P.bg2, borderRadius: '26px 26px 0 0', padding: '14px 20px 28px',
        maxHeight: '75%', overflowY: 'auto',
      }}>
        <div style={{ width: 36, height: 4, borderRadius: 999, background: P.ink4, opacity: 0.4, margin: '0 auto 14px' }} />
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 14 }}>
          <h2 style={{ ...S.serif, fontSize: 24, margin: 0, color: P.ink }}>Habits</h2>
          <span style={{ ...S.caps, color: P.ink3 }}>tap to tend</span>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {list.map((h, i) => (
            <button key={i} onClick={() => {
              if (h.hk) vantaPost('/vanta/habit', { habit: h.hk, completed: !h.done });
              update(s => ({ habits: { ...s.habits, [h.k]: !h.done } }));
            }} style={{
              appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left',
              display: 'grid', gridTemplateColumns: '24px 1fr auto', alignItems: 'center', gap: 12,
              padding: '11px 14px', borderRadius: 14, minHeight: 44,
              background: h.done ? P.tileMoss : P.tile,
              transition: 'background .15s',
            }}>
              <span style={{
                width: 20, height: 20, borderRadius: 6,
                border: `1.2px solid ${h.done ? P.accent : P.ink4}`,
                background: h.done ? P.accent : 'transparent',
                display: 'grid', placeItems: 'center', transition: 'background .15s',
              }}>
                {h.done && <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={P.bg} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" /></svg>}
              </span>
              <span style={{ fontSize: 14.5, color: h.done ? P.ink2 : P.ink }}>{h.k}</span>
              <span style={{ ...S.num, fontSize: 11, color: P.ink4 }}>{h.streak ? `${h.streak}d` : '—'}</span>
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

// Compact area card — taps expand it in place (accordion), no navigation.
function VAreaCard({ area, areaKey, data, P, store, update, expanded, onToggle }) {
  const S = vStyles(P);
  const ring = V_RING(P, areaKey);

  // Height-tweened expansion: measure the body, animate px, settle on
  // 'auto' so content that grows while open (planted thoughts, tapped
  // rows) never clips.
  const bodyRef = React.useRef(null);
  const wrapRef = React.useRef(null);
  const firstRender = React.useRef(true);
  const [bodyH, setBodyH] = React.useState(expanded ? 'auto' : 0);
  React.useEffect(() => {
    if (firstRender.current) { firstRender.current = false; return; }
    const body = bodyRef.current, wrap = wrapRef.current;
    if (!body || !wrap) return;
    if (expanded) {
      setBodyH(body.scrollHeight);
      const t = setTimeout(() => setBodyH('auto'), 320);
      return () => clearTimeout(t);
    }
    // Collapse from 'auto': pin the current px height, flush layout, then
    // let React set 0 so the transition has a concrete starting point.
    wrap.style.height = `${body.scrollHeight}px`;
    void wrap.offsetHeight;
    setBodyH(0);
  }, [expanded]);
  const pcts = area.details.map(d => typeof d.pct === 'number' ? d.pct : null).filter(v => v != null);
  const avg = pcts.length ? pcts.reduce((a, b) => a + b, 0) / pcts.length : 0.5;
  const touched = vantaAgo(store.lastTouch[areaKey]);
  const isConnect = area.connect && !store.linked[areaKey];

  return (
    <div style={{ background: V_TILE_BG(P, areaKey), borderRadius: 22, width: '100%', boxSizing: 'border-box' }}>
      <button onClick={onToggle} aria-expanded={expanded} style={{
        appearance: 'none', border: 0, cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left',
        background: 'transparent', padding: '14px 18px',
        display: 'flex', flexDirection: 'column', gap: 10, width: '100%', boxSizing: 'border-box',
      }}>
        <div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr auto', gap: 14, alignItems: 'center', width: '100%' }}>
          {isConnect ? (
            <span style={{ width: 44, height: 44, borderRadius: '50%', display: 'grid', placeItems: 'center', background: P.chip, border: `1px dashed ${ring}` }}>
              <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke={ring} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                <rect x="4" y="10" width="16" height="10" rx="2" /><path d="M8 10V7a4 4 0 0 1 8 0v3" />
              </svg>
            </span>
          ) : (
            <Ring pct={avg} size={44} stroke={4.5} color={ring} track={P.ring}>
              <span style={{ ...S.num, fontSize: 10, fontWeight: 600, color: P.ink }}>{Math.round(avg * 100)}</span>
            </Ring>
          )}
          <span style={{ minWidth: 0, display: 'block' }}>
            <span style={{ ...S.caps, color: ring, display: 'flex', gap: 8, alignItems: 'baseline' }}>
              {area.label.toLowerCase()}
              {touched && <span style={{ color: P.ink4, letterSpacing: '0.05em', textTransform: 'none' }}>· {touched}</span>}
            </span>
            <span style={{ ...S.serif, fontSize: 19, color: P.ink, lineHeight: 1.1, marginTop: 2, display: 'block' }}>
              {isConnect ? area.headline : area.headline}
            </span>
            <span style={{ fontSize: 11.5, color: P.ink3, marginTop: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{area.sub}</span>
          </span>
          <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke={P.ink4} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"
            style={{ transition: 'transform .28s cubic-bezier(.4,0,.2,1)', transform: expanded ? 'rotate(90deg)' : 'none' }}>
            <path d="M1 1l5 5-5 5" />
          </svg>
        </div>
        {!isConnect && <Spark data={area.trend} w={290} h={22} stroke={ring} fill={ring + '22'} strokeWidth={1.3} />}
      </button>

      {/* Depth, in place — measured height tween, settles on auto. */}
      <div ref={wrapRef} style={{
        overflow: 'hidden',
        height: bodyH === 'auto' ? 'auto' : `${bodyH}px`,
        opacity: expanded ? 1 : 0,
        visibility: expanded || bodyH !== 0 ? 'visible' : 'hidden',
        transition: 'height .3s cubic-bezier(.4,0,.2,1), opacity .25s ease',
      }}>
        <div ref={bodyRef} style={{ padding: '0 18px 16px' }}>
          <VAreaBody areaKey={areaKey} area={area} data={data} P={P} store={store} update={update} variant="card" />
        </div>
      </div>
    </div>
  );
}

function VThemes({ goals, P }) {
  const S = vStyles(P);
  return (
    <section style={{ margin: '12px 20px 0', background: P.tile, borderRadius: 22, padding: '18px 18px 16px' }}>
      <div style={{ ...S.caps, color: P.accent, marginBottom: 4 }}>the year, in four lines</div>
      <h2 style={{ ...S.serif, fontSize: 24, margin: '0 0 14px', color: P.ink }}>Themes for 2026</h2>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
        {goals.details.map((g, i) => (
          <div key={i}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6 }}>
              <span style={{ ...S.serif, fontSize: 16, color: P.ink }}>{g.k}</span>
              <span style={{ ...S.num, fontSize: 13, color: P.ink2 }}>{g.v} <span style={{ color: P.ink4, fontSize: 11 }}>· {g.goal}</span></span>
            </div>
            <ProgBar pct={g.pct} h={2} color={[P.rust, P.accent, P.sky, P.ink2][i]} track={P.rule} radius={999} />
          </div>
        ))}
      </div>
    </section>
  );
}

// Working thought composer.
function VPlantBar({ P, store, update }) {
  const S = vStyles(P);
  const [open, setOpen] = React.useState(false);
  const [text, setText] = React.useState('');
  const inputRef = React.useRef(null);
  React.useEffect(() => { if (open && inputRef.current) inputRef.current.focus(); }, [open]);

  const plant = () => {
    const t = text.trim();
    if (!t) { setOpen(false); return; }
    vantaPost('/vanta/thought', { text: t });
    update(s => ({
      thoughts: [{ text: t, ts: Date.now() }, ...s.thoughts].slice(0, 50),
      lastTouch: { ...s.lastTouch, thoughts: Date.now() },
    }));
    setText('');
    setOpen(false);
  };

  return (
    <div style={{
      position: 'sticky', bottom: 0, padding: '12px 20px 14px', zIndex: 20,
      background: `linear-gradient(to top, ${P.bg} 65%, transparent)`,
    }}>
      {open ? (
        <div style={{
          display: 'grid', gridTemplateColumns: '1fr auto', gap: 10, alignItems: 'center',
          background: P.ink, borderRadius: 999, padding: '6px 6px 6px 20px', minHeight: 48, boxSizing: 'border-box',
        }}>
          <input
            ref={inputRef}
            value={text}
            onChange={e => setText(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter') plant(); if (e.key === 'Escape') setOpen(false); }}
            placeholder="what's on your mind…"
            style={{
              appearance: 'none', border: 0, outline: 'none', background: 'transparent',
              color: P.bg, fontFamily: '"Instrument Serif", serif', fontStyle: 'italic', fontSize: 16,
              width: '100%', padding: 0,
            }}
          />
          <button onClick={plant} style={{
            appearance: 'none', border: 0, cursor: 'pointer',
            width: 36, height: 36, borderRadius: '50%', background: P.bg, color: P.ink,
            display: 'grid', placeItems: 'center',
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5M5 12l7-7 7 7" /></svg>
          </button>
        </div>
      ) : (
        <button onClick={() => setOpen(true)} style={{
          appearance: 'none', border: 0, cursor: 'pointer', width: '100%', boxSizing: 'border-box',
          display: 'grid', gridTemplateColumns: '1fr auto', gap: 10, alignItems: 'center',
          background: P.ink, borderRadius: 999, padding: '6px 6px 6px 20px', minHeight: 48, textAlign: 'left',
        }}>
          <span style={{ ...S.serif, fontSize: 16, color: P.bg, opacity: 0.85 }}>plant a thought…</span>
          <span style={{ width: 36, height: 36, borderRadius: '50%', background: P.bg, color: P.ink, display: 'grid', placeItems: 'center' }}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>
          </span>
        </button>
      )}
    </div>
  );
}

const V_MAGENTA = '#c026d3';

// Local YYYY-MM-DD (not toISOString — that's UTC and would shift the
// calendar a day for anyone west of Greenwich).
function vLocalISO(d) {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}

// Contiguous list of YYYY-MM-DD strings from startISO..endISO inclusive.
// Anchored at local noon so day-stepping never trips DST or a UTC shift.
function vIsoRange(startISO, endISO) {
  const out = [];
  const cur = new Date(startISO + 'T12:00:00');
  const end = new Date(endISO + 'T12:00:00');
  while (cur <= end) { out.push(vLocalISO(cur)); cur.setDate(cur.getDate() + 1); }
  return out;
}

// The rhythm card: streak, a consistency line, and the magenta calendar.
// Encoding is literal — a day's square carries opacity equal to the share of
// habits kept (a 20% day is 20% opacity), a full day is a full square, and a
// full day with objectives shipped on top gets a soft aura. A day that was
// tracked but kept nothing shows a faint dot (an honest zero); a day with no
// record at all stays an empty outline (absence, never a fabricated zero).
// Day boundaries follow the SERVER's clock (data.today) — the same one habits
// are logged under — so today's square fills the moment you check a habit.
function VRhythm({ P, flush }) {
  const S = vStyles(P);
  const [data, setData] = React.useState(window.VANTA_RHYTHM || null);

  React.useEffect(() => {
    let alive = true;
    const load = () => fetch('/vanta/rhythm?days=70')
      .then(r => (r.ok ? r.json() : null))
      .then(d => { window.VANTA_RHYTHM = d || null; if (alive) setData(d || undefined); })
      .catch(() => { if (alive) setData(undefined); });
    if (window.VANTA_RHYTHM) setData(window.VANTA_RHYTHM); else load();
    // A habit/objective/chore write invalidates the cache elsewhere and fires
    // this — so today's square and the streak update the moment you act.
    const onChanged = () => load();
    window.addEventListener('vanta:changed', onChanged);
    return () => { alive = false; window.removeEventListener('vanta:changed', onChanged); };
  }, []);

  const total = data?.total || 0;
  const byDay = new Map((data?.days || []).map(r => [r.d, r]));
  const todayISO = (data && data.today) || vLocalISO(new Date());

  // 8 calendar-aligned weeks, sun→sat columns, bottom row is this week —
  // walked back from the server's today so cell keys match the row keys.
  const anchor = new Date(todayISO + 'T12:00:00');
  const start = new Date(anchor);
  start.setDate(anchor.getDate() - anchor.getDay() - 49);
  const cells = [];
  for (let i = 0; i < 56; i++) {
    const d = new Date(start);
    d.setDate(start.getDate() + i);
    const iso = vLocalISO(d);
    const rec = byDay.get(iso);
    const done = rec ? rec.done : 0;
    const pct = total > 0 ? Math.min(1, done / total) : 0;
    cells.push({
      iso, future: iso > todayISO, today: iso === todayISO,
      has: !!rec, done, extras: rec ? rec.extras : 0, pct,
    });
  }

  // Consistency line over CONTIGUOUS calendar days (first record → last
  // record), so gaps read as dips instead of collapsing out of existence.
  // Fixed [0,1] domain so a steady 60% sits at 60% height, not on the floor.
  const recDays = data?.days || [];
  let line = [];
  if (recDays.length && total > 0) {
    const range = vIsoRange(recDays[0].d, recDays[recDays.length - 1].d).slice(-42);
    line = range.map(iso => { const r = byDay.get(iso); return r ? Math.min(1, r.done / total) : 0; });
  }
  const streak = data?.streak || 0;
  const anyLogged = recDays.some(r => r.done > 0);

  return (
    <section style={{ margin: flush ? 0 : '14px 20px 0', background: P.tile, borderRadius: 22, padding: '16px 18px', boxSizing: 'border-box' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
        <span style={{ ...S.caps, color: V_MAGENTA }}>rhythm</span>
        <span style={{ ...S.serif, fontSize: 14.5, color: P.ink2, marginLeft: 'auto' }}>
          {streak > 0 ? `${vantaNumWord(streak)}-day streak` : 'no streak yet'}
        </span>
      </div>

      {data === undefined && (
        <div style={{ fontSize: 12.5, color: P.ink3, marginTop: 10 }}>
          the garden can't reach habit history right now — nothing to show, honestly.
        </div>
      )}
      {data === null && <div style={{ fontSize: 12.5, color: P.ink3, marginTop: 10 }}>reading the log…</div>}

      {data && !anyLogged && (
        <div style={{ ...S.serif, fontSize: 14, color: P.ink3, marginTop: 10 }}>
          no rhythm on record yet — keep one habit today and the first square appears.
        </div>
      )}

      {data && anyLogged && (
        <React.Fragment>
          <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', alignItems: 'flex-end', marginTop: 10 }}>
            {line.length > 1 && (
              <div style={{ flex: '1 1 230px', minWidth: 0 }}>
                <div style={{ ...S.caps, color: P.ink4, marginBottom: 4 }}>consistency · % of habits kept, by day</div>
                <Spark data={line} w={280} h={34} stroke={V_MAGENTA} fill={V_MAGENTA + '1e'} strokeWidth={1.4} domain={[0, 1]} />
              </div>
            )}
            <div style={{ flex: '0 1 240px' }}>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 4 }}>
                {['s', 'm', 't', 'w', 't', 'f', 's'].map((w, i) => (
                  <span key={i} style={{ ...S.caps, fontSize: 8.5, textAlign: 'center', color: P.ink4 }}>{w}</span>
                ))}
                {cells.map(c => {
                  if (c.future) return <span key={c.iso} style={{ aspectRatio: '1' }} />;
                  const aura = c.pct === 1 && c.extras > 0;
                  const zeroKept = c.has && c.done === 0;      // tracked, kept nothing
                  const title = c.has
                    ? `${c.iso} · ${c.done}/${total} kept${c.extras > 0 ? ` · +${c.extras} beyond` : ''}`
                    : `${c.iso} · nothing logged`;
                  return (
                    <span key={c.iso} title={title} style={{
                      aspectRatio: '1', borderRadius: 4, position: 'relative',
                      boxShadow: [
                        c.pct === 0 ? `inset 0 0 0 1px ${P.rule}` : null,
                        aura ? `0 0 9px 1.5px ${V_MAGENTA}66` : null,
                        c.today ? `inset 0 0 0 1.5px ${P.ink}` : null,
                      ].filter(Boolean).join(', ') || 'none',
                    }}>
                      {c.pct > 0 && (
                        <span style={{
                          position: 'absolute', inset: 0, borderRadius: 4,
                          background: V_MAGENTA, opacity: c.pct,
                        }} />
                      )}
                      {zeroKept && (
                        <span style={{
                          position: 'absolute', top: '50%', left: '50%', width: 3, height: 3,
                          transform: 'translate(-50%,-50%)', borderRadius: '50%', background: P.ink4,
                        }} />
                      )}
                    </span>
                  );
                })}
              </div>
            </div>
          </div>
          <div style={{ ...S.caps, fontSize: 8.5, color: P.ink4, marginTop: 8, letterSpacing: '0.12em' }}>
            full square = all kept · shine = went beyond · fade = partial · dot = kept none · empty = untracked
          </div>
        </React.Fragment>
      )}
    </section>
  );
}

// Frown→smile SVG. Hoisted to module scope so its identity is stable across
// re-renders — a component defined inside VMoodFace would remount the whole
// face on every open/pick. A dashed outline + dotted mouth means "not logged".
function VMoodFaceSvg({ P, s, px, dashed }) {
  const color = s == null ? P.ink4 : s <= 2 ? P.rust : s === 3 ? P.ink2 : V_MAGENTA;
  const bend = s == null ? 0 : (s - 3) * 2.7;
  const y0 = 15.5 - bend * 0.28;
  return (
    <svg width={px} height={px} viewBox="0 0 24 24" style={{ display: 'block' }}>
      <circle cx="12" cy="12" r="10.4" fill="none" stroke={color} strokeWidth="1.5"
        strokeDasharray={dashed ? '2.6 2.8' : 'none'} opacity={dashed ? 0.75 : 1} />
      <circle cx="8.6" cy="9.6" r="1.25" fill={color} />
      <circle cx="15.4" cy="9.6" r="1.25" fill={color} />
      <path d={`M 8 ${y0} Q 12 ${y0 + bend * 1.9} 16 ${y0}`} fill="none"
        stroke={color} strokeWidth="1.5" strokeLinecap="round"
        strokeDasharray={dashed ? '1.6 2.2' : 'none'} />
    </svg>
  );
}

// A small face that carries the day's mood. Reads the logged score from the
// server (via /vanta/data); tapping opens five faces to pick from, and the
// pick writes back through /vanta/mood — the same endpoint an iOS Shortcut
// can hit from a Journal routine (Apple exposes no API to read Journal
// itself, so the Shortcut is the bridge — see docs/mood-shortcut.md).
function VMoodFace({ P, size = 30 }) {
  const [score, setScore] = React.useState(() => {
    const m = window.VANTA_LIVE && window.VANTA_LIVE.mood;
    return typeof m === 'number' ? m : null;
  });
  const [open, setOpen] = React.useState(false);
  const wrapRef = React.useRef(null);

  // Dismiss the picker on tap-away or Escape WITHOUT committing — closing it
  // must never conjure a mood the user didn't deliberately choose.
  React.useEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); };
  }, [open]);

  const pick = (s) => {
    setScore(s); setOpen(false);
    vantaPost('/vanta/mood', { score: s });
    // Land the write on the global even if the initial live load failed
    // (VANTA_LIVE null), so the other shell reads it after a remount.
    window.VANTA_LIVE = window.VANTA_LIVE || {};
    window.VANTA_LIVE.mood = s;
  };

  return (
    <span ref={wrapRef} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, position: 'relative' }}>
      {open ? (
        <span style={{
          display: 'inline-flex', gap: 5, alignItems: 'center',
          background: P.tile, borderRadius: 999, padding: '4px 8px',
        }}>
          {[1, 2, 3, 4, 5].map(s => (
            <button key={s} onClick={() => pick(s)} aria-label={`mood ${s} of 5`} style={{
              appearance: 'none', border: 0, background: 'transparent', cursor: 'pointer',
              padding: 2, display: 'grid', placeItems: 'center',
              opacity: score === s ? 1 : 0.72,
            }}>
              <VMoodFaceSvg P={P} s={s} px={size - 6} />
            </button>
          ))}
        </span>
      ) : (
        <button onClick={() => setOpen(o => !o)} aria-label={score == null ? 'log mood' : `mood ${score} of 5 — tap to change`}
          title={score == null ? 'log today’s mood' : `today’s mood · ${score}/5`}
          style={{ appearance: 'none', border: 0, background: 'transparent', cursor: 'pointer', padding: 2, display: 'grid', placeItems: 'center' }}>
          <VMoodFaceSvg P={P} s={score} px={size} dashed={score == null} />
        </button>
      )}
    </span>
  );
}

Object.assign(window, {
  VHomeHeader, VReflection, VHorizon, VHorizonPanel, VMoves, VHabitsCard, VHabitsSheet,
  VAreaCard, VThemes, VPlantBar, VArrange, VRhythm, VMoodFace, VGlance,
  vHabitList, vGardenAreaNotable, vPartitionAreas, vantaCardOrder, V_MOVES, V_DEFAULT_ORDER,
});
