/* ── Check-In meeting ritual ────────────────────────────────────────────────
   The paced, forward-only, together-on-the-couch ceremony. Full-bleed overlay
   mounted by LedgerApp (above the shell, so it covers the sidebar) when the
   user presses Begin/Resume on the Check-In tab.

   Five screens: answers reveal · expenses · savings (allocate) · commonplace ·
   outro. Progress persists in the meeting record (screenIndex) so an exit
   mid-ritual resumes cleanly. Allocations and commonplace actions commit
   immediately (Decision 5) — nothing is staged.

   See CHECK-IN-ARCHITECTURE.md §5.2. state here is effectiveState (table-merged)
   so expenses + brainDump.notes are live.
   ────────────────────────────────────────────────────────────────────────── */

const MEETING_SCREENS = ['answers', 'expenses', 'savings', 'commonplace', 'outro'];
const capDay = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);

function MeetingRitual({ state, setState, brainOps, userId, onExit }) {
  const status = checkInStatus(state, userId);
  const cycleKey = status.cycleKey;
  const [confirmLeave, setConfirmLeave] = React.useState(false);

  // Ensure a meeting record exists. Create one at Begin (snapshotting the
  // questions + both partners' answers); resume the existing one otherwise.
  const initRef = React.useRef(false);
  React.useEffect(() => {
    if (initRef.current) return;
    initRef.current = true;
    setState(s => {
      if ((s.checkInMeetings || []).some(m => m.cycleKey === cycleKey)) return s;
      const questions = [...(s.checkInQuestions || [])]
        .sort((a, b) => a.order - b.order)
        .map(q => ({ id: q.id, prompt: q.prompt }));
      const responses = checkInParticipants(s).map(m => {
        const ci = checkInForMember(s, cycleKey, m.id);
        return { memberId: m.id, name: m.name, answers: (ci && ci.answers) || {} };
      });
      const meeting = {
        id: 'cm' + Date.now().toString(36),
        cycleKey,
        startedAt: new Date().toISOString(),
        completedAt: null,
        screenIndex: 0,
        questions, responses,
        allocations: [], commonplaceActions: [],
      };
      return { ...s, checkInMeetings: [...(s.checkInMeetings || []), meeting] };
    });
  }, []);

  // Escape → confirm leave (the ritual owns Esc; LedgerApp suppresses its own
  // shortcuts while the ritual is mounted).
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key !== 'Escape') return;
      // If a Manage/Edit sheet is open on top of the ritual, let it own Esc —
      // don't pop the leave-confirm out from under it.
      if (document.querySelector('.meeting-overlay .modal-backdrop:not(.closing)')) return;
      e.preventDefault(); e.stopPropagation(); setConfirmLeave(true);
    };
    window.addEventListener('keydown', onKey, true);
    return () => window.removeEventListener('keydown', onKey, true);
  }, []);

  const meeting = meetingForCycle(state, cycleKey);

  const patchMeeting = (patch) => setState(s => ({
    ...s,
    checkInMeetings: (s.checkInMeetings || []).map(m =>
      m.cycleKey === cycleKey ? { ...m, ...(typeof patch === 'function' ? patch(m) : patch) } : m),
  }));

  const advance = () => patchMeeting(m => ({ screenIndex: Math.min(m.screenIndex + 1, MEETING_SCREENS.length - 1) }));

  const finish = () => {
    patchMeeting({ completedAt: new Date().toISOString() });
    onExit();
  };

  if (!meeting) {
    // First paint before the create-effect's setState lands. Show the calm
    // backdrop so there's no flash of the app behind.
    return <div className="meeting-overlay" />;
  }

  const screen = MEETING_SCREENS[meeting.screenIndex] || 'answers';

  let stage = null;
  if (screen === 'answers') {
    stage = <MeetingAnswersReveal meeting={meeting} state={state} userId={userId} onDone={advance} />;
  } else if (screen === 'expenses') {
    stage = <MeetingExpensesReveal state={state} cycleKey={cycleKey} onDone={advance} />;
  } else if (screen === 'savings') {
    stage = <MeetingSavingsReveal state={state} setState={setState} cycleKey={cycleKey} patchMeeting={patchMeeting} onDone={advance} />;
  } else if (screen === 'commonplace') {
    stage = <MeetingCommonplaceReveal state={state} setState={setState} brainOps={brainOps} userId={userId} patchMeeting={patchMeeting} onDone={advance} />;
  } else {
    stage = <MeetingOutro status={status} onFinish={finish} />;
  }

  return (
    <div className={`meeting-overlay meeting-screen-${screen}`}>
      <div className="meeting-frame">
        <div className="meeting-progress">
          {MEETING_SCREENS.map((_, i) => (
            <span key={i} className={`meeting-dot ${i === meeting.screenIndex ? 'active' : ''} ${i < meeting.screenIndex ? 'past' : ''}`} />
          ))}
        </div>
        {/* key on screenIndex re-mounts the stage so its enter animation plays */}
        <div className="meeting-stage" key={meeting.screenIndex}>
          {stage}
        </div>
      </div>

      {confirmLeave && (
        <div className="meeting-confirm-backdrop" onClick={() => setConfirmLeave(false)}>
          <div className="meeting-confirm" onClick={e => e.stopPropagation()}>
            <div className="meeting-confirm-title">Leave the meeting?</div>
            <div className="meeting-confirm-sub">Your progress is saved — you can pick up right here.</div>
            <div className="meeting-confirm-actions">
              <button className="btn" onClick={() => setConfirmLeave(false)}>Stay</button>
              <button className="btn ghost" onClick={onExit}>Leave</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* Screen 1 — the emotional core. One question at a time. EVERY answer starts
   sealed (couples check in together on one device, so no one peeks ahead) —
   you tap each card to unseal it, reading them together. Mine sorts first. */
function MeetingAnswersReveal({ meeting, state, userId, onDone }) {
  const questions = meeting.questions || [];
  const responses = meeting.responses || [];
  const [qi, setQi] = React.useState(0);
  const [revealed, setRevealed] = React.useState({}); // { [memberId]: true } for this question

  const me = (state.family || []).find(m => m.authUserId === userId);
  const myId = me ? me.id : (responses[0] && responses[0].memberId);
  const ordered = [
    ...responses.filter(r => r.memberId === myId),
    ...responses.filter(r => r.memberId !== myId),
  ];
  // First-name-only labels, disambiguated only on collision (part 4a).
  const displayNames = checkInDisplayNames(responses.map(r => ({ id: r.memberId, name: r.name })));

  if (questions.length === 0) {
    return (
      <div className="meeting-content meeting-answers">
        <div className="meeting-eyebrow">Check-in</div>
        <p className="meeting-answers-empty">No questions this week. Add some in Check-In settings for next time.</p>
        <MeetingDone label="Continue" onClick={onDone} />
      </div>
    );
  }

  const q = questions[qi];
  const isLast = qi === questions.length - 1;
  const next = () => {
    if (!isLast) { setQi(qi + 1); setRevealed({}); }
    else onDone();
  };
  const reveal = (mid) => setRevealed(r => ({ ...r, [mid]: true }));

  return (
    <div className="meeting-content meeting-answers">
      <div className="meeting-eyebrow">Check-in · {qi + 1} of {questions.length}</div>
      <h2 className="meeting-answers-q">{q.prompt}</h2>

      <div className="meeting-answers-cards">
        {ordered.map(r => {
          const isMe = r.memberId === myId;
          const shown = !!revealed[r.memberId];
          return (
            <div className={`meeting-answer-card ${isMe ? 'mine' : 'theirs'} ${shown ? 'revealed' : 'sealed'}`} key={r.memberId}>
              {shown ? (
                <div className="meeting-answer-row">
                  <div className="meeting-answer-who">{displayNames[r.memberId] || r.name}</div>
                  <CheckInAnswerText raw={r.answers && r.answers[q.id]} className="meeting-answer-text" />
                </div>
              ) : (
                <button className="meeting-answer-seal" onClick={() => reveal(r.memberId)}>
                  Reveal {displayNames[r.memberId] || r.name}'s answer
                </button>
              )}
            </div>
          );
        })}
      </div>

      <MeetingDone label={isLast ? 'Done' : 'Next question'} onClick={next} />
    </div>
  );
}

/* Screen 2 — the honest weekly number, seen together. */
function MeetingExpensesReveal({ state, cycleKey, onDone }) {
  const closedMonths = state.closedMonths || [];
  const spend = cycleExpenses(state.expenses, cycleKey, closedMonths);
  const total = spend.reduce((s, e) => s + expenseNet(e), 0);

  // Any cycle-window expenses excluded because their month is closed out? The
  // window can straddle a month boundary, so the first meeting of a new month
  // would otherwise re-show the previous (already-reconciled) month's tail.
  const excluded = cycleExpenses(state.expenses, cycleKey)
    .filter(e => closedMonths.includes(monthKey(e.date)));
  const excludedMonthKeys = [...new Set(excluded.map(e => monthKey(e.date)))];
  const excludedMonthName = excludedMonthKeys.length === 1
    ? MONTH_NAMES[parseInt(excludedMonthKeys[0].slice(5, 7), 10) - 1]
    : null;

  // How much is left to spend this month — same shape as the Savings screen's
  // month P&L: confirmed income + one-offs − recurring − planned savings − spent.
  const mk = monthKey(cycleKey);
  const monthInc = computeMonthIncome(state, mk).confirmed;
  const monthExtra = extraSpendTotal((state.extra || []).filter(x => monthKey(x.date) === mk));
  const monthFixed = (state.fixed || []).reduce((s, f) => s + monthlyEquiv(f), 0);
  const monthSpent = (state.expenses || []).filter(e => monthKey(e.date) === mk).reduce((s, e) => s + expenseNet(e), 0);
  const savingsTarget = state.savingsTarget || 0;
  const monthRemaining = monthInc + monthExtra - monthFixed - savingsTarget - monthSpent;

  // Group by tag for a warm breakdown.
  const byTag = {};
  spend.forEach(e => {
    const t = tagById(state.tags || [], e.tagId);
    const key = t.name;
    if (!byTag[key]) byTag[key] = { name: t.name, color: t.color, total: 0 };
    byTag[key].total += expenseNet(e);
  });
  const rows = Object.values(byTag).sort((a, b) => b.total - a.total);
  const max = rows.reduce((m, r) => Math.max(m, r.total), 0) || 1;

  return (
    <div className="meeting-content meeting-expenses">
      <div className="meeting-eyebrow">This week, together</div>
      <div className="meeting-expenses-total">{fmt(total, { cents: false })}</div>
      <div className="meeting-expenses-caption muted">
        across {spend.length} expense{spend.length === 1 ? '' : 's'}
      </div>

      <div className="meeting-expenses-month">
        <span className="meeting-expenses-month-num">{fmt(Math.abs(monthRemaining), { cents: false })}</span>
        <span className="meeting-expenses-month-label">
          {monthRemaining >= 0 ? 'left to spend this month' : 'over your plan this month'}
        </span>
      </div>

      <div className="meeting-expenses-breakdown">
        {rows.length === 0 && <p className="muted">Nothing logged this week.</p>}
        {rows.map(r => (
          <div className="meeting-expenses-row" key={r.name}>
            <div className="meeting-expenses-row-head">
              <span className="meeting-expenses-tag" style={{ background: TAG_COLORS[r.color] || TAG_COLORS.slate }} />
              <span className="meeting-expenses-name">{r.name}</span>
              <span className="num">{fmt(r.total, { cents: false })}</span>
            </div>
            <div className="meeting-expenses-bar">
              <div className="meeting-expenses-fill"
                   style={{ width: (r.total / max * 100).toFixed(1) + '%', background: TAG_COLORS[r.color] || TAG_COLORS.slate }} />
            </div>
          </div>
        ))}
      </div>

      {excluded.length > 0 && (
        <p className="meeting-expenses-closed-note">
          {excluded.length} {excludedMonthName ? excludedMonthName + ' ' : ''}
          expense{excluded.length === 1 ? " isn't" : "s aren't"} shown —{' '}
          {excludedMonthName ? excludedMonthName : 'that month'} is closed out.
        </p>
      )}

      <p className="meeting-expenses-sidedoor muted">
        Numbers look off? You can add anything missing after the meeting.
      </p>
      <MeetingDone label="Done" onClick={onDone} />
    </div>
  );
}

/* Screen 3 — the one screen with a verb. Allocate pool → goals; plus full
   Manage (deposit/withdraw/spend/purchase items) and Edit, reusing the same
   GoalManageSheet + GoalModal as the Savings screen, mounted above the ritual. */
function MeetingSavingsReveal({ state, setState, cycleKey, patchMeeting, onDone }) {
  // Weekly cadence → every meeting allocates. Monthly cadence → only the first
  // meeting of the month allocates; later ones show the lightweight hold card.
  const allocationWeek = getCheckInConfig(state).savingsCadence !== 'monthly' || isAllocationMeeting(state, cycleKey);
  const [activeGoal, setActiveGoal] = React.useState(null);
  const [amount, setAmount] = React.useState('');
  const [managingId, setManagingId] = React.useState(null);
  const [editingId, setEditingId] = React.useState(null);

  const goals = state.goals || [];
  const managingGoal = managingId ? goals.find(g => g.id === managingId) : null;
  const editingGoal = editingId ? goals.find(g => g.id === editingId) : null;

  // Deposit pool → goal, and record it as a meeting allocation for the archive.
  const depositAndRecord = (goalId, rawAmount, note) => {
    const amt = parseFloat(rawAmount);
    if (!amt || amt <= 0) return;
    setState(s => {
      const moved = Math.min(amt, s.pool);
      if (moved <= 0) return s;
      const after = applyGoalDeposit(s, goalId, moved, note);
      const goal = (s.goals || []).find(g => g.id === goalId);
      const goalName = goal ? goal.name : undefined;
      const alloc = { id: 'ca' + Date.now().toString(36), goalId, itemId: null, amount: moved, at: new Date().toISOString(), goalName };
      return { ...after, checkInMeetings: (after.checkInMeetings || []).map(m =>
        m.cycleKey === cycleKey ? { ...m, allocations: [...m.allocations, alloc] } : m) };
    });
  };
  const allocate = (goalId, rawAmount) => { depositAndRecord(goalId, rawAmount); setActiveGoal(null); setAmount(''); };

  // GoalManageSheet wiring — Add is a recorded deposit; pool/spent are plain
  // reversible moves that don't count as meeting allocations.
  const onAdjust = (goalId, mode, amt, note) => {
    if (mode === 'add') depositAndRecord(goalId, amt, note);
    else if (mode === 'pool') setState(s => applyGoalWithdraw(s, goalId, amt, note));
    else if (mode === 'spent') setState(s => applyGoalCashout(s, goalId, amt, note));
  };
  const onPurchaseItem = (goalId, itemId, reverse) => setState(s => applyItemPurchase(s, goalId, itemId, reverse));

  const zero = state.pool <= 0;

  // Monthly cadence off-week → lightweight card.
  if (!allocationWeek) {
    return (
      <div className="meeting-content meeting-savings">
        <div className="meeting-eyebrow">The pool</div>
        <div className="meeting-savings-hold-amount">{fmt(state.pool)}</div>
        <p className="meeting-savings-hold-note muted">Allocation happens at your first meeting next month. Nothing to do today.</p>
        <MeetingDone label="Done" onClick={onDone} />
      </div>
    );
  }

  return (
    <div className="meeting-content meeting-savings">
      <div className="meeting-eyebrow">The pool</div>
      <div className="meeting-savings-pool">{fmt(state.pool)}</div>
      {zero && <p className="meeting-savings-hold-note muted">Nothing to allocate this week — and that's completely fine.</p>}

      <div className="meeting-savings-goals">
        {goals.length === 0 && <p className="muted">No savings goals yet. You can add one on the Savings screen.</p>}
        {goals.map(g => {
          const pct = g.target > 0 ? Math.min(100, g.saved / g.target * 100) : 100;
          const isActive = activeGoal === g.id;
          return (
            <div className={`meeting-goal ${isActive ? 'active' : ''}`} key={g.id}>
              <div className="meeting-goal-head">
                <div className="meeting-goal-head-left">
                  <span className="meeting-goal-name">{g.name}</span>
                  {g.monthly > 0 && <span className="meeting-goal-monthly">{fmt(g.monthly)}/mo planned</span>}
                </div>
                <span className="muted num">{fmt(g.saved)}{g.target > 0 ? ' / ' + fmt(g.target) : ''}</span>
              </div>
              <div className="meeting-goal-bar"><div className="meeting-goal-fill" style={{ width: pct.toFixed(1) + '%' }} /></div>

              {!isActive && (
                <div className="meeting-goal-actions">
                  {!zero && (
                    <button className="meeting-goal-alloc" onClick={() => { setActiveGoal(g.id); setAmount(g.monthly > 0 ? String(g.monthly) : ''); }}>
                      Allocate{g.monthly > 0 ? ` ${fmt(g.monthly)}` : ''} →
                    </button>
                  )}
                  <button className="meeting-goal-2nd" onClick={() => setManagingId(g.id)}>Manage</button>
                  <button className="meeting-goal-2nd" onClick={() => setEditingId(g.id)}>Edit</button>
                </div>
              )}
              {isActive && (
                <div className="meeting-goal-amount">
                  <AmountInput value={amount} onChange={setAmount} autoFocus />
                  <button className="btn primary" onClick={() => allocate(g.id, amount)}>Add</button>
                  <button className="btn ghost" onClick={() => { setActiveGoal(null); setAmount(''); }}>Cancel</button>
                </div>
              )}
            </div>
          );
        })}
      </div>

      <MeetingDone label={zero ? 'Hold this week' : 'Done'} onClick={onDone} />

      {managingGoal && (
        <GoalManageSheet
          goal={managingGoal}
          pool={state.pool}
          onAdjust={(mode, amt, note) => onAdjust(managingGoal.id, mode, amt, note)}
          onPurchaseItem={(itemId, reverse) => onPurchaseItem(managingGoal.id, itemId, reverse)}
          onClose={() => setManagingId(null)} />
      )}
      {editingGoal && (
        <GoalModal state={state} setState={setState} editing={editingGoal} onClose={() => setEditingId(null)} />
      )}
    </div>
  );
}

/* Screen 4 — the shared board, for pointing and talking. Reuses the real
   BrainCard + masonry from the Commonplace wall (same as the Dashboard's
   pinned strip), filtered to family-shared notes instead of pinned ones.
   Fully interactive like the wall: tap a card to open/edit it, add a new
   card to the board, and right-click for the quick flag/archive radial. */
function MeetingCommonplaceReveal({ state, setState, brainOps, userId, patchMeeting, onDone }) {
  const bd = state.brainDump || { notes: [], tags: [], pinOrder: [] };
  const brainTags = bd.tags || [];
  // Shared board = family-visible notes to revisit together. Habits are
  // personal streak-tracking, not discussion items — leave them off the board.
  const notes = (bd.notes || []).filter(n => n.privacy === 'public' && !n.archived && n.type !== 'habit');
  const [expanded, setExpanded] = React.useState({});
  const toggleExpand = (id) => setExpanded(e => ({ ...e, [id]: !e[id] }));
  // Edit/create modal (null = closed; { editingId } where editingId is a note
  // id to edit or null to capture a new card) and the flag/archive radial —
  // both mounted inside the overlay, same as the Savings screen's GoalModal.
  const [noteModal, setNoteModal] = React.useState(null);
  const [radial, setRadial] = React.useState(null);

  // Snapshot human-readable labels AT THE MOMENT of the action, so the archive
  // stays legible after the note (or its checklist item) is later deleted.
  const recordAction = (noteId, itemId, action, extra) => {
    const note = (bd.notes || []).find(n => n.id === noteId);
    const noteTitle = note
      ? (note.title || (note.body || '').trim().slice(0, 60) || 'Untitled note')
      : undefined;
    const item = note && itemId ? (note.items || []).find(it => it.id === itemId) : null;
    const itemLabel = item ? item.text : undefined;
    patchMeeting(m => ({ commonplaceActions: [...m.commonplaceActions, {
      noteId, itemId: itemId || undefined, action, at: new Date().toISOString(),
      noteTitle, itemLabel, ...(extra || {}),
    }] }));
  };

  // Dual-write helpers, mirroring the brain wall / dashboard strip exactly.
  const updateNote = (id, updater) => {
    // Source the write from the table-backed copy (`state` is
    // effectiveState), not the blob, and write unconditionally so a note
    // missing from the blob array can't drop the edit. Self-heal the blob.
    const current = (state.brainDump?.notes || []).find(n => n.id === id);
    if (!current) return;
    const updated = updater(current);
    setState(s => {
      const b = s.brainDump || { notes: [], tags: [], pinOrder: [] };
      const exists = (b.notes || []).some(n => n.id === id);
      const notes = exists
        ? b.notes.map(n => n.id === id ? updated : n)
        : [updated, ...(b.notes || [])];
      return { ...s, brainDump: { ...b, notes } };
    });
    brainOps && brainOps.updateNote(id, updated);
  };
  // Quick flag/archive — same edits the Commonplace radial makes. Archiving
  // also unpins and drops the card off the board on the next render. Both record
  // a snapshotted action for the meeting archive.
  const setFlag = (id, flag) => {
    updateNote(id, n => ({ ...n, flag: flag || undefined }));
    recordAction(id, null, flag ? 'note-flagged' : 'note-unflagged', flag ? { flag } : undefined);
  };
  const setArchived = (id, archived) => {
    updateNote(id, n => ({ ...n, archived: archived || undefined, pinned: archived ? false : n.pinned }));
    recordAction(id, null, archived ? 'note-archived' : 'note-unarchived');
  };
  const toggleDone = (id) => {
    const note = (bd.notes || []).find(n => n.id === id);
    const newDone = !(note && note.done);
    updateNote(id, n => ({ ...n, done: !n.done }));
    recordAction(id, null, newDone ? 'note-done' : 'note-undone');
  };
  const toggleItem = (noteId, itemId) => {
    const note = (bd.notes || []).find(n => n.id === noteId);
    const item = note && (note.items || []).find(it => it.id === itemId);
    if (!item) return;
    const newDone = !item.done;
    setState(s => {
      const sd = s.brainDump || { notes: [], tags: [], pinOrder: [] };
      return { ...s, brainDump: { ...sd, notes: sd.notes.map(n => n.id === noteId
        ? { ...n, items: (n.items || []).map(it => it.id === itemId ? { ...it, done: newDone } : it) }
        : n) } };
    });
    brainOps && brainOps.updateItem(itemId, { done: newDone });
    recordAction(noteId, itemId, newDone ? 'item-done' : 'item-undone');
  };

  // Height-balanced masonry distribution (mirrors screen-brain-dump.jsx).
  const cols = notes.length > 6 ? 3 : 2;
  const distribute = (list, n) => {
    const c = Math.max(1, Math.min(n, list.length || 1));
    const out = Array.from({ length: c }, () => []);
    const hs = new Array(c).fill(0);
    list.forEach(note => {
      let mi = 0;
      for (let i = 1; i < c; i++) if (hs[i] < hs[mi]) mi = i;
      out[mi].push(note);
      hs[mi] += brainEstHeight(note, expanded[note.id]) + 18;
    });
    return out;
  };
  const columns = distribute(notes, cols);

  return (
    <div className="meeting-content meeting-commonplace">
      <div className="meeting-cp-head">
        <div className="meeting-eyebrow">Things you've been keeping</div>
        <button className="meeting-cp-add" onClick={() => setNoteModal({ editingId: null })}>
          <span className="meeting-cp-add-plus">+</span> Add a card
        </button>
      </div>

      {notes.length === 0 ? (
        <p className="meeting-cp-empty muted">Nothing shared on the board yet. Add a card, or mark notes as shared in Commonplace and they'll gather here.</p>
      ) : (
        <div className="brain-masonry meeting-cp-masonry">
          {columns.map((col, ci) => (
            <div key={ci} className="brain-masonry-col">
              {col.map(n => (
                <BrainCard
                  key={n.id}
                  note={n}
                  tags={brainTags}
                  query=""
                  setQuery={() => {}}
                  expanded={!!expanded[n.id]}
                  hidePin
                  onEdit={() => setNoteModal({ editingId: n.id })}
                  onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); setRadial({ x: e.clientX, y: e.clientY, noteId: n.id }); }}
                  onTogglePin={() => {}}
                  onToggleDone={() => toggleDone(n.id)}
                  onToggleItem={(itemId) => toggleItem(n.id, itemId)}
                  onToggleExpand={() => toggleExpand(n.id)}
                  onTagClick={() => {}}
                  onHabitAction={() => {}} />
              ))}
            </div>
          ))}
        </div>
      )}

      <MeetingDone label="Done" onClick={onDone} />

      {radial && (() => {
        const target = (state.brainDump?.notes || []).find(n => n.id === radial.noteId);
        return (
          <BrainCardRadial
            x={radial.x} y={radial.y}
            currentFlag={target?.flag || null}
            onPickFlag={(flag) => { setFlag(radial.noteId, flag); setRadial(null); }}
            onArchive={() => { setArchived(radial.noteId, true); setRadial(null); }}
            onClose={() => setRadial(null)} />
        );
      })()}

      {noteModal && (
        <BrainNoteModal
          state={state}
          setState={setState}
          editingId={noteModal.editingId}
          userId={userId}
          pendingDraft={null}
          brainOps={brainOps}
          initialPrivacy="public"
          onClose={() => setNoteModal(null)} />
      )}
    </div>
  );
}

/* Screen 5 — the exhale. Animated text, then dark.

   There used to be one more prompt here — "One thing to look forward to?",
   saved as the meeting's headline in the archive. It asked the ritual's
   fourth question a second time, and it titled the record of the week you'd
   just finished with something that hadn't happened yet, so the archive read
   a week out of step with itself. Meetings file under their date now.
   `outroNote` survives on old records; nothing writes or reads it. */
function MeetingOutro({ status, onFinish }) {
  const dayLabel = capDay(status.meetingDay);
  const line = `See you next ${dayLabel}.`;

  return (
    <div className="meeting-content meeting-outro">
      <div className="meeting-outro-line" aria-label={line}>
        {line.split('').map((ch, i) => (
          <span className="meeting-outro-letter" style={{ animationDelay: (i * 40) + 'ms' }} key={i}>
            {ch === ' ' ? ' ' : ch}
          </span>
        ))}
      </div>

      <button className="btn primary meeting-outro-close" onClick={onFinish}>
        Finish
      </button>
    </div>
  );
}

/* Shared forward-advance button — the only way through the ritual. */
function MeetingDone({ label, onClick }) {
  return (
    <div className="meeting-done-row">
      <button className="btn primary meeting-done" onClick={onClick}>{label}</button>
    </div>
  );
}

Object.assign(window, { MeetingRitual });
