feat: implement interactive pedagogical reasoning with compact tooltips

- Add DecompositionWithReasons component with interactive term groups
- Implement current step highlighting with amber glow animation
- Create compact tooltips with step-by-step breakdowns and expansion reasoning
- Remove visual noise: no default term styling, only current step and group hover
- Add unified hover targets spanning entire term groups with reasoning context
- Integrate with tutorial context for synchronized step progression

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-25 15:01:47 -05:00
parent 704a8a8228
commit 2c095162e8
5 changed files with 920 additions and 7 deletions

View File

@@ -26,6 +26,7 @@
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",

View File

@@ -0,0 +1,323 @@
'use client'
import React, { useMemo, useState, createContext, useContext } from 'react'
import * as Tooltip from '@radix-ui/react-tooltip'
import { ReasonTooltip } from './ReasonTooltip'
import type { UnifiedStepData } from '../../utils/unifiedStepGenerator'
import { useTutorialContext } from './TutorialContext'
import './decomposition-reasoning.css'
export type PedagogicalRule = 'Direct' | 'FiveComplement' | 'TenComplement' | 'Cascade'
export interface SegmentDecision {
rule: PedagogicalRule
conditions: string[]
explanation: string[]
}
export interface PedagogicalSegment {
id: string
place: number
digit: number
a: number
L: number
U: 0 | 1
goal: string
plan: SegmentDecision[]
expression: string
termIndices: number[]
termRange: { startIndex: number; endIndex: number }
}
export interface TermReason {
termIndex: number
segmentId: string
rule: PedagogicalRule
shortReason: string
bullets?: string[]
}
// Context for tracking active terms to highlight related segments
interface DecompositionContextType {
activeTerms: Set<number>
activeSegmentId: string | null
addActiveTerm: (termIndex: number, segmentId?: string) => void
removeActiveTerm: (termIndex: number, segmentId?: string) => void
}
const DecompositionContext = createContext<DecompositionContextType>({
activeTerms: new Set(),
activeSegmentId: null,
addActiveTerm: () => {},
removeActiveTerm: () => {}
})
interface DecompositionWithReasonsProps {
fullDecomposition: string
termPositions: Array<{ startIndex: number; endIndex: number }>
segments?: PedagogicalSegment[]
termReasons?: TermReason[]
steps?: UnifiedStepData[]
}
interface TermSpanProps {
termIndex: number
text: string
segment?: PedagogicalSegment
reason?: TermReason
isCurrentStep?: boolean
}
function TermSpan({
termIndex,
text,
segment,
reason,
isCurrentStep = false
}: TermSpanProps) {
const { activeSegmentId } = useContext(DecompositionContext)
const rule = reason?.rule ?? segment?.plan[0]?.rule
// Only show styling for terms that have pedagogical reasoning
if (!rule) {
return <span className="term term--plain">{text}</span>
}
// Determine CSS classes based on current step only
const cssClasses = [
'term',
isCurrentStep && 'term--current' // New class for current step highlighting
].filter(Boolean).join(' ')
return (
<span className={cssClasses}>
{text}
</span>
)
}
// Component for rendering a segment group with unified hover target
interface SegmentGroupProps {
segment: PedagogicalSegment
fullDecomposition: string
termPositions: Array<{ startIndex: number; endIndex: number }>
termReasons?: TermReason[]
steps: UnifiedStepData[]
children: React.ReactNode
}
function SegmentGroup({ segment, fullDecomposition, steps, children }: SegmentGroupProps) {
const [tooltipOpen, setTooltipOpen] = useState(false)
const { addActiveTerm, removeActiveTerm } = useContext(DecompositionContext)
// Calculate the original term that was expanded
// digit * 10^place gives us the original value (e.g., digit=5, place=1 -> 50)
const originalValue = (segment.digit * Math.pow(10, segment.place)).toString()
const handleTooltipChange = (open: boolean) => {
setTooltipOpen(open)
// Activate/deactivate all terms in this segment
if (open) {
segment.termIndices.forEach(termIndex => addActiveTerm(termIndex, segment.id))
} else {
segment.termIndices.forEach(termIndex => removeActiveTerm(termIndex, segment.id))
}
}
return (
<ReasonTooltip
termIndex={segment.termIndices[0]} // Use first term for tooltip ID
segment={segment}
originalValue={originalValue}
steps={steps}
open={tooltipOpen}
onOpenChange={handleTooltipChange}
>
<span
className="segment-group"
onMouseEnter={() => handleTooltipChange(true)}
onMouseLeave={() => handleTooltipChange(false)}
onFocus={() => handleTooltipChange(true)}
onBlur={() => handleTooltipChange(false)}
>
{children}
</span>
</ReasonTooltip>
)
}
export function DecompositionWithReasons({
fullDecomposition,
termPositions,
segments,
termReasons,
steps = []
}: DecompositionWithReasonsProps) {
const [activeTerms, setActiveTerms] = useState<Set<number>>(new Set())
// Get current step index from tutorial context
const { state } = useTutorialContext()
const currentStepIndex = state.currentMultiStep
// Build segment boundaries and ranges
const segmentRanges = useMemo(() => {
if (!segments) return []
return segments.map(seg => ({
segment: seg,
startIndex: Math.min(...seg.termIndices.map(i => termPositions[i]?.startIndex ?? Infinity)),
endIndex: Math.max(...seg.termIndices.map(i => termPositions[i]?.endIndex ?? 0))
})).sort((a, b) => a.startIndex - b.startIndex)
}, [segments, termPositions])
// Build a quick lookup: termIndex -> segment
const termIndexToSegment = useMemo(() => {
const map = new Map<number, PedagogicalSegment>()
segments?.forEach(seg => seg.termIndices.forEach(i => map.set(i, seg)))
return map
}, [segments])
// Determine which segment should be highlighted based on active terms
const activeSegmentId = useMemo(() => {
if (activeTerms.size === 0) return null
// Find the segment that contains any of the active terms
for (const termIndex of activeTerms) {
const segment = termIndexToSegment.get(termIndex)
if (segment) {
return segment.id
}
}
return null
}, [activeTerms, termIndexToSegment])
const addActiveTerm = (termIndex: number, segmentId?: string) => {
setActiveTerms(prev => new Set([...prev, termIndex]))
}
const removeActiveTerm = (termIndex: number, segmentId?: string) => {
setActiveTerms(prev => {
const next = new Set(prev)
next.delete(termIndex)
return next
})
}
// Slice the decomposition string using termPositions
const pieces: React.ReactNode[] = []
let cursor = 0
// Render elements with segment groupings
const renderElements = () => {
const elements: React.ReactNode[] = []
let cursor = 0
let currentSegmentIndex = 0
for (let termIndex = 0; termIndex < termPositions.length; termIndex++) {
const { startIndex, endIndex } = termPositions[termIndex]
const segment = termIndexToSegment.get(termIndex)
const reason = termReasons?.find(r => r.termIndex === termIndex)
// Add connector text before this term
if (cursor < startIndex) {
elements.push(
<span key={`connector-${cursor}`}>
{fullDecomposition.slice(cursor, startIndex)}
</span>
)
}
// Check if this term starts a new segment
if (segment && segment.termIndices[0] === termIndex) {
// This is the first term of a segment - wrap all segment terms
const segmentElements: React.ReactNode[] = []
let segmentCursor = startIndex
for (const segTermIndex of segment.termIndices) {
const segPos = termPositions[segTermIndex]
if (!segPos) continue
// Add connector within segment
if (segmentCursor < segPos.startIndex) {
segmentElements.push(
<span key={`seg-connector-${segmentCursor}`}>
{fullDecomposition.slice(segmentCursor, segPos.startIndex)}
</span>
)
}
const segText = fullDecomposition.slice(segPos.startIndex, segPos.endIndex)
const segReason = termReasons?.find(r => r.termIndex === segTermIndex)
segmentElements.push(
<TermSpan
key={`seg-term-${segTermIndex}`}
termIndex={segTermIndex}
text={segText}
segment={segment}
reason={segReason}
isCurrentStep={segTermIndex === currentStepIndex}
/>
)
segmentCursor = segPos.endIndex
}
elements.push(
<SegmentGroup
key={`segment-${segment.id}`}
segment={segment}
fullDecomposition={fullDecomposition}
termPositions={termPositions}
termReasons={termReasons}
steps={steps}
>
{segmentElements}
</SegmentGroup>
)
// Skip ahead past all terms in this segment
const lastSegTermIndex = segment.termIndices[segment.termIndices.length - 1]
const lastSegPos = termPositions[lastSegTermIndex]
cursor = lastSegPos?.endIndex ?? endIndex
termIndex = lastSegTermIndex // Will be incremented by for loop
} else if (!segment) {
// Regular term not in a segment
const termText = fullDecomposition.slice(startIndex, endIndex)
elements.push(
<TermSpan
key={`term-${termIndex}`}
termIndex={termIndex}
text={termText}
segment={segment}
reason={reason}
isCurrentStep={termIndex === currentStepIndex}
/>
)
cursor = endIndex
}
// If this term is part of a segment but not the first, it was already handled above
}
// Add trailing text
if (cursor < fullDecomposition.length) {
elements.push(
<span key="trailing">
{fullDecomposition.slice(cursor)}
</span>
)
}
return elements
}
return (
<DecompositionContext.Provider value={{ activeTerms, activeSegmentId, addActiveTerm, removeActiveTerm }}>
<Tooltip.Provider delayDuration={200} skipDelayDuration={100}>
<div className="decomposition">
{renderElements()}
</div>
</Tooltip.Provider>
</DecompositionContext.Provider>
)
}

View File

@@ -0,0 +1,177 @@
'use client'
import React from 'react'
import * as Tooltip from '@radix-ui/react-tooltip'
import type { PedagogicalRule, PedagogicalSegment, TermReason } from './DecompositionWithReasons'
import type { UnifiedStepData } from '../../utils/unifiedStepGenerator'
interface ReasonTooltipProps {
children: React.ReactNode
termIndex: number
segment?: PedagogicalSegment
reason?: TermReason
originalValue?: string
steps?: UnifiedStepData[]
open?: boolean
onOpenChange?: (open: boolean) => void
}
export function ReasonTooltip({
children,
termIndex,
segment,
reason,
originalValue,
steps,
open,
onOpenChange
}: ReasonTooltipProps) {
const rule = reason?.rule ?? segment?.plan[0]?.rule
const shortReason = reason?.shortReason
const bullets = reason?.bullets
if (!rule) {
return <>{children}</>
}
const getRuleInfo = (rule: PedagogicalRule) => {
switch (rule) {
case 'Direct':
return {
emoji: '✨',
name: 'Direct Move',
description: 'Simple bead movement',
color: 'green'
}
case 'FiveComplement':
return {
emoji: '🤝',
name: 'Five Friend',
description: 'Using pairs that make 5',
color: 'blue'
}
case 'TenComplement':
return {
emoji: '🔟',
name: 'Ten Friend',
description: 'Using pairs that make 10',
color: 'purple'
}
case 'Cascade':
return {
emoji: '🌊',
name: 'Chain Reaction',
description: 'One move triggers another',
color: 'orange'
}
default:
return {
emoji: '💭',
name: 'Strategy',
description: 'Abacus technique',
color: 'gray'
}
}
}
const ruleInfo = getRuleInfo(rule)
const contentClasses = `reason-tooltip reason-tooltip--${ruleInfo.color}`
return (
<Tooltip.Root open={open} onOpenChange={onOpenChange} delayDuration={300}>
<Tooltip.Trigger asChild>
{children}
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
className={contentClasses}
sideOffset={8}
side="top"
align="center"
onPointerDownOutside={(e) => e.preventDefault()}
>
<div className="reason-tooltip__content">
<div className="reason-tooltip__header">
<span className="reason-tooltip__emoji">{ruleInfo.emoji}</span>
<div className="reason-tooltip__title">
<h4 className="reason-tooltip__name">{ruleInfo.name}</h4>
<p className="reason-tooltip__description">{ruleInfo.description}</p>
</div>
</div>
{shortReason && (
<div className="reason-tooltip__explanation">
<p>{shortReason}</p>
</div>
)}
{/* Show expansion reasoning from segment plan */}
{segment?.plan && segment.plan.length > 0 && (
<div className="reason-tooltip__reasoning">
<h5 className="reason-tooltip__section-title">Why this expansion?</h5>
{segment.plan.map((decision, idx) => (
<div key={idx} className="reason-tooltip__decision">
{decision.explanation.map((explain, explainIdx) => (
<p key={explainIdx} className="reason-tooltip__explanation-text">
{explain}
</p>
))}
</div>
))}
</div>
)}
{bullets && bullets.length > 0 && (
<div className="reason-tooltip__details">
<ul>
{bullets.map((bullet, idx) => (
<li key={idx}>
<span className="reason-tooltip__bullet"></span>
<span>{bullet}</span>
</li>
))}
</ul>
</div>
)}
{/* Show step-by-step breakdown for multi-step segments */}
{segment && steps && segment.stepIndices && segment.stepIndices.length > 1 && (
<div className="reason-tooltip__steps">
<h5 className="reason-tooltip__section-title">Step-by-step breakdown:</h5>
<ol className="reason-tooltip__step-list">
{segment.stepIndices.map((stepIndex, idx) => {
const step = steps[stepIndex]
if (!step) return null
return (
<li key={stepIndex} className="reason-tooltip__step">
<code className="reason-tooltip__step-term">{step.mathematicalTerm}</code>
<span className="reason-tooltip__step-instruction">{step.englishInstruction}</span>
</li>
)
})}
</ol>
</div>
)}
{originalValue && segment?.expression && (
<div className="reason-tooltip__formula">
<div className="reason-tooltip__expansion">
<span className="reason-tooltip__original">{originalValue}</span>
<span className="reason-tooltip__arrow"></span>
<code className="reason-tooltip__expanded">{segment.expression}</code>
</div>
<div className="reason-tooltip__label">
{originalValue} becomes {segment.expression}
</div>
</div>
)}
</div>
<Tooltip.Arrow className="reason-tooltip__arrow" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
)
}

View File

@@ -12,6 +12,7 @@ import { calculateBeadDiffFromValues } from '../../utils/beadDiff'
import { generateUnifiedInstructionSequence } from '../../utils/unifiedStepGenerator'
import { TutorialProvider, useTutorialContext } from './TutorialContext'
import { PedagogicalDecompositionDisplay } from './PedagogicalDecompositionDisplay'
import { DecompositionWithReasons } from './DecompositionWithReasons'
import { useAbacusDisplay } from '@/contexts/AbacusDisplayContext'
// Helper function to find the topmost bead with arrows
@@ -199,6 +200,7 @@ function TutorialPlayerContent({
const isProgrammaticChange = useRef(false)
const [showHelpForCurrentStep, setShowHelpForCurrentStep] = useState(false)
// Use tutorial context instead of local state
const {
state,
@@ -252,7 +254,7 @@ function TutorialPlayerContent({
}
// Define the static expected steps using our unified step generator
const { expectedSteps, fullDecomposition, isMeaningfulDecomposition } = useMemo(() => {
const { expectedSteps, fullDecomposition, isMeaningfulDecomposition, pedagogicalSegments, termPositions } = useMemo(() => {
try {
const unifiedSequence = generateUnifiedInstructionSequence(currentStep.startValue, currentStep.targetValue)
@@ -267,16 +269,23 @@ function TutorialPlayerContent({
termPosition: step.termPosition // Add the precise position information
}))
// Extract term positions from steps for DecompositionWithReasons
const positions = unifiedSequence.steps.map(step => step.termPosition).filter(Boolean)
return {
expectedSteps: steps,
fullDecomposition: unifiedSequence.fullDecomposition,
isMeaningfulDecomposition: unifiedSequence.isMeaningfulDecomposition
isMeaningfulDecomposition: unifiedSequence.isMeaningfulDecomposition,
pedagogicalSegments: unifiedSequence.segments,
termPositions: positions
}
} catch (error) {
return {
expectedSteps: [],
fullDecomposition: '',
isMeaningfulDecomposition: false
isMeaningfulDecomposition: false,
pedagogicalSegments: [],
termPositions: []
}
}
}, [currentStep.startValue, currentStep.targetValue])
@@ -1101,7 +1110,7 @@ function TutorialPlayerContent({
Guidance
</p>
{/* Pedagogical decomposition with current term highlighted */}
{/* Pedagogical decomposition with interactive reasoning */}
{fullDecomposition && isMeaningfulDecomposition && (
<div className={css({
mb: 4,
@@ -1120,14 +1129,16 @@ function TutorialPlayerContent({
letterSpacing: 'tight',
lineHeight: '1.5'
})}>
<PedagogicalDecompositionDisplay
variant="guidance"
decomposition={renderHighlightedDecomposition()}
<DecompositionWithReasons
fullDecomposition={fullDecomposition}
termPositions={termPositions}
segments={pedagogicalSegments}
/>
</p>
</div>
)}
<div className={css({
fontSize: 'sm',
color: 'amber.800',

View File

@@ -0,0 +1,401 @@
/* CSS styling for DecompositionWithReasons component */
.decomposition {
display: inline;
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace;
font-size: inherit;
line-height: inherit;
}
.term {
display: inline-block;
padding: 2px 4px;
margin: 0 1px;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: inherit;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.term--plain {
cursor: default;
padding: 0;
margin: 0;
border: none;
}
.term:focus {
outline: none;
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.5);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
/* Active term - the one being directly hovered/clicked */
.term--active {
background: rgba(59, 130, 246, 0.25);
border-color: rgba(59, 130, 246, 0.6);
font-weight: 600;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
}
/* Current step - the term that matches the current tutorial step */
.term--current {
background: rgba(245, 158, 11, 0.2);
border-color: rgba(245, 158, 11, 0.6);
border-width: 2px;
font-weight: 600;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3);
animation: currentStepGlow 1.5s ease-in-out infinite;
}
/* Grouped terms - terms that belong to pedagogical segments */
.term--grouped {
background: transparent;
border-color: transparent;
}
/* Reason tooltip styling */
.reason-tooltip {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 200px;
max-width: 240px;
z-index: 50;
animation: fadeIn 0.2s ease-out;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.reason-tooltip__content {
display: flex;
flex-direction: column;
gap: 8px;
}
.reason-tooltip__header {
display: flex;
align-items: flex-start;
gap: 8px;
}
.reason-tooltip__emoji {
font-size: 18px;
line-height: 1;
flex-shrink: 0;
}
.reason-tooltip__title {
flex: 1;
}
.reason-tooltip__name {
font-size: 16px;
font-weight: 600;
margin: 0 0 4px 0;
color: #1f2937;
}
.reason-tooltip__description {
font-size: 13px;
margin: 0;
color: #6b7280;
opacity: 0.9;
}
.reason-tooltip__explanation {
font-size: 14px;
color: #374151;
line-height: 1.4;
}
.reason-tooltip__explanation p {
margin: 0;
}
.reason-tooltip__details ul {
margin: 0;
padding: 0;
list-style: none;
}
.reason-tooltip__details li {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 6px;
font-size: 13px;
color: #4b5563;
line-height: 1.4;
}
.reason-tooltip__details li:last-child {
margin-bottom: 0;
}
.reason-tooltip__bullet {
color: #9ca3af;
font-weight: bold;
margin-top: 1px;
flex-shrink: 0;
}
.reason-tooltip__formula {
padding-top: 8px;
border-top: 1px solid #f3f4f6;
}
.reason-tooltip__expansion {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
padding: 8px;
background: #fafbfc;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.reason-tooltip__original {
background: #fff2cc;
border: 1px solid #ffd93d;
border-radius: 4px;
padding: 4px 8px;
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace;
font-size: 14px;
font-weight: 600;
color: #b45309;
}
.reason-tooltip__arrow {
color: #6b7280;
font-weight: bold;
font-size: 16px;
}
.reason-tooltip__expanded {
background: #f0f9ff;
border: 1px solid #7dd3fc;
border-radius: 4px;
padding: 4px 8px;
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace;
font-size: 12px;
color: #1e40af;
flex: 1;
}
.reason-tooltip__section-title {
font-size: 12px;
font-weight: 600;
color: #374151;
margin: 0 0 8px 0;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 4px;
}
.reason-tooltip__reasoning {
margin-bottom: 12px;
}
.reason-tooltip__decision {
margin-bottom: 8px;
}
.reason-tooltip__explanation-text {
font-size: 12px;
color: #4b5563;
margin: 0 0 4px 0;
line-height: 1.4;
}
.reason-tooltip__steps {
margin-bottom: 12px;
}
.reason-tooltip__step-list {
margin: 0;
padding: 0 0 0 16px;
list-style: decimal;
}
.reason-tooltip__step {
margin-bottom: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.reason-tooltip__step:last-child {
margin-bottom: 0;
}
.reason-tooltip__step-term {
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 3px;
padding: 2px 6px;
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace;
font-size: 11px;
color: #1f2937;
font-weight: 600;
}
.reason-tooltip__step-instruction {
font-size: 11px;
color: #6b7280;
line-height: 1.3;
font-style: italic;
}
.reason-tooltip__label {
font-size: 11px;
font-weight: 500;
color: #6b7280;
text-align: center;
font-style: italic;
}
.reason-tooltip__code {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 4px;
padding: 4px 6px;
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace;
font-size: 12px;
color: #1f2937;
}
/* Radix tooltip arrow */
.reason-tooltip .reason-tooltip__arrow {
fill: #ffffff;
stroke: #e5e7eb;
stroke-width: 2px;
}
/* Color variants */
.reason-tooltip--green {
border-color: #10b981;
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
}
.reason-tooltip--green .reason-tooltip__name {
color: #065f46;
}
.reason-tooltip--green .reason-tooltip__emoji {
filter: hue-rotate(120deg);
}
.reason-tooltip--blue {
border-color: #3b82f6;
background: linear-gradient(135deg, #ffffff 0%, #eff6ff 100%);
}
.reason-tooltip--blue .reason-tooltip__name {
color: #1e40af;
}
.reason-tooltip--purple {
border-color: #8b5cf6;
background: linear-gradient(135deg, #ffffff 0%, #f5f3ff 100%);
}
.reason-tooltip--purple .reason-tooltip__name {
color: #5b21b6;
}
.reason-tooltip--orange {
border-color: #f59e0b;
background: linear-gradient(135deg, #ffffff 0%, #fffbeb 100%);
}
.reason-tooltip--orange .reason-tooltip__name {
color: #92400e;
}
.reason-tooltip--gray {
border-color: #6b7280;
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes groupPulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.02);
box-shadow: 0 0 0 1px rgba(147, 51, 234, 0.4);
}
100% {
transform: scale(1);
}
}
@keyframes currentStepGlow {
0% {
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3);
}
50% {
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.5), 0 0 8px rgba(245, 158, 11, 0.3);
}
100% {
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3);
}
}
/* Segment group styling */
.segment-group {
display: inline;
cursor: pointer;
border-radius: 6px;
padding: 2px 4px;
margin: 0 1px;
transition: all 0.2s ease;
}
.segment-group:hover {
background: rgba(147, 51, 234, 0.1);
box-shadow: 0 0 0 2px rgba(147, 51, 234, 0.2);
}
/* Accessibility improvements */
.term:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
.segment-group:focus-visible {
outline: 2px solid #8b5cf6;
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.term {
transition: none;
}
.reason-popover {
animation: none;
}
}