@@ -287,6 +287,22 @@ export function ReportView({ state, verdict, reportSections }: ReportViewProps)
287287 const bId = pickedBId ?? defaultBId ;
288288 const isNActor = state . actorIds . length > 2 ;
289289
290+ // Pair-focus toggle. Default OFF for cohort runs (3+ actors) so the
291+ // user lands on the all-actor turn-by-turn view first instead of a
292+ // pair-focused strip/sparkline/trajectory block that hides 6 of the
293+ // 8 actors in their swarm. Pair-mode sections are visually present
294+ // even when toggle is OFF — they collapse into a "focus pair" CTA
295+ // panel that explains why the strip/sparklines are hidden and offers
296+ // a one-click reveal. For 2-actor pair runs the toggle isn't shown
297+ // and the legacy pair view renders unchanged.
298+ const [ isPairFocus , setIsPairFocus ] = useState < boolean > ( ! isNActor ) ;
299+ // Keep the toggle in sync if the run swaps from a pair to a cohort
300+ // (or vice versa) mid-session — without this, switching scenarios
301+ // can leave the toggle stuck in the wrong default state.
302+ useEffect ( ( ) => {
303+ setIsPairFocus ( ! isNActor ) ;
304+ } , [ isNActor ] ) ;
305+
290306 // Per-actor turn map. One entry per actor, keyed by actor id. The
291307 // pair-mode `turns` derivation below picks two of these for the
292308 // strip/sparklines/trajectory; the N-actor turn-by-turn view
@@ -541,54 +557,77 @@ export function ReportView({ state, verdict, reportSections }: ReportViewProps)
541557 N actors via the horizontally-scrolling track — the picker
542558 does NOT scope it. */ }
543559 { isNActor && (
544- < div className = { styles . actorPairPicker } role = "region" aria-label = "Pair focus for strip + sparklines + trajectory" >
545- < span className = { styles . actorPairPickerLabel } > Focus pair</ span >
546- < select
547- aria-label = "Left side actor"
548- className = { styles . actorPairPickerSelect }
549- value = { aId ?? '' }
550- onChange = { ( e ) => setPickedAId ( e . target . value || null ) }
551- >
552- { state . actorIds . map ( id => (
553- < option key = { id } value = { id } disabled = { id === bId } >
554- { state . actors [ id ] ?. leader ?. name ?? id }
555- </ option >
556- ) ) }
557- </ select >
558- < span className = { styles . actorPairPickerVs } > vs</ span >
559- < select
560- aria-label = "Right side actor"
561- className = { styles . actorPairPickerSelect }
562- value = { bId ?? '' }
563- onChange = { ( e ) => setPickedBId ( e . target . value || null ) }
564- >
565- { state . actorIds . map ( id => (
566- < option key = { id } value = { id } disabled = { id === aId } >
567- { state . actors [ id ] ?. leader ?. name ?? id }
568- </ option >
569- ) ) }
570- </ select >
571- < span className = { styles . actorPairPickerHint } >
572- applies to strip + metrics + trajectory only · turn-by-turn
573- below shows all { state . actorIds . length } actors
574- </ span >
560+ < div className = { styles . actorPairPicker } role = "region" aria-label = "Pair focus toggle" >
561+ < label className = { styles . actorPairPickerToggle } >
562+ < input
563+ type = "checkbox"
564+ checked = { isPairFocus }
565+ onChange = { ( e ) => setIsPairFocus ( e . target . checked ) }
566+ aria-label = "Toggle pair-focus view for strip, sparklines, and trajectory"
567+ />
568+ < span className = { styles . actorPairPickerToggleText } > Focus pair</ span >
569+ </ label >
570+ { isPairFocus ? (
571+ < >
572+ < select
573+ aria-label = "Left side actor"
574+ className = { styles . actorPairPickerSelect }
575+ value = { aId ?? '' }
576+ onChange = { ( e ) => setPickedAId ( e . target . value || null ) }
577+ >
578+ { state . actorIds . map ( id => (
579+ < option key = { id } value = { id } disabled = { id === bId } >
580+ { state . actors [ id ] ?. leader ?. name ?? id }
581+ </ option >
582+ ) ) }
583+ </ select >
584+ < span className = { styles . actorPairPickerVs } > vs</ span >
585+ < select
586+ aria-label = "Right side actor"
587+ className = { styles . actorPairPickerSelect }
588+ value = { bId ?? '' }
589+ onChange = { ( e ) => setPickedBId ( e . target . value || null ) }
590+ >
591+ { state . actorIds . map ( id => (
592+ < option key = { id } value = { id } disabled = { id === aId } >
593+ { state . actors [ id ] ?. leader ?. name ?? id }
594+ </ option >
595+ ) ) }
596+ </ select >
597+ < span className = { styles . actorPairPickerHint } >
598+ strip + metrics + trajectory show this pair only · turn-by-turn below shows all { state . actorIds . length } actors
599+ </ span >
600+ </ >
601+ ) : (
602+ < span className = { styles . actorPairPickerHint } >
603+ Showing all { state . actorIds . length } actors. Toggle on to focus on a 2-actor head-to-head view in the strip, sparklines, and trajectory cards.
604+ </ span >
605+ ) }
575606 </ div >
576607 ) }
577608
578- < section id = "strip" >
579- < RunStrip turns = { stripCells } leaderAName = { nameA } leaderBName = { nameB } />
580- </ section >
609+ { ( ! isNActor || isPairFocus ) && (
610+ < section id = "strip" >
611+ < RunStrip turns = { stripCells } leaderAName = { nameA } leaderBName = { nameB } />
612+ </ section >
613+ ) }
581614
582- < section id = "sparklines" >
583- < MetricSparklines metrics = { metricSeries } leaderAName = { nameA } leaderBName = { nameB } />
584- </ section >
615+ { ( ! isNActor || isPairFocus ) && (
616+ < section id = "sparklines" >
617+ < MetricSparklines metrics = { metricSeries } leaderAName = { nameA } leaderBName = { nameB } />
618+ </ section >
619+ ) }
585620
586621 { /* Commander personality arcs. Shown once per side once there's at
587622 least one turn of drift data, so the user can visually inspect
588623 how each commander's HEXACO evolved across the run. Data comes
589624 from drift SSE events emitted after every turn. */ }
625+ { /* Trajectory cards: pair-mode by default. When the cohort toggle
626+ is off (default for cohort runs) every actor's drift arc gets a
627+ dedicated card so the user can compare HEXACO evolution across
628+ the full swarm. */ }
590629 < section id = "trajectory" >
591- { hasTrajectories && (
630+ { hasTrajectories && isPairFocus && (
592631 < div className = { `responsive-grid-2 ${ styles . trajectoryGrid } ` } >
593632 < CommanderTrajectoryCard
594633 events = { sideA ?. events ?? [ ] }
@@ -602,6 +641,22 @@ export function ReportView({ state, verdict, reportSections }: ReportViewProps)
602641 />
603642 </ div >
604643 ) }
644+ { hasTrajectories && ! isPairFocus && (
645+ < div className = { `responsive-grid-2 ${ styles . trajectoryGrid } ` } >
646+ { state . actorIds . map ( id => {
647+ const side = state . actors [ id ] ;
648+ if ( ! side ?. events . length ) return null ;
649+ return (
650+ < CommanderTrajectoryCard
651+ key = { id }
652+ events = { side . events }
653+ actorName = { side . leader ?. name || id }
654+ baselineHexaco = { side . leader ?. hexaco }
655+ />
656+ ) ;
657+ } ) }
658+ </ div >
659+ ) }
605660 </ section >
606661
607662 { /* Cost breakdown trigger. Moved out of the StatsBar header when
0 commit comments