In multiplayer steam sprint games, the train was displaying passengers from
all players (including inactive ones) instead of only showing passengers
belonging to the first active player.
The issue was that boardedPassengers was filtering for any claimed passenger,
not checking if the claiming player was still active.
Changes to SteamTrainJourney.tsx:
- Filter boardedPassengers to only include passengers where claimedBy matches
the firstActivePlayer's ID
- Add firstActivePlayer.id to the useMemo dependency array
- Update comment to clarify filtering logic
Now the train correctly displays only the first active player's passengers
in their train cars, matching the player emoji shown as the engineer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The interactive flashcards section at the bottom of the homepage was
hardcoding beadShape="circle" instead of respecting the app-wide abacus
configuration context that all other abaci on the site use.
Changes to InteractiveFlashcards.tsx:
- Import useAbacusConfig hook from @soroban/abacus-react
- Call useAbacusConfig() in DraggableCard component
- Use appConfig.beadShape instead of hardcoded "circle"
This ensures visual consistency across all abacus displays on the site
and respects user preferences for bead styling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
On mobile screens (base breakpoint), the level details cards were taking
up all vertical space within the 500px maxHeight constraint, pushing the
abacus completely out of view.
Solution: Hide level details on mobile (display: { base: 'none', lg: 'grid' })
so mobile users see only the slider and abacus, while desktop users see
all components. Also added minH: 0 to containers to ensure proper flex
shrinking behavior.
Changes to LevelSliderDisplay.tsx:
- Level details grid now hidden on base breakpoint, visible on lg+
- Simplified grid columns to single value since it's desktop-only
- Added minH: 0 to flex containers for proper shrinking
Tested on iPhone 14 (390px viewport).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added maxHeight constraint and reduced padding to ensure the abacus
stays visible while scrubbing the slider on mobile devices.
Changes:
- Added maxHeight: 500px on mobile (base), none on desktop (md)
- Reduced padding from 6 to 4 on mobile (base)
This ensures users can see both the slider and abacus simultaneously
on iPhone displays without scrolling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed horizontal overflow issues on small screens (iPhone 14, 390px viewport):
Changes to LevelSliderDisplay component:
- Made abacus display container stack vertically on mobile (base) and horizontal on large screens (lg)
- Made level details grid responsive: 2 cols (mobile), 3 cols (sm), 2 cols (lg)
- Removed fixed widths from detail cards, using flexible widths with min/max constraints
- Added horizontal scroll (overflow-x: auto) to abacus container as fallback
- Reduced slider thumb size on mobile: 120px x 96px (base) vs 180px x 128px (md)
- Scaled down bead in slider thumb to 75% on mobile
- Reduced emoji tick sizes: 2xl (mobile), 3xl (sm), 4xl (md)
These changes ensure the "Your Journey" slider and abacus display fit properly
on mobile devices without horizontal overflow while maintaining the desktop experience.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Problem: Cards jumped when grabbed, especially if already rotated. The grab
point would slip during rotation instead of staying under the cursor.
Root cause: Grab offset was stored in screen space with the card already
rotated. When we later rotated this offset during drag, we were applying
rotation on top of rotation.
Solution: Convert grab offset to local (unrotated) coordinates when grabbing:
- Calculate screen-space offset from card center
- Rotate by -currentRotation to get local coordinates
- Store in grabOffsetRef
- During drag, rotate this local offset by the current rotation angle
This ensures the grab point stays perfectly under the cursor regardless of
the card's initial or current rotation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed the issue where cards would slip out from under the cursor when rotating.
The grab point now stays perfectly under your cursor throughout the drag.
The problem: Simple delta positioning didn't account for rotation causing the
grab point to rotate away from the cursor position.
The solution: Properly convert between coordinate systems:
1. Calculate rotated grab offset (2D rotation matrix)
2. Determine card center: cursor position - rotated grab offset (viewport space)
3. Convert from viewport coordinates to container coordinates
4. Calculate top-left position from center for CSS translate()
Key changes:
- Pass containerRef down to DraggableCard components
- Get container bounds for viewport→container conversion
- Apply rotation matrix to grab offset
- Position card so rotated grab point aligns with cursor
Now when you grab a card by its edge and drag, the exact point you grabbed
stays glued to your cursor while the card rotates around its center.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Reverted the complex rotation-compensated positioning that was causing
cards to jump to the right of the viewport on click.
The issue: The previous attempt to keep the grab point "stuck" to the cursor
while rotating was mixing screen coordinates with container-relative coords,
causing cards to teleport on pointer down.
The solution: Use simple delta positioning (cursor movement from drag start)
and let CSS handle rotation around the card center. While this means the grab
point won't stay perfectly under the cursor as the card rotates, it's much
better than cards jumping unexpectedly.
The rotation still works - cards rotate based on grab point physics - but
the positioning is now stable and predictable.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed issue where cards appeared to pivot around the grab point in
viewport space instead of rotating around their own center.
The problem: When dragging, the card would stay "pinned" at the grab
point in screen space, making it rotate around that viewport location.
The solution: As the card rotates, calculate the rotated grab offset
and reposition the card so that:
1. The grab point stays under the cursor
2. The card rotates around its own center
3. The visual result feels like the card is "stuck" to your finger
Implementation:
- Convert rotation angle to radians
- Apply 2D rotation matrix to grab offset vector
- Calculate card center position: cursor - rotated grab offset
- Convert center position to top-left for CSS translate positioning
This creates the natural feeling of grabbing and spinning a physical
card by its edge.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The previous scale factor of 5000 made rotation changes too subtle to see.
Reduced to 500 (10x more sensitive) so card rotation is clearly visible
when dragging from off-center grab points.
Also added detailed rotation logging during drag to help debug:
- Shows current rotation angle
- Shows rotation influence being applied
- Shows cross product value from physics calculation
This will help test and tune the rotation feel.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements physics-based rotation that responds to where the user grabs
the card. When you grab a card off-center and drag it, it rotates
naturally based on the grab point and drag direction.
Features:
- Calculate grab offset from card center on pointer down
- Apply rotation using cross product of grab offset and drag direction
- Rotation clamped to ±45° to prevent excessive spinning
- Final rotation preserved when card is released
- Console logging for grab point coordinates and rotation changes
Physics details:
- Cross product (grabOffset.x * deltaY - grabOffset.y * deltaX) determines
rotation direction and magnitude
- Grabbing left side + dragging right = clockwise rotation
- Grabbing right side + dragging left = counter-clockwise rotation
- Scale factor of 5000 provides smooth, realistic rotation feel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed logging issue where speed logs weren't showing during drag:
- Added separate lastLogTimeRef for logging throttle
- Logs now appear every ~200ms during drag (was never logging before)
- Velocity calculation unchanged, only logging throttle fixed
Now you can see speed values in console during drag to verify
shadow responsiveness.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
First physics enhancement - shadow changes based on how fast you drag:
- Tracks drag velocity (distance/time) during pointer move
- Shadow grows larger and darker with faster dragging
- Base: 8px offset, 24px blur, 0.3 opacity
- Fast: 32px offset, 64px blur, 0.6 opacity
- Smooth decay when released
Console logging included:
- [Shadow] logs on drag start/release
- Speed/distance/time logged during drag (throttled to ~100ms)
Test: Drag cards slowly vs fast and watch shadow change
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replaced complex react-spring + useDrag implementation with simple
PointerEvents-based drag and drop:
- Uses native onPointerDown/Move/Up events
- Tracks position with useState (no animation library)
- Cards stay exactly where dropped (no physics or snap-back)
- Simple scale-up effect while dragging
- Much more predictable and maintainable
Removed dependencies on:
- @react-spring/web
- @use-gesture/react
- Complex velocity tracking and decay physics
- Transform-origin calculations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Removed conflicting top-level config that was interfering with decay
animations. Now using explicit config objects for each property:
- x, y: decay physics with velocity
- scale, rotation: wobbly spring animations
This should fix the issue where cards were snapping back to pickup
position instead of staying where dropped.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed the issue where cards were snapping back to pickup position:
- Use api.set() to immediately snap spring position to dropped location
- Then apply decay animation with momentum from that position
- Removed problematic 'from' property which doesn't work with decay
The bug was that react-spring's 'from' property is ignored when using
decay: true, causing the spring to animate from its current value rather
than the specified position. Using api.set() first ensures the spring
starts from the correct dropped position.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Made interactive flashcards a fun easter egg by:
- Changed overflow from 'hidden' to 'visible' to allow cards to be
thrown anywhere on the page, not just within container bounds
- Fixed position persistence: cards now stay exactly where dropped
instead of snapping back to pickup location
- Updated currentPositionRef immediately on drop before applying
momentum physics
Cards can now be dragged and tossed freely around the entire page
and will maintain their position after being dropped.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Increased mobile scale from 2.5 to 3.5 to better utilize available screen
space on mobile devices. Screenshot review showed ample room above and
below the abacus without risk of overlapping title or scroll hint.
Final scales:
- Mobile (base): scale(3.5) - 75% larger than original
- Medium (md): scale(3.5) - 16% larger than original
- Desktop (lg): scale(4.25) - 6% larger than original
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Reduced mobile scale from 2.8 to 2.5 to ensure the "Scroll to explore"
hint at the bottom is not covered by the abacus.
Final scales:
- Mobile (base): scale(2.5) - 25% larger than original, no overlap
- Medium (md): scale(3.5) - 16% larger than original
- Desktop (lg): scale(4.25) - 6% larger than original
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fine-tuned scale values based on visual feedback:
- Mobile (base): scale(2.8) - increased for better mobile visibility (40% larger than original 2)
- Medium screens (md): scale(3.5) - unchanged (16% larger than original 3)
- Desktop (lg): scale(4.25) - reduced 15% for safety (6% larger than original 4)
This balances maximum visual impact on mobile while preventing any
layout issues on larger screens.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Increase scale values across all breakpoints, especially on mobile:
- Mobile (base): scale(2) → scale(3) (50% increase)
- Medium screens (md): scale(3) → scale(4.5) (50% increase)
- Large screens (lg): scale(4) → scale(6) (50% increase)
The abacus now fills more of the available hero space without
clipping, improving visual impact and usability on all devices.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Two small refinements to navbar branding:
- Added whiteSpace: 'nowrap' to subtitle to prevent text wrapping
- Removed abacus emoji from "Abaci One" branding for cleaner look
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed logic - the glassmorphism effect (backdrop blur, subtle backgrounds,
borders, shadows) now applies when the navbar is transparent at the top of
the homepage (isTransparent=true), not when scrolled down.
When scrolled or on other pages, the links are simpler without the extra
glassmorphism styling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Enhanced nav links (Create, Guide, Games) with floating glass pill effect
when navbar is in scrolled mode:
- Added backdrop-filter blur(8px) to blur content underneath each link
- Subtle semi-transparent backgrounds (white for inactive, purple for active)
- Added borders with purple/white tints for enhanced definition
- Soft box shadows that glow purple on hover
- Enhanced hover states with stronger purple effects
The links now have better contrast against the dark navbar while maintaining
the ethereal floating illusion. Effects are only applied when scrolled (not
when navbar is transparent on homepage hero).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Removed the 10px gradient fade at the bottom of the navbar per user request.
Kept the border fix that removes the black line artifact when transparent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed two visual issues with the navbar:
1. Removed black border line when transparent: Changed borderBottom to
conditionally be 'none' instead of '1px solid transparent', which was
showing up as a visible line when the navbar is transparent on homepage
2. Added 10px gradient fade at bottom: Applied linear-gradient that fades
from the dark background to transparent over the last 10px, creating a
softer transition instead of a sharp cut-off
The navbar now seamlessly blends into the page content with no visual artifacts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated AppNavBar styling to blend with homepage's dark glassmorphism theme:
- Changed navbar background from white to semi-transparent dark (rgba(0,0,0,0.5))
- Added backdrop-filter blur(12px) for glassmorphism effect
- Updated border color to purple accent (rgba(139,92,246,0.2))
- Changed logo/branding text to white/light purple tones
- Updated nav link colors from gray to light gray/purple palette
- Enhanced hover states with purple highlights (rgba(196,181,253))
The navbar now seamlessly integrates with the homepage's dark theme while
maintaining transparency when the hero section is visible.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Prevents the level slider from causing horizontal overflow on mobile
devices, which was making the page width wider than intended.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed layout breakpoints from md (768px) to xl (1280px) to ensure:
- Skills section doesn't get clipped at medium viewport sizes
- Layout only switches to side-by-side when there's sufficient space
- Container min-width is responsive (100% below xl, 1400px at xl+)
This prevents the issue where content was overflowing at intermediate
viewport widths.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Moved "Learn by Doing" section outside the maxW:7xl container to allow
the demo div to be 1400px wide and properly centered with mx:auto.
Remaining content stays within the 7xl constrained container.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added minW: '1400px' and removed maxW: '1200px' constraint to ensure
the "What You'll Learn" section is properly sized and centered.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Increased container from 650px to 800px to ensure skill card titles
like "Friends techniques" don't wrap awkwardly across multiple lines.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed text being cut off on the right side of skill cards by:
- Adding minWidth: '0' to text container to enable proper flex shrinking
- Adding flexWrap: 'wrap' to title/badge row for better wrapping behavior
This fixes the flexbox issue where text was overflowing the card boundaries.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed width breakpoint from md to lg to match when the grid switches
to 2 columns. This prevents overflow on medium-width screens where the
container was 650px wide but still showing 1-column layout.
Now:
- Below lg: 100% width, 1 column
- At lg+: 650px width, 2 columns
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Increased the "What You'll Learn" container width from 420px to 650px
to give the 2-column grid of skill cards proper breathing room and
prevent cramped layouts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed skill card abacus sizing by:
- Changing MiniAbacus to use width/height 100% instead of fixed 75px/80px
- Increased scale from 0.6 to 0.75 for better visibility
- Now properly fills the 120px/150px container set on the wrapper
This fixes the clipping issue by making the component hierarchy work correctly:
outer container (120px/150px) -> MiniAbacus (100%) -> scaled AbacusReact
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Increase abacus container width from 95px/110px to 120px (mobile) and
150px (desktop) to properly accommodate 3-column abacus visualizations
without clipping.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove overflow:hidden and increase the abacus container width from
75px to responsive widths (95px mobile, 110px desktop) to properly
accommodate the abacus visualizations without clipping.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add overflow: hidden to skill card containers to properly contain
content within card bounds and prevent visual overflow issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Change abacus container height to minHeight to prevent content
overflow in skill cards. This allows the container to grow as
needed while maintaining minimum dimensions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update "What You'll Learn" section to display in a responsive grid:
- One column on mobile/tablet
- Two columns on larger screens (lg breakpoint)
- Increased padding and height on two-column layout
- Added emojis to skill titles for better visual appeal
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update arcade section subtitle to highlight both single-player
challenges and multiplayer battles. Also mention the ability to
invite friends to observe games live over the network.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace "Available Now" section with "The Arcade" to better describe
the multiplayer room system. Updated subtitle to explain that users
can create or join rooms to play real-time games with friends over
the network.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Consolidate the complete levels slider UI from /levels page into a reusable
LevelSliderDisplay component. This eliminates code duplication and provides
a single source of truth for the levels progression display.
Changes:
- Created LevelSliderDisplay component (862 lines) with all levels data,
state management, animations, and UI
- Updated /levels page to use shared component (959 → 117 lines, 87.8% reduction)
- Updated homepage to use shared component (removed duplicate state/data)
- Deleted incomplete LevelsSlider component from previous commit
Component includes:
- Complete allLevels array (21 levels: 10th Kyu through 10th Dan)
- Interactive Radix slider with emoji tick marks
- StandaloneBead thumb with animated emoji transitions
- Level details for Kyu levels (Add/Sub, Multiply, Divide stats)
- Animated abacus display with speed scaling for Dan levels
- Auto-advance functionality (3s interval, pauses on hover)
- Dark theme styling
Both homepage and /levels page now use the same component with identical
behavior and appearance, eliminating maintenance overhead.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace the static emoji progression display in "Your Journey" section with
an interactive slider component adapted from /levels page. Features:
- Auto-advancing slider (3s intervals, pauses on hover)
- Interactive bead thumb with animated emoji overlay
- Animated abacus display that changes with level
- Shows 8 key milestone levels (10th Kyu through 10th Dan)
- Emoji tick marks above slider for visual navigation
- Dynamic border colors based on level category
Components:
- Created LevelsSlider component to encapsulate the UI
- Reuses existing React Spring animations from /levels page
- Uses Radix Slider for accessible interaction
- Dark theme abacus styling for consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Combine the interactive flashcards demonstration and the "Create
Flashcards" call-to-action into a single unified section. The card
throwing area, feature highlights, and CTA button are now part of
one cohesive pane instead of two separate sections.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add physics-based flashcard component with drag, throw, and momentum:
- Random generation of 8-15 flashcards with abacus visualizations
- Smooth drag interactions with velocity-based rotation
- Decay physics for realistic throwing and sliding
- Transform-origin pivot point based on click position
- Position persistence so cards stay where thrown
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fix flashcards being positioned outside visible area by using the
container's actual dimensions instead of hardcoded pixel values.
The previous implementation used CONTAINER_WIDTH=800px and
CONTAINER_HEIGHT=500px to position cards, but the actual container
width is 100% which varies by screen size. This caused cards to be
positioned outside the visible area on smaller screens.
Changes:
- Add containerRef to get actual container dimensions
- Calculate card positions based on offsetWidth/offsetHeight
- Remove hardcoded dimension constants
- Ensure cards stay within visible bounds with proper margins
This makes the flashcard positioning responsive to any screen size.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fix interactive flashcards not rendering by properly converting
react-spring animated values to CSS transforms. The x, y, rotation,
and scale spring values now use the `to` helper to create proper
CSS transform strings.
Changes:
- Import `to` helper from @react-spring/web
- Convert spring values to CSS transform using translate/rotate/scale
- Set position to absolute with left:0, top:0 as transform origin
This fixes the issue where flashcards were invisible because the
spring values weren't being properly converted to CSS.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add a fun, interactive flashcard display to the homepage's flashcard
generator section. Users can drag and throw 8-15 randomly generated
flashcards around with realistic physics-based momentum.
Features:
- Drag and drop flashcards with mouse/touch
- Throw cards with velocity-based physics
- 8-15 randomly generated flashcards (100-999 range)
- Real AbacusReact components for each card
- Client-side rendering to avoid hydration errors
Technical implementation:
- Uses @use-gesture/react for drag gesture handling
- Uses @react-spring/web for smooth physics animations
- Cards generated client-side with useEffect to prevent SSR mismatch
- Each card maintains its own spring-based position and rotation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>