// Phase 07 — Apology on-ramp. Private editable draft, live coach flags, real Claude feedback. // Quiet, narrow, ritual. console.log('[phases-apology] loading'); function ApologyPhase({ seat, godView, onPhase }) { const S = window.SCENARIO; const isJordan = seat === 'B'; const isMaya = seat === 'A'; const isGod = seat === 'god'; // Receiver view: maya doesn't see jordan's draft. We show a waiting panel. if (isMaya) return ; // Jordan (seat B) or God view — show the drafting runway. return ; } // ———————————————————————————————————————————————————————————————————————————— // Receiver (Maya) — private, "something is happening, you'll see it if/when it's sent" // ———————————————————————————————————————————————————————————————————————————— function ApologyReceiverView({ onPhase }) { const S = window.SCENARIO; const [dots, setDots] = React.useState(1); React.useEffect(() => { const t = setInterval(() => setDots(d => (d % 3) + 1), 700); return () => clearInterval(t); }, []); return (
phase 07 · apology on-ramp · your view · maya

the runway is private. this is what that looks like from your side.

jordan's coach noticed a softening signal and offered to help them draft something. the content is firewalled. you'll see it only if jordan chooses to send it into the room — on their time, not yours.

firewalled · content private
jordan is drafting something{'.'.repeat(dots)}
you will not see what they're writing, how long they take, or how many drafts. this runway belongs to jordan. if and when they hit send, the final version lands in the center and becomes public record.
your options while you wait
  • · keep reviewing the map / established truths on your side
  • · talk to your coach privately — it's ok not to wait
  • · park the room entirely — come back tomorrow
your coach · private
"you're not owed this. if it lands, you can take any piece of it into your acknowledgment column — or none of it. the apology is theirs to write; what you do with it is yours."
want to see what's happening on jordan's side right now? switch to their seat — or see both at once in god view.
); } // ———————————————————————————————————————————————————————————————————————————— // Drafter (Jordan) — the real editor // ———————————————————————————————————————————————————————————————————————————— // Stages: 'offer' → user accepts coach offer to draft → 'editing' → user iterates → 'confirm' → modal → 'sending' → animation → 'landed' function ApologyDraftingView({ seat, godView, onPhase, showBothSeats }) { const S = window.SCENARIO; const [stage, setStage] = React.useState(() => localStorage.getItem('cg_apology_stage') || 'offer'); const [draft, setDraft] = React.useState(() => localStorage.getItem('cg_apology_draft') || S.apologyDraftB.rough); const [flags, setFlags] = React.useState([]); const [flagsLoading, setFlagsLoading] = React.useState(false); const [deepFeedback, setDeepFeedback] = React.useState(null); const [deepLoading, setDeepLoading] = React.useState(false); const [showConfirm, setShowConfirm] = React.useState(false); const [landedAt, setLandedAt] = React.useState(() => localStorage.getItem('cg_apology_landed') || null); React.useEffect(() => { localStorage.setItem('cg_apology_stage', stage); }, [stage]); React.useEffect(() => { localStorage.setItem('cg_apology_draft', draft); }, [draft]); React.useEffect(() => { if (landedAt) localStorage.setItem('cg_apology_landed', landedAt); }, [landedAt]); // Live coach flags — debounced, calls claude with a tight prompt React.useEffect(() => { if (stage !== 'editing' || !draft.trim()) { setFlags([]); return; } const t = setTimeout(async () => { setFlagsLoading(true); const res = await callCoachFlags(draft); setFlags(res); setFlagsLoading(false); }, 900); return () => clearTimeout(t); }, [draft, stage]); const resetAll = () => { localStorage.removeItem('cg_apology_stage'); localStorage.removeItem('cg_apology_draft'); localStorage.removeItem('cg_apology_landed'); setStage('offer'); setDraft(S.apologyDraftB.rough); setFlags([]); setDeepFeedback(null); setLandedAt(null); }; const onAskDeep = async () => { setDeepLoading(true); setDeepFeedback(null); const res = await callDeepFeedback(draft); setDeepFeedback(res); setDeepLoading(false); }; const onSend = () => setShowConfirm(true); const onConfirmSend = () => { setShowConfirm(false); setStage('sending'); setTimeout(() => { const ts = new Date(); const stamp = `apr 15 · ${ts.getHours() % 12 || 12}:${String(ts.getMinutes()).padStart(2,'0')}${ts.getHours() >= 12 ? 'pm' : 'am'}`; setLandedAt(stamp); setStage('landed'); }, 1800); }; return (
{showBothSeats ? (
) : ( )}
); } // —— header —— function ApologyHeader({ seat, showBothSeats, onReset, hasProgress }) { return (
phase 07 · apology on-ramp · private · {showBothSeats ? 'god view · both seats' : 'your view · jordan'}

no apologies with a gun to the head. the runway is yours.

write freely. your coach reads along and flags the patterns that turn apologies into their opposite. nothing lands until you hit send — and then it's permanent.

{hasProgress && ( )}
); } // —— drafter (main column) —— function ApologyDrafter(props) { const { stage } = props; if (stage === 'offer') return ; if (stage === 'landed') return ; // 'editing' | 'sending' return ; } // —— stage: offer (before user accepts to draft) —— function ApologyOfferCard({ setStage, setDraft }) { const S = window.SCENARIO; return (
softening signal · your coach noticed
you · chat c9 · earlier
"i can sit with the pattern read. the recent three — yeah. they've been closer together than usual."
private · only you see this
you just did something a lot of people can't do. you named a pattern you're inside of.
if you want, i can help you draft something specific — not a performance, not a sandwich. just the thing that's true. you'd keep full edit control. maya sees nothing until you decide.
the "check on me later" option is real. if you're not ready, the coach backs off and re-surfaces the offer at a softer moment — never in front of maya, never with a timer.
); } // —— stage: editing —— function ApologyEditor({ draft, setDraft, flags, flagsLoading, deepFeedback, deepLoading, onAskDeep, showConfirm, setShowConfirm, onSend, onConfirmSend, stage, godView }) { const S = window.SCENARIO; const taRef = React.useRef(null); const [lastSaved, setLastSaved] = React.useState(null); React.useEffect(() => { const t = setTimeout(() => setLastSaved(new Date()), 500); return () => clearTimeout(t); }, [draft]); const wordCount = draft.trim().split(/\s+/).filter(Boolean).length; const checklist = buildChecklist(draft, flags); const passRate = checklist.filter(c => c.pass).length; const readyToSend = passRate >= 3 && flags.filter(f => f.severity === 'red').length === 0 && wordCount >= 8; const sending = stage === 'sending'; return (
your draft · private
{wordCount} {wordCount === 1 ? 'word' : 'words'} · saved