// Phase 05 · LIVE the room.
//
// Three panes:
//   LEFT  · private — your coach pre-send pass, your private callouts from the pattern watcher,
//          your draft composer.
//   CENTER· public — the posted openings (from phase 03), the accepted steel-mans (phase 04),
//          and the live message thread.
//   RIGHT · public — established truths. Either party can propose; BOTH must confirm.
//
// State (live mode):
//   shared.posted = [{ id, from:'A'|'B'|'mediator'|'system', kind:'opening'|'steelman'|'msg'|'truth'|'sys', text, ts, ref? }]
//   shared.truths = [{ id, text, proposedBy:'A'|'B', confirmedA:bool, confirmedB:bool, ts }]
//
// Pre-existing shared.posted used a simpler shape. We only append; never rewrite.
// Messages: after the room-coach returns, user clicks send-original OR send-gentler.
// Post-send: pattern watcher runs in the background and may produce a private callout.

(function () {
  const { useState, useEffect, useRef } = React;

  window.LiveRoomPhase = function LiveRoomPhase({ seat, onPhase }) {
    const [, tick] = useState(0);
    useEffect(() => CG.session.onState(() => tick(t => t + 1)), []);

    const shared = CG.session.shared || {};
    const names = (CG.session.getNames && CG.session.getNames()) || {};
    const mp = CG.session.isMultiplayer && CG.session.isMultiplayer();
    const isHost = !mp || seat === 'A';
    const otherSeat = seat === 'A' ? 'B' : 'A';
    const myName = names[seat] || (seat === 'A' ? 'host' : 'guest');
    const otherName = names[otherSeat] || (otherSeat === 'A' ? 'host' : 'guest');

    // Prerequisite guard
    const myPosted = shared.openings?.[seat]?.step === 'posted';
    const theirPosted = shared.openings?.[otherSeat]?.step === 'posted';
    const steel = shared.steelman || {};
    const myDone = (steel[`${seat}_attempts`] || []).some(a => a.status === 'accepted');
    const theirDone = (steel[`${otherSeat}_attempts`] || []).some(a => a.status === 'accepted');
    if (!myPosted || !theirPosted || !myDone || !theirDone) {
      return (
        <div style={{ padding: 40, maxWidth: 720, margin: '0 auto' }}>
          <div className="tiny">phase 05 · the room</div>
          <h1 style={{ marginTop: 6 }}>not ready.</h1>
          <p style={{ color: 'var(--ink-2)', lineHeight: 1.55 }}>
            the steel-man gate must be cleared in both directions first.{' '}
            <a onClick={() => onPhase && onPhase('steelman')} style={{ color: 'var(--blue)', cursor: 'pointer' }}>← back</a>
          </p>
        </div>
      );
    }

    // Seed the room thread once: openings + accepted steelmans (as "you said" cards).
    const posted = shared.posted || [];
    const hasSeed = posted.some(p => p.kind === 'opening');
    useEffect(() => {
      if (hasSeed) return;
      if (!isHost) return; // host seeds deterministically
      const out = [];
      const openA = shared.openings?.A;
      const openB = shared.openings?.B;
      if (openA?.synthesis) {
        out.push({ id: 'op-a', from: 'mediator', kind: 'opening', text: openA.synthesis, attribution: 'A', ts: Date.now() });
      }
      if (openB?.synthesis) {
        out.push({ id: 'op-b', from: 'mediator', kind: 'opening', text: openB.synthesis, attribution: 'B', ts: Date.now() + 1 });
      }
      const accA = (steel.A_attempts || []).find(a => a.status === 'accepted');
      const accB = (steel.B_attempts || []).find(a => a.status === 'accepted');
      if (accA) out.push({ id: 'st-a', from: 'A', kind: 'steelman', text: accA.text, attribution: 'B', ts: Date.now() + 2 });
      if (accB) out.push({ id: 'st-b', from: 'B', kind: 'steelman', text: accB.text, attribution: 'A', ts: Date.now() + 3 });
      if (out.length) {
        CG.session.setShared({ posted: [...posted, ...out] });
      }
    }, [hasSeed, isHost]);

    // ----- phase 06 · callout state -----
    const callout = shared.callout || null;
    const [calloutError, setCalloutError] = useState('');
    const [calloutLoading, setCalloutLoading] = useState(false);
    const [calloutRefusal, setCalloutRefusal] = useState(null); // { reason } — shown to requester only

    const requestCallout = async () => {
      if (calloutLoading) return;
      if (callout && callout.status !== 'committed' && callout.status !== 'rejected') return;
      setCalloutError('');
      setCalloutRefusal(null);
      const id = `co-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
      const requesting = { id, requestedBy: seat, status: 'requesting', ackA: false, ackB: false, ts: Date.now() };
      if (isHost) CG.session.setShared({ callout: requesting });
      else CG.peer.send('state:propose', { callout: requesting });

      // Host runs the mediator call; if guest requested, we still want the host to execute,
      // but we can let the requester run it too (routed via session proxy anyway).
      setCalloutLoading(true);
      try {
        const reading = await CG.agents.patternCaller.call({ requestedBy: seat });
        if (!reading.found) {
          // do NOT post. clear the callout; show a private refusal note to the requester only.
          setCalloutRefusal({ reason: reading.not_yet_reason || 'not enough to name yet.' });
          if (isHost) CG.session.setShared({ callout: null });
          else CG.peer.send('state:propose', { callout: { id, status: 'rejected' } });
          return;
        }
        const proposed = { ...requesting, status: 'proposed', reading };
        if (isHost) CG.session.setShared({ callout: proposed });
        else {
          // Guest-initiated: guest runs the call, but only host's setShared is authoritative for
          // writing the full reading. Route a write through a dedicated path.
          CG.peer.send('state:propose', { __callout_propose: proposed });
        }
      } catch (e) {
        setCalloutError(String(e.message || e));
        if (isHost) CG.session.setShared({ callout: null });
        else CG.peer.send('state:propose', { callout: { id, status: 'rejected' } });
      } finally {
        setCalloutLoading(false);
      }
    };

    const ackCallout = () => {
      if (!callout || callout.status !== 'proposed') return;
      const field = seat === 'A' ? 'ackA' : 'ackB';
      const next = { ...callout, [field]: true };
      // If both acked, commit: append to posted and clear callout.
      const bothAcked = (field === 'ackA' ? true : callout.ackA) && (field === 'ackB' ? true : callout.ackB);
      if (bothAcked) {
        const entry = {
          id: `pat-${callout.id}`,
          from: 'mediator',
          kind: 'pattern',
          title: callout.reading.title,
          text: callout.reading.reading,
          anchor: callout.reading.anchor,
          requestedBy: callout.requestedBy,
          ts: Date.now(),
        };
        if (isHost) {
          CG.session.setShared({ callout: null, posted: [...(shared.posted || []), entry] });
        } else {
          CG.peer.send('state:propose', { __append_posted: entry, callout: null });
        }
      } else {
        if (isHost) CG.session.setShared({ callout: next });
        else CG.peer.send('state:propose', { callout: next });
      }
    };
    const rejectCallout = () => {
      if (!callout) return;
      const rejected = { ...callout, status: 'rejected' };
      if (isHost) CG.session.setShared({ callout: null });
      else CG.peer.send('state:propose', { callout: rejected });
    };

    // ----- left rail state -----
    const [draft, setDraft] = useState('');
    const [review, setReview] = useState(null); // { note, gentler_version, issues, ok_to_send }
    const [reviewing, setReviewing] = useState(false);
    const [reviewError, setReviewError] = useState('');
    const [sending, setSending] = useState(false);

    // private callouts, keyed per seat — stored in component state only (not shared)
    const [callouts, setCallouts] = useState([]);

    // persist draft locally
    useEffect(() => { (async () => {
      const s = await CG.store.get(`room_draft_${seat}`);
      if (s && typeof s === 'string') setDraft(s);
    })(); }, [seat]);
    useEffect(() => { CG.store.set(`room_draft_${seat}`, draft); }, [draft, seat]);

    const publicThread = posted.filter(p => p.kind === 'msg' || p.kind === 'opening' || p.kind === 'steelman');

    // Presence
    useEffect(() => {
      let activity = 'in the room';
      if (sending) activity = 'posting';
      else if (reviewing) activity = 'reviewing with coach';
      else if ((draft || '').trim().length > 0) activity = 'drafting';
      try { CG.session.setCursor && CG.session.setCursor({ activity }); } catch (e) {}
    }, [sending, reviewing, draft]);

    const runReview = async () => {
      const t = draft.trim();
      if (!t || reviewing) return;
      setReviewing(true);
      setReviewError('');
      setReview(null);
      try {
        const r = await CG.agents.roomCoach.review(seat, t, publicThread);
        setReview(r);
      } catch (e) {
        setReviewError(String(e.message || e));
      } finally {
        setReviewing(false);
      }
    };

    const sendPost = (text) => {
      if (!text || sending) return;
      setSending(true);
      const entry = { id: `m-${Date.now()}-${Math.random().toString(36).slice(2,6)}`, from: seat, kind: 'msg', text, ts: Date.now() };
      // posted is append-only; use the __append_posted mechanism on cg-session for guest
      if (isHost) {
        CG.session.setShared({ posted: [...(shared.posted || []), entry] });
      } else {
        // guest appends via dedicated protocol (supported by _applyGuestProposal)
        CG.peer.send('state:propose', { __append_posted: entry });
      }
      setDraft('');
      setReview(null);
      setSending(false);
      CG.store.set(`room_draft_${seat}`, '');

      // Run the pattern watcher in the background (private)
      (async () => {
        try {
          const c = await CG.agents.patternWatcher.watch(seat, [...publicThread, entry]);
          if (c) setCallouts(cs => [...cs, c]);
        } catch {}
      })();
    };

    // ----- establish-truth flow (right rail) -----
    const [truthDraft, setTruthDraft] = useState('');
    const truths = shared.truths || [];
    const proposeTruth = () => {
      const t = truthDraft.trim();
      if (!t) return;
      const entry = { id: `t-${Date.now()}-${Math.random().toString(36).slice(2,6)}`, text: t, proposedBy: seat, confirmedA: seat === 'A', confirmedB: seat === 'B', ts: Date.now() };
      const nextTruths = [...truths, entry];
      if (isHost) CG.session.setShared({ truths: nextTruths });
      else CG.peer.send('state:propose', { truths: nextTruths, __truth_append: entry });
      setTruthDraft('');
    };
    const confirmTruth = (id) => {
      const nextTruths = truths.map(t => t.id === id ? { ...t, [seat === 'A' ? 'confirmedA' : 'confirmedB']: true } : t);
      if (isHost) CG.session.setShared({ truths: nextTruths });
      else CG.peer.send('state:propose', { truths: nextTruths, __truth_confirm: { id, seat } });
    };

    return (
      <div style={{ height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
        {/* Header */}
        <div style={{ padding: '14px 24px 10px', borderBottom: '1px solid var(--rule)', flex: '0 0 auto' }}>
          <div className="tiny">phase 05 · the room · live</div>
          <h2 style={{ marginTop: 2, marginBottom: 0, fontFamily: 'var(--font-prose)', fontWeight: 500 }}>
            public record. your coach runs every message before it posts.
          </h2>
        </div>

        {/* Three panes */}
        <div style={{ display: 'grid', gridTemplateColumns: '340px 1fr 300px', flex: 1, minHeight: 0 }}>
          {/* LEFT */}
          <div style={{ borderRight: '1px solid var(--rule)', background: 'var(--paper-2)', overflow: 'auto', padding: 14 }}>
            <LeftPrivatePane
              seat={seat}
              draft={draft}
              setDraft={setDraft}
              review={review}
              setReview={setReview}
              reviewing={reviewing}
              reviewError={reviewError}
              onReview={runReview}
              onSendOriginal={() => sendPost(draft.trim())}
              onSendGentler={() => sendPost((review?.gentler_version || draft).trim())}
              callouts={callouts}
              dismissCallout={(i) => setCallouts(cs => cs.filter((_, k) => k !== i))}
              otherName={otherName}
              callout={callout}
              calloutLoading={calloutLoading}
              calloutError={calloutError}
              calloutRefusal={calloutRefusal}
              dismissRefusal={() => setCalloutRefusal(null)}
              onRequestCallout={requestCallout}
              seatSelf={seat}
            />
          </div>

          {/* CENTER */}
          <div style={{ overflow: 'auto', padding: '16px 22px', position: 'relative' }}>
            {callout && callout.status === 'proposed' && callout.reading && (
              <CalloutProposalCard
                callout={callout}
                seat={seat}
                names={names}
                onAck={ackCallout}
                onReject={rejectCallout}
              />
            )}
            {callout && callout.status === 'requesting' && (
              <div className="paper-card p-3 mb-3 fade-up" style={{ background: 'var(--paper-2)', borderLeft: '3px solid var(--ink-3)' }}>
                <div className="tiny mb-1">mediator · reading the room…</div>
                <div className="mono" style={{ fontSize: '0.78rem', color: 'var(--ink-3)' }}>
                  {callout.requestedBy === seat ? 'you asked for a pattern reading.' : `${callout.requestedBy === 'A' ? (names.A || 'host') : (names.B || 'guest')} asked for a pattern reading.`}
                </div>
              </div>
            )}
            <PublicThread posted={posted} names={names} myseat={seat} />
          </div>

          {/* RIGHT */}
          <div style={{ borderLeft: '1px solid var(--rule)', background: 'var(--paper-2)', overflow: 'auto', padding: 14 }}>
            <RightTruthsPane
              truths={truths}
              seat={seat}
              onConfirm={confirmTruth}
              truthDraft={truthDraft}
              setTruthDraft={setTruthDraft}
              onPropose={proposeTruth}
              onContinue={() => onPhase && onPhase('apology')}
            />
          </div>
        </div>
      </div>
    );
  };

  // -------- LEFT · private pane --------

  function LeftPrivatePane({ seat, draft, setDraft, review, setReview, reviewing, reviewError, onReview, onSendOriginal, onSendGentler, callouts, dismissCallout, otherName, callout, calloutLoading, calloutError, calloutRefusal, dismissRefusal, onRequestCallout, seatSelf }) {
    const canRequest = !calloutLoading && (!callout || callout.status === 'committed' || callout.status === 'rejected');
    const calloutBusy = callout && (callout.status === 'requesting' || callout.status === 'proposed');
    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
          <div className="tiny">you &amp; your coach</div>
          <AgentBadge agent={seat === 'B' ? 'coachB' : 'coachA'} />
        </div>

        {/* phase 06 · pattern callout trigger */}
        <div className="paper-card p-2 mb-3" style={{ background: 'var(--paper)', borderLeft: '2px dashed var(--ink-3)' }}>
          <div className="tiny mb-1">pattern callout · mediator</div>
          <div className="mono mb-2" style={{ fontSize: '0.7rem', color: 'var(--ink-3)', textTransform: 'none', letterSpacing: 0, lineHeight: 1.4 }}>
            stuck in a loop? ask the mediator to name the pattern. both of you decide whether to accept it.
          </div>
          <button
            className="btn btn-sm"
            disabled={!canRequest}
            onClick={onRequestCallout}
            style={{ width: '100%' }}
          >
            {calloutLoading ? 'reading the room…' : calloutBusy ? 'one already in progress' : 'call a pattern'}
          </button>
          {calloutError && (
            <div className="mono mt-2" style={{ fontSize: '0.7rem', color: 'var(--red)' }}>{calloutError}</div>
          )}
          {calloutRefusal && (
            <div className="paper-card p-2 mt-2" style={{ background: 'var(--paper-2)', borderLeft: '2px solid var(--ink-3)' }}>
              <div className="micro mb-1">mediator declined · private to you</div>
              <div style={{ fontSize: '0.8rem', lineHeight: 1.4 }}>{calloutRefusal.reason}</div>
              <button className="btn btn-sm btn-ghost mt-1" style={{ padding: '2px 6px', fontSize: '0.7rem' }} onClick={dismissRefusal}>dismiss</button>
            </div>
          )}
        </div>

        {/* pattern-watcher callouts */}
        {callouts.length > 0 && (
          <div style={{ marginBottom: 14 }}>
            {callouts.slice().reverse().map((c, i) => (
              <div key={c.ts} className="paper-card p-3 fade-up" style={{
                background: 'var(--paper)',
                borderLeft: `2px solid ${c.severity === 'high' ? 'var(--red)' : 'oklch(55% 0.14 85)'}`,
                marginBottom: 6,
                position: 'relative',
              }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
                  <div className="micro" style={{ color: c.severity === 'high' ? 'var(--red)' : 'oklch(45% 0.14 85)' }}>
                    pattern · {c.severity}
                  </div>
                  <button onClick={() => dismissCallout(callouts.length - 1 - i)} className="mono" style={{ background: 'none', border: 0, cursor: 'pointer', fontSize: '0.7rem', color: 'var(--ink-3)' }}>
                    dismiss
                  </button>
                </div>
                <div style={{ fontSize: '0.88rem', lineHeight: 1.45, marginBottom: 4 }}>{c.text}</div>
                <div className="mono" style={{ fontSize: '0.65rem', color: 'var(--ink-3)' }}>
                  references: "{c.ledger_ref}"
                </div>
              </div>
            ))}
          </div>
        )}

        <div className="tiny mb-2">draft · nothing's posted until you choose to send</div>
        <textarea
          className="paper-input"
          rows={5}
          placeholder={`say something to ${otherName}…`}
          value={draft}
          onChange={e => setDraft(e.target.value)}
          disabled={reviewing}
        />

        {!review && (
          <div style={{ display: 'flex', gap: 8, marginTop: 10, alignItems: 'center' }}>
            <button
              className="btn btn-sm btn-primary"
              disabled={!draft.trim() || reviewing}
              onClick={onReview}
            >{reviewing ? 'coach reading…' : 'coach · review before sending'}</button>
            <div style={{ flex: 1 }} />
            <span className="mono" style={{ fontSize: '0.7rem', color: 'var(--ink-3)' }}>
              {draft.trim().split(/\s+/).filter(Boolean).length} w
            </span>
          </div>
        )}

        {reviewError && (
          <div className="paper-card p-2 mt-3" style={{ background: 'var(--red-soft)', borderLeft: '2px solid var(--red)' }}>
            <div className="micro" style={{ color: 'var(--red)' }}>error</div>
            <div style={{ fontSize: '0.82rem' }}>{reviewError}</div>
          </div>
        )}

        {review && (
          <div className="fade-up" style={{ marginTop: 12 }}>
            {review.note && (
              <div className="paper-card p-2 mb-2" style={{ background: 'var(--paper)', borderLeft: '2px solid var(--ink-3)' }}>
                <div className="micro mb-1">coach</div>
                <div style={{ fontSize: '0.85rem', lineHeight: 1.4 }}>{review.note}</div>
              </div>
            )}

            {review.issues.length > 0 && (
              <div style={{ marginBottom: 10 }}>
                {review.issues.map((iss, i) => (
                  <div key={i} style={{ fontSize: '0.78rem', padding: '5px 8px', borderLeft: '2px solid oklch(55% 0.18 30)', background: 'var(--paper)', marginBottom: 4 }}>
                    <div className="mono" style={{ fontSize: '0.6rem', color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
                      {iss.kind}
                    </div>
                    <div style={{ fontStyle: 'italic' }}>"{iss.quote}"</div>
                    <div style={{ color: 'var(--ink-2)' }}>{iss.reason}</div>
                  </div>
                ))}
              </div>
            )}

            <div className="tiny mb-1">gentler version</div>
            <div className="paper-card p-2 mb-2" style={{ background: 'var(--paper)', borderLeft: `2px dashed var(--blue)` }}>
              <div style={{ fontSize: '0.88rem', lineHeight: 1.5, fontStyle: review.gentler_version === draft.trim() ? 'normal' : 'italic' }}>
                {review.gentler_version}
              </div>
              {review.gentler_version === draft.trim() && (
                <div className="mono mt-1" style={{ fontSize: '0.65rem', color: 'var(--ink-3)' }}>
                  (your original is already clean.)
                </div>
              )}
            </div>

            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {review.gentler_version !== draft.trim() && (
                <button className="btn btn-sm btn-primary" onClick={onSendGentler}>
                  send gentler version
                </button>
              )}
              <button className="btn btn-sm" onClick={onSendOriginal}>
                send original as-is
              </button>
              <button className="btn btn-sm btn-ghost" onClick={() => setReview(null)}>
                keep editing
              </button>
            </div>
          </div>
        )}
      </div>
    );
  }

  // -------- CENTER · public thread --------

  function PublicThread({ posted, names, myseat }) {
    const entries = posted.slice().sort((a, b) => a.ts - b.ts);
    return (
      <div>
        <div className="tiny mb-2">public record · everyone sees this exactly</div>
        {entries.length === 0 && (
          <div className="mono" style={{ color: 'var(--ink-3)', fontSize: '0.8rem' }}>thread is empty.</div>
        )}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {entries.map(e => <PostedEntry key={e.id} entry={e} names={names} myseat={myseat} />)}
        </div>
      </div>
    );
  }

  function PostedEntry({ entry, names, myseat }) {
    const fromName = entry.from === 'mediator' ? 'mediator'
                    : entry.from === 'A' ? (names.A || 'host')
                    : entry.from === 'B' ? (names.B || 'guest')
                    : entry.from;
    const accent = entry.from === 'A' ? '#b94b4b' : entry.from === 'B' ? '#3a78b8' : 'var(--ink-3)';
    const isMine = entry.from === myseat;

    if (entry.kind === 'opening') {
      const who = entry.attribution === 'A' ? (names.A || 'host') : (names.B || 'guest');
      return (
        <div className="paper-card p-3" style={{ background: 'var(--paper-2)', borderLeft: '3px solid var(--blue)' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
            <div className="tiny">opening · {who}</div>
            <AgentBadge agent="mediator" />
          </div>
          <div style={{ fontSize: '0.95rem', lineHeight: 1.55, whiteSpace: 'pre-wrap' }}>{entry.text}</div>
        </div>
      );
    }
    if (entry.kind === 'steelman') {
      const author = entry.from === 'A' ? (names.A || 'host') : (names.B || 'guest');
      const about = entry.attribution === 'A' ? (names.A || 'host') : (names.B || 'guest');
      return (
        <div className="paper-card p-3" style={{ background: 'var(--paper-2)', borderLeft: '3px solid var(--green)' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
            <div className="tiny"><b>{author}</b> restated <b>{about}</b> · accepted</div>
            <span className="stamp stamp-green" style={{ fontSize: '0.58rem' }}>heard</span>
          </div>
          <div style={{ fontSize: '0.92rem', lineHeight: 1.55, whiteSpace: 'pre-wrap', fontStyle: 'italic' }}>{entry.text}</div>
        </div>
      );
    }

    if (entry.kind === 'pattern') {
      const by = entry.requestedBy === 'A' ? (names.A || 'host') : (names.B || 'guest');
      return (
        <div className="paper-card p-3" style={{ background: 'oklch(98% 0.015 85)', borderLeft: '3px solid oklch(62% 0.14 85)', position: 'relative' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6 }}>
            <div className="tiny">pattern · called by {by} · both accepted</div>
            <AgentBadge agent="mediator" />
          </div>
          <div style={{ fontSize: '1.05rem', fontFamily: 'var(--font-prose)', fontWeight: 500, marginBottom: 6, letterSpacing: '-0.01em' }}>
            {entry.title || '(untitled)'}
          </div>
          <div style={{ fontSize: '0.92rem', lineHeight: 1.55, whiteSpace: 'pre-wrap', marginBottom: 8 }}>{entry.text}</div>
          {entry.anchor && (
            <div style={{ fontSize: '0.88rem', lineHeight: 1.45, fontStyle: 'italic', color: 'var(--ink-2)', borderTop: '1px dashed var(--rule)', paddingTop: 8 }}>
              → {entry.anchor}
            </div>
          )}
        </div>
      );
    }

    // kind === 'msg'
    return (
      <div style={{
        padding: '10px 14px',
        borderLeft: `3px solid ${accent}`,
        background: isMine ? 'var(--paper)' : 'transparent',
      }}>
        <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 4 }}>
          <div className="mono" style={{ color: accent, fontWeight: 600, fontSize: '0.8rem' }}>{fromName}</div>
          <div className="mono" style={{ fontSize: '0.68rem', color: 'var(--ink-4)' }}>{formatTs(entry.ts)}</div>
        </div>
        <div style={{ fontSize: '0.95rem', lineHeight: 1.55, whiteSpace: 'pre-wrap' }}>{entry.text}</div>
      </div>
    );
  }

  function formatTs(ts) {
    try {
      const d = new Date(ts);
      return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    } catch { return ''; }
  }

  // -------- callout · proposal overlay --------

  function CalloutProposalCard({ callout, seat, names, onAck, onReject }) {
    const r = callout.reading || {};
    const myAck = seat === 'A' ? callout.ackA : callout.ackB;
    const theirAck = seat === 'A' ? callout.ackB : callout.ackA;
    const theirName = seat === 'A' ? (names.B || 'guest') : (names.A || 'host');
    const requester = callout.requestedBy === 'A' ? (names.A || 'host') : (names.B || 'guest');
    return (
      <div className="paper-card fade-up" style={{
        background: 'oklch(98% 0.02 85)',
        borderLeft: '3px solid oklch(62% 0.14 85)',
        marginBottom: 14,
        padding: '14px 16px',
        position: 'relative',
      }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
          <div className="tiny">mediator · proposed pattern reading</div>
          <div className="mono" style={{ fontSize: '0.68rem', color: 'var(--ink-3)' }}>
            requested by {requester}
          </div>
        </div>
        <div style={{ fontSize: '1.1rem', fontFamily: 'var(--font-prose)', fontWeight: 500, marginBottom: 8, letterSpacing: '-0.01em' }}>
          {r.title || '(untitled)'}
        </div>
        <div style={{ fontSize: '0.94rem', lineHeight: 1.55, whiteSpace: 'pre-wrap', marginBottom: 10 }}>
          {r.reading}
        </div>
        {r.anchor && (
          <div style={{ fontSize: '0.9rem', lineHeight: 1.45, fontStyle: 'italic', color: 'var(--ink-2)', borderTop: '1px dashed var(--rule)', paddingTop: 10, marginBottom: 12 }}>
            → {r.anchor}
          </div>
        )}
        <div className="mono" style={{ fontSize: '0.7rem', color: 'var(--ink-3)', marginBottom: 10, textTransform: 'none', letterSpacing: 0 }}>
          both of you have to accept for this to post. declining makes it disappear.
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          {!myAck ? (
            <>
              <button className="btn btn-sm btn-primary" onClick={onAck}>accept · post to thread</button>
              <button className="btn btn-sm" onClick={onReject}>decline</button>
            </>
          ) : (
            <div className="mono" style={{ fontSize: '0.75rem', color: 'var(--green)' }}>
              you accepted · waiting for {theirName}
            </div>
          )}
          <div style={{ flex: 1 }} />
          <div className="mono" style={{ fontSize: '0.7rem', color: 'var(--ink-3)' }}>
            {callout.ackA ? (names.A || 'host') + ' ✓' : (names.A || 'host') + ' ·'}
            {' '}
            {callout.ackB ? (names.B || 'guest') + ' ✓' : (names.B || 'guest') + ' ·'}
          </div>
        </div>
      </div>
    );
  }

  // -------- RIGHT · truths pane --------

  function RightTruthsPane({ truths, seat, onConfirm, truthDraft, setTruthDraft, onPropose, onContinue }) {
    const established = truths.filter(t => t.confirmedA && t.confirmedB);
    const pending = truths.filter(t => !(t.confirmedA && t.confirmedB));
    return (
      <div>
        <div className="tiny mb-2">established truths</div>
        <div className="micro mb-3" style={{ textTransform: 'none', letterSpacing: 0, color: 'var(--ink-3)', lineHeight: 1.4 }}>
          the anchor. both of you have to confirm. the mediator can propose, but cannot impose.
        </div>

        {established.length === 0 && (
          <div className="mono" style={{ fontSize: '0.72rem', color: 'var(--ink-4)' }}>nothing yet.</div>
        )}
        {established.map(t => (
          <div key={t.id} className="paper-card p-2 mb-2" style={{ background: 'var(--paper)', borderLeft: '2px solid var(--green)' }}>
            <div style={{ fontSize: '0.88rem', lineHeight: 1.45 }}>
              <span className="highlight">{t.text}</span>
            </div>
            <div className="mono mt-1" style={{ fontSize: '0.62rem', color: 'var(--ink-3)' }}>✓ both confirmed</div>
          </div>
        ))}

        {pending.length > 0 && (
          <>
            <div className="tiny mt-3 mb-2">pending · awaiting confirm</div>
            {pending.map(t => {
              const me = seat === 'A' ? t.confirmedA : t.confirmedB;
              const them = seat === 'A' ? t.confirmedB : t.confirmedA;
              return (
                <div key={t.id} className="paper-card p-2 mb-2" style={{ background: 'var(--paper)' }}>
                  <div style={{ fontSize: '0.86rem', lineHeight: 1.45 }}>{t.text}</div>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 6 }}>
                    <div className="mono" style={{ fontSize: '0.65rem', color: 'var(--ink-3)' }}>
                      {me ? 'you ✓' : '·'} {them ? '· partner ✓' : ''}
                    </div>
                    {!me && (
                      <button className="btn btn-sm btn-primary" onClick={() => onConfirm(t.id)}>confirm</button>
                    )}
                  </div>
                </div>
              );
            })}
          </>
        )}

        <div className="dashed" style={{ margin: '14px 0' }} />

        <div className="tiny mb-2">propose a new truth</div>
        <textarea
          className="paper-input"
          rows={2}
          placeholder="something both of you would say yes to."
          value={truthDraft}
          onChange={e => setTruthDraft(e.target.value)}
        />
        <button className="btn btn-sm mt-2" disabled={!truthDraft.trim()} onClick={onPropose}>
          propose
        </button>

        <div className="dashed" style={{ margin: '18px 0 10px' }} />

        <div className="tiny mb-2">continue</div>
        <button className="btn btn-sm btn-primary" disabled={established.length === 0} onClick={onContinue}>
          → phase 07 · apology on-ramp <Icons.Arrow size={10} />
        </button>
        {established.length === 0 && (
          <div className="mono mt-2" style={{ fontSize: '0.68rem', color: 'var(--ink-3)' }}>
            establish at least one truth before moving on.
          </div>
        )}
      </div>
    );
  }

  console.log('[phases-room-live] loaded');
})();
