// The main room — disagreement map + rebuttal + established truths // Depends on: SCENARIO, Icons, ScribbleCheck, InkLine, AgentBadge, Thermometer function MapCard({ item, onDrag, compact, showSource }) { const S = window.SCENARIO; const isAgreed = item.bucket === 'agreed'; const pinColor = item.pin || 'red'; const rot = `rot-${(item.id.charCodeAt(0) % 4) + 1}`; return (
{pinColor && } {isAgreed && (
)}
{isAgreed ? {item.text} : item.text}
raised by{' '} {item.raisedBy === 'mediator' ? ( mediator ) : item.raisedBy === 'A' ? ( maya ) : ( jordan )} {item.confirmedBy && ( confirmed · {item.confirmedBy === 'both' ? 'both' : item.confirmedBy.toLowerCase()} )}
); } function DisagreementMap({ godView }) { const S = window.SCENARIO; const buckets = [ { id: 'agreed', label: 'established · agreed', desc: 'truths both parties have confirmed. the anchor.', color: 'var(--green)', bg: 'oklch(95% 0.04 140)' }, { id: 'contested_facts', label: 'contested · facts', desc: 'disputes with a knowable answer.', color: 'var(--red)', bg: 'oklch(96% 0.03 30)' }, { id: 'contested_interp', label: 'contested · meaning', desc: 'same facts, different readings. this is where the real work is.', color: 'oklch(55% 0.12 85)', bg: 'oklch(96% 0.035 85)' }, { id: 'needs_info', label: 'needs more info', desc: 'we don\'t have the answer yet.', color: 'var(--blue)', bg: 'oklch(95% 0.03 240)' }, ]; return (
right rail · disagreement map · live
every claim pinned. either of you can drag between columns.
{buckets.map((b, bi) => (
{b.label}
{b.desc}
{S.mapItems.filter(m => m.bucket === b.id).map(item => ( ))} {b.id === 'agreed' && (
4 so far ✓
)}
))}
); } function RebuttalThread({ seat, godView, onTempChange }) { const S = window.SCENARIO; const [highlighted, setHighlighted] = React.useState(null); const renderMsg = (c) => { const isCallout = c.type === 'callout'; const isA = c.by === 'A'; const isB = c.by === 'B'; const isMediator = c.by === 'mediator'; const party = isA ? S.parties.A : isB ? S.parties.B : null; if (isCallout) { return (
{c.severity === 'yellow' ? '!' : '‼'}
{c.severity} card · {c.patterns.join(' + ')}
{c.text}
→ evidence cited · "{S.patterns.find(p => p.eventId === c.id)?.evidence || '…'}"
); } return (
setHighlighted(c.ref)} onMouseLeave={() => setHighlighted(null)} >
{isMediator ? ( ) : ( <>
{party.name.toLowerCase()} )}
{c.ref && ( → map · {c.ref} )}
{c.text}
); }; return (
center · rebuttal round · turn-tokened
free-form chat is banned. every message attaches to a map item or a specific claim.
jordan's turn
{S.chat.map(renderMsg)}
); } function LeftRail({ seat, godView }) { const S = window.SCENARIO; const [draft, setDraft] = React.useState("you always make me feel like"); const party = seat === 'B' ? S.parties.B : S.parties.A; const coachAgent = seat === 'B' ? 'coachB' : 'coachA'; if (seat === 'god') { return (
god view · both coach threads
maya's coach · last
"the word 'always' is doing a lot of work. before you send — is it literally true here?"
jordan's coach · last
"she's about to be confronted about apr 9. the vague version won't hold. do you want to preempt it?"
mediator's queue (pending approval)
summary_for_mediator · from coachB · awaiting jordan's sign-off
"b acknowledges the apr 9 change was less than 1hr notice and the reason given was incomplete."
); } return (
left rail · just you & your coach
coach · latest
{seat === 'B' ? "she's about to be confronted about apr 9. the vague version won't hold. do you want to preempt it?" : "the word 'always' is doing a lot of work. before you send — is it literally true here?"}
draft composer