/*
 * style.css for InDepth MVE - Phase 2
 * 
 * Purpose:
 * - Defines core color palette and design system
 * - Handles animations and transitions
 * - Provides mobile-specific touch optimizations
 * 
 * Architecture:
 * - Most layout styling handled by Tailwind utility classes in HTML
 * - This file contains only what Tailwind can't do: custom animations, variables
 * - Keeps CSS minimal for MVE speed and maintainability
 */

/*
 * CSS Custom Properties (Design System)
 * 
 * Why use CSS variables:
 * - Single source of truth for colors (easier to update theme)
 * - Can be referenced in JavaScript if needed
 * - Better than hardcoded hex values scattered throughout CSS
 * 
 * Color palette rationale:
 * - Dark theme reduces eye strain for nighttime baseball viewing
 * - Blue accent color evokes baseball tradition (MLB branding)
 * - High contrast for readability on mobile devices
 */
:root {
    --color-background: #0D1B2A;  /* Deep navy blue - primary background */
    --color-primary: #1B263B;      /* Lighter navy - cards and panels */
    --color-secondary: #415A77;    /* Medium blue-gray - borders and dividers */
    --color-text: #E0E1DD;         /* Off-white - primary text color */
    --color-accent: #3B82F6;       /* Bright blue (Tailwind blue-500) - interactive elements */
}

/*
 * Base body styles for app-like mobile experience
 * 
 * Why these properties matter:
 * - -webkit-tap-highlight-color: Removes the flash when tapping on mobile Safari
 * - touch-action: manipulation: Disables double-tap to zoom (prevents accidental zoom)
 * 
 * Together, these create a native app feel instead of "mobile website" feel
 */
body {
    -webkit-tap-highlight-color: transparent; /* Removes tap highlight on iOS Safari */
    touch-action: manipulation; /* Disables double-tap zoom - critical for swipe nav */
    position: fixed; /* Prevents body scrolling when keyboard appears */
    width: 100%;
    height: 100%;
    overflow: hidden;
}

/*
 * v8.8 Mobile Keyboard Fix: Visual Viewport API Solution
 * 
 * Problem: When mobile keyboard appears, it pushes the app footer up, eating screen real estate
 * 
 * Solution: JavaScript sets app container to FIXED pixel height
 * - JavaScript captures visualViewport.height on page load
 * - Sets #app-container height to exact pixel value (e.g., 844px)
 * - Height NEVER changes, even when keyboard appears
 * - When keyboard appears, it covers the bottom navigation (desired behavior)
 * - Chat feed flexes to fill available space above keyboard
 * 
 * Result: Footer is hidden behind keyboard, giving more room for conversation
 * 
 * Why JavaScript not CSS:
 * - All viewport-based units (vh, dvh, svh) recalculate when keyboard opens
 * - JavaScript can capture the INITIAL height and keep it fixed
 * - Visual Viewport API is designed specifically for this use case
 * 
 * CSS Role:
 * - Keep body position fixed to prevent scrolling
 * - Don't set height on #app-container (JavaScript handles this)
 * - Let flexbox layout do its job
 */
#app-container {
    /* Height set dynamically by JavaScript - DO NOT add height here */
    /* Tailwind classes handle: w-full flex flex-col */
}

/*
 * Ensure chat hub uses full available height and flexes properly
 * This allows the chat feed to grow/shrink while keeping input bar fixed
 */
#chat-hub {
    /* Tailwind classes handle: flex flex-col h-full */
    min-height: 0; /* Critical: Allows flex children to shrink below content size */
}

/*
 * Chat feed must be scrollable and flexible
 * When keyboard appears, this shrinks to accommodate
 */
#chat-feed {
    /* Tailwind classes handle: flex-grow overflow-y-auto */
    min-height: 0; /* Critical: Allows flexbox to shrink below content size */
}

/*
 * Active navigation button styling
 * 
 * Why separate from Tailwind:
 * - JavaScript toggles 'active' class dynamically
 * - Cleaner to define state change in CSS than inline Tailwind
 * 
 * Usage:
 * - Applied to whichever nav button corresponds to the current hub
 * - JavaScript updateNavButtons() manages this class
 */
.nav-button.active {
    color: var(--color-accent); /* Blue highlight for active hub */
}

/*
 * ============================================
 * STRIKE ZONE MICRO-INTERACTION ANIMATIONS
 * ============================================
 */

/* 1. Count-Based Intensity States - Glowing borders based on game situation */
@keyframes glow-red {
    0%, 100% { box-shadow: 0 0 10px rgba(239, 68, 68, 0.4), 0 0 20px rgba(239, 68, 68, 0.2); }
    50% { box-shadow: 0 0 20px rgba(239, 68, 68, 0.6), 0 0 30px rgba(239, 68, 68, 0.3); }
}

@keyframes glow-green {
    0%, 100% { box-shadow: 0 0 10px rgba(34, 197, 94, 0.4), 0 0 20px rgba(34, 197, 94, 0.2); }
    50% { box-shadow: 0 0 20px rgba(34, 197, 94, 0.6), 0 0 30px rgba(34, 197, 94, 0.3); }
}

@keyframes glow-gold {
    0%, 100% { box-shadow: 0 0 15px rgba(251, 191, 36, 0.5), 0 0 30px rgba(251, 191, 36, 0.3), 0 0 45px rgba(251, 191, 36, 0.1); }
    50% { box-shadow: 0 0 25px rgba(251, 191, 36, 0.7), 0 0 40px rgba(251, 191, 36, 0.4), 0 0 60px rgba(251, 191, 36, 0.2); }
}

.zone-danger { animation: glow-red 2s ease-in-out 3; }
.zone-advantage { animation: glow-green 2s ease-in-out 3; }
.zone-full-count { animation: glow-gold 1.5s ease-in-out 3; }

/* 2. Ball/Strike Counter Pop Animations */
@keyframes counter-pop-green {
    0% { transform: scale(0.5); opacity: 0; }
    50% { transform: scale(1.3); }
    100% { transform: scale(1); opacity: 1; }
}

@keyframes counter-pop-red {
    0% { transform: scale(0.5); opacity: 0; }
    50% { transform: scale(1.3); }
    100% { transform: scale(1); opacity: 1; }
}

@keyframes counter-shake {
    0%, 100% { transform: translateX(0); }
    25% { transform: translateX(-3px); }
    75% { transform: translateX(3px); }
}

.ball-pop { animation: counter-pop-green 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
.strike-pop { animation: counter-pop-red 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
.counter-shake-anim { animation: counter-shake 0.3s ease-in-out; }

/* 3. New Pitch Landing Animations */
@keyframes pitch-land {
    0% { transform: translate(-50%, -50%) scale(0.3); opacity: 0; }
    60% { transform: translate(-50%, -50%) scale(1.15); }
    100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}

@keyframes pitch-ripple {
    0% { transform: translate(-50%, -50%) scale(0.8); opacity: 0.8; }
    100% { transform: translate(-50%, -50%) scale(2.5); opacity: 0; }
}

@keyframes pitch-glow {
    0%, 100% { filter: brightness(1); }
    50% { filter: brightness(1.8) drop-shadow(0 0 8px currentColor); }
}

.pitch-marker-land {
    animation: pitch-land 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.pitch-marker-glow {
    animation: pitch-glow 1.5s ease-in-out 2;
}

/* Ripple effect container */
.pitch-ripple {
    position: absolute;
    width: 100%;
    height: 100%;
    border-radius: 50%;
    border: 2px solid currentColor;
    animation: pitch-ripple 0.8s ease-out;
    pointer-events: none;
}

/* 4. Anticipation Pulse - REMOVED per user request */

/* 5. Pitch Trail Animation */
@keyframes pitch-trail {
    0% { stroke-dashoffset: 300; opacity: 0; }
    20% { opacity: 1; }
    100% { stroke-dashoffset: 0; opacity: 0; }
}

.pitch-trail-line {
    stroke-dasharray: 300;
    stroke-dashoffset: 300;
    animation: pitch-trail 1s ease-out;
}

/* 6. Combo Animations */
@keyframes balls-shake {
    0%, 100% { transform: translateX(0); }
    10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
    20%, 40%, 60%, 80% { transform: translateX(2px); }
}

/* Zone narrow removed - strike zone should never move */

@keyframes foul-flash {
    0%, 100% { background-color: transparent; }
    50% { background-color: rgba(251, 191, 36, 0.2); }
}

.balls-shake-anim { animation: balls-shake 0.5s ease-in-out; }
.foul-flash-anim { animation: foul-flash 0.4s ease-in-out 3; }

/*
 * Deep Dive Panel slide-up animation
 * 
 * How it works:
 * - Panel starts off-screen: transform: translateY(100%) in HTML
 * - JavaScript adds 'visible' class when showing panel
 * - CSS transition handles smooth animation
 * 
 * Why this approach:
 * - Pure CSS animation (better performance than JavaScript)
 * - Native mobile pattern (bottom sheet)
 * - Hardware-accelerated (transform uses GPU)
 */
#deep-dive-panel.visible {
    display: block; /* Shows the backdrop */
}

#deep-dive-panel.visible #deep-dive-content {
    transform: translateY(0); /* Slides panel up to visible position */
}

/*
 * Lineup tab active state styling
 * 
 * Usage:
 * - Applied to Away/Home tabs in Lineup Hub
 * - JavaScript toggles when user switches teams
 * 
 * Visual design:
 * - Blue bottom border indicates selected tab
 * - Text color matches for consistency
 */
.lineup-tab.active {
    border-color: var(--color-accent); /* Blue underline for active tab */
    color: var(--color-accent);        /* Blue text for active tab */
}

/*
 * Fade-in animation for dynamically loaded content
 * 
 * Why animate content loading:
 * - Smoother perceived performance
 * - Reduces jarring effect of instant DOM updates
 * - Professional polish for MVE
 * 
 * Usage:
 * - Applied to hub containers when they render
 * - Subtle 0.5s fade creates smooth transitions
 * 
 * Performance:
 * - Opacity animation is GPU-accelerated
 * - No layout thrashing (opacity doesn't trigger reflow)
 */
.fade-in {
    animation: fadeIn 0.5s ease-in-out;
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

/*
 * Additional mobile optimizations
 * (Claude's suggestion for notch support)
 * 
 * Why safe-area-inset:
 * - Modern iPhones have notches and camera cutouts
 * - env(safe-area-inset-bottom) adds padding to avoid the home indicator
 * - Ensures navigation buttons are fully touchable
 * 
 * Browser support:
 * - iOS Safari 11.0+
 * - Falls back gracefully (ignored by browsers that don't support it)
 */
nav {
    padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
}

/*
 * Smooth scrolling for better UX
 * (Claude's suggestion for polish)
 * 
 * Why this helps:
 * - Smooths anchor link navigation
 * - Better for in-hub content scrolling
 * - Native app feel
 * 
 * Browser support:
 * - All modern browsers (95%+ support)
 * - Gracefully ignored in older browsers
 */
html {
    scroll-behavior: smooth;
}

/*
 * Disabled card state for player cards without valid data
 * (Phase 3 Bug Fix: At-Bat Hub hardening)
 * 
 * Applied to player cards when no valid player ID exists
 * (e.g., closed games where current_play is null).
 * 
 * Visual indicators:
 * - opacity: 0.5 - Fades the card to indicate inactive state
 * - filter: grayscale(50%) - Desaturates color to reinforce disabled state
 * 
 * Behavioral protection:
 * - cursor: not-allowed - Shows "forbidden" cursor on hover
 * - pointer-events: none - CRITICAL: Completely disables all click/hover events
 *   This CSS property prevents user interaction at the browser level
 * 
 * Defense-in-depth strategy:
 * This CSS complements JavaScript validation (conditional event handler attachment
 * and showDeepDivePanel validation) to ensure invalid cards cannot be clicked
 * through any mechanism (user clicks, programmatic clicks, accessibility tools).
 */
.disabled-card {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
    filter: grayscale(50%);
}

/*
 * Strike Zone Grid Styles
 * 
 * Architecture:
 * - Outer wrapper: Large boundary for all pitches (balls + strikes)
 * - Inner 3x3 grid: Strike zone with dashed lines
 * - Grid cells use dashed borders with selective removal for clean edges
 * 
 * Visual design:
 * - Dashed lines: rgba(255, 255, 255, 0.4) - prominent but not overpowering
 * - Inner grid: 75% of wrapper size (25% padding creates outer zone)
 */
.strike-zone-wrapper {
    position: relative;
    width: 100%;
    height: 100%;
    border: 2px solid rgba(255, 255, 255, 0.2);
    border-radius: 0.25rem;
}

.strike-zone-inner {
    position: absolute;
    /* Position, width, and height set dynamically via inline styles based on API boundaries */
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(3, 1fr);
}

.strike-zone-cell {
    border: 1px dashed rgba(255, 255, 255, 0.4);
}

/*
 * Heartbeat Animation for Last Pitch
 * 
 * Purpose:
 * - Draws attention to most recent pitch
 * - Runs exactly 3 times when pitch is thrown or page loads
 * 
 * Animation:
 * - Pulsing scale effect (100% → 115% → 100%)
 * - 1.5s duration with ease-in-out timing
 * - Stops after 3 iterations (4.5s total)
 * 
 * Performance:
 * - Uses transform (GPU-accelerated)
 * - No layout thrashing
 */
@keyframes heartbeat {
    0%, 100% {
        transform: translate(-50%, -50%) scale(1);
    }
    50% {
        transform: translate(-50%, -50%) scale(1.15);
    }
}

.pitch-marker-last {
    animation: heartbeat 1.5s ease-in-out 3;
}

/* ============================================================================
 * MICROINTERACTIONS SYSTEM - Phase 1 & 2
 * ============================================================================
 * 
 * Philosophy: Every animation should communicate change, build tension, or
 * celebrate moments - never just decoration.
 * 
 * Performance: All animations use GPU-accelerated properties (transform, opacity)
 * and respect prefers-reduced-motion for accessibility.
 */

/* Accessibility: Disable animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* ---------------------------------------------------------------------------
 * PHASE 1: SCORE CHANGE ANIMATION
 * ---------------------------------------------------------------------------
 * When: A run scores (detected via polling)
 * Effect: Old score fades up/out, new score drops in, +1 badge appears
 */

@keyframes scoreChangeOut {
    0% {
        transform: scale(1);
        opacity: 1;
    }
    100% {
        transform: translateY(-20px) scale(1.2);
        opacity: 0;
    }
}

@keyframes scoreChangeIn {
    0% {
        transform: translateY(20px);
        opacity: 0;
    }
    60% {
        transform: translateY(-5px);
    }
    100% {
        transform: translateY(0);
        opacity: 1;
    }
}

@keyframes badgeAppear {
    0% {
        transform: scale(0) rotate(-10deg);
        opacity: 0;
    }
    50% {
        transform: scale(1.2) rotate(5deg);
    }
    100% {
        transform: scale(1) rotate(0);
        opacity: 1;
    }
}

.score-change-out {
    animation: scoreChangeOut 0.4s ease-out forwards;
}

.score-change-in {
    animation: scoreChangeIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}

.score-badge {
    display: inline-block;
    animation: badgeAppear 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}

/* ---------------------------------------------------------------------------
 * PHASE 1: TENSION ZONE GLOW (Leverage-based)
 * ---------------------------------------------------------------------------
 * When: Always active, intensity varies by leverage
 * Effect: Pulsing glow that gets faster/more intense with higher leverage
 */

@keyframes tensionGlowLow {
    0%, 100% {
        box-shadow: 0 0 10px rgba(59, 130, 246, 0.3),
                    0 0 20px rgba(59, 130, 246, 0.2),
                    inset 0 0 15px rgba(59, 130, 246, 0.1);
    }
    50% {
        box-shadow: 0 0 15px rgba(59, 130, 246, 0.5),
                    0 0 30px rgba(59, 130, 246, 0.3),
                    inset 0 0 20px rgba(59, 130, 246, 0.2);
    }
}

@keyframes tensionGlowMedium {
    0%, 100% {
        box-shadow: 0 0 15px rgba(168, 85, 247, 0.4),
                    0 0 30px rgba(168, 85, 247, 0.3),
                    inset 0 0 20px rgba(168, 85, 247, 0.15);
    }
    50% {
        box-shadow: 0 0 25px rgba(168, 85, 247, 0.6),
                    0 0 40px rgba(168, 85, 247, 0.4),
                    inset 0 0 30px rgba(168, 85, 247, 0.25);
    }
}

@keyframes tensionGlowHigh {
    0%, 100% {
        box-shadow: 0 0 20px rgba(251, 146, 60, 0.5),
                    0 0 40px rgba(251, 146, 60, 0.4),
                    inset 0 0 25px rgba(251, 146, 60, 0.2);
    }
    50% {
        box-shadow: 0 0 30px rgba(251, 146, 60, 0.8),
                    0 0 50px rgba(251, 146, 60, 0.6),
                    inset 0 0 35px rgba(251, 146, 60, 0.3);
    }
}

.tension-glow-low {
    animation: tensionGlowLow 3s ease-in-out infinite;
}

.tension-glow-medium {
    animation: tensionGlowMedium 2s ease-in-out infinite;
}

.tension-glow-high {
    animation: tensionGlowHigh 1s ease-in-out infinite;
}

/* ---------------------------------------------------------------------------
 * PHASE 1: INNING PROGRESSION LINE (Google-style)
 * ---------------------------------------------------------------------------
 * When: Half-inning changes
 * Effect: Line slides across with slinky elastic easing
 */

@keyframes inningLineSlideRight {
    0% {
        transform: translateX(-100%);
        opacity: 0;
    }
    20% {
        opacity: 1;
    }
    80% {
        opacity: 1;
    }
    100% {
        transform: translateX(100%);
        opacity: 0;
    }
}

@keyframes inningLineSlideLeft {
    0% {
        transform: translateX(100%);
        opacity: 0;
    }
    20% {
        opacity: 1;
    }
    80% {
        opacity: 1;
    }
    100% {
        transform: translateX(-100%);
        opacity: 0;
    }
}

.inning-line {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: linear-gradient(90deg, transparent, currentColor, transparent);
}

.inning-line-right {
    animation: inningLineSlideRight 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}

.inning-line-left {
    animation: inningLineSlideLeft 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}

/* ---------------------------------------------------------------------------
 * PHASE 1: AT-BAT CONCLUSION SEQUENCE
 * ---------------------------------------------------------------------------
 * When: Strikeout, hit, walk, etc.
 * Effect: Result text slides in, holds, fades out
 */

@keyframes resultSlideIn {
    0% {
        transform: scale(0.5) translateY(30px);
        opacity: 0;
    }
    100% {
        transform: scale(1) translateY(0);
        opacity: 1;
    }
}

@keyframes resultFadeOut {
    0% {
        opacity: 1;
    }
    100% {
        opacity: 0;
    }
}

.result-text-enter {
    animation: resultSlideIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}

.result-text-exit {
    animation: resultFadeOut 0.3s ease-out forwards;
}

/* ---------------------------------------------------------------------------
 * PHASE 2: COUNT UPDATE ANIMATIONS
 * ---------------------------------------------------------------------------
 * When: Balls/Strikes change
 * Effect: Circles materialize with ripple (balls) or snap (strikes)
 */

@keyframes ballRipple {
    0% {
        transform: scale(0);
        opacity: 0;
    }
    50% {
        transform: scale(1.3);
        opacity: 0.7;
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

@keyframes strikeSnap {
    0% {
        transform: scale(0);
        opacity: 0;
    }
    70% {
        transform: scale(1.15);
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

.ball-added {
    animation: ballRipple 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

.strike-added {
    animation: strikeSnap 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}

.strike-flash {
    animation: flash 0.2s ease-out;
}

@keyframes flash {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; background-color: rgba(239, 68, 68, 0.3); }
}

/* ---------------------------------------------------------------------------
 * PHASE 2: CURRENT BATTER HIGHLIGHT
 * ---------------------------------------------------------------------------
 * When: New batter comes to plate
 * Effect: Gradient highlight slides in, previous fades out
 */

@keyframes highlightSlideIn {
    0% {
        opacity: 0;
        transform: translateX(-10px);
    }
    100% {
        opacity: 1;
        transform: translateX(0);
    }
}

.current-batter-highlight {
    background: linear-gradient(90deg, rgba(59, 130, 246, 0.15), transparent);
    border-left: 3px solid rgb(59, 130, 246);
    animation: highlightSlideIn 0.4s ease-out forwards;
}

.batting-badge {
    animation: badgeAppear 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}

/* ---------------------------------------------------------------------------
 * PHASE 2: GAME IMPACT PULSE
 * ---------------------------------------------------------------------------
 * When: WPA score updates
 * Effect: Number glows and counts up with directional indicator
 */

@keyframes impactPulsePositive {
    0% {
        box-shadow: 0 0 0 rgba(34, 197, 94, 0);
        color: inherit;
    }
    50% {
        box-shadow: 0 0 15px rgba(34, 197, 94, 0.6);
        color: rgb(34, 197, 94);
    }
    100% {
        box-shadow: 0 0 0 rgba(34, 197, 94, 0);
        color: inherit;
    }
}

@keyframes impactPulseNegative {
    0% {
        box-shadow: 0 0 0 rgba(239, 68, 68, 0);
        color: inherit;
    }
    50% {
        box-shadow: 0 0 15px rgba(239, 68, 68, 0.6);
        color: rgb(239, 68, 68);
    }
    100% {
        box-shadow: 0 0 0 rgba(239, 68, 68, 0);
        color: inherit;
    }
}

.impact-pulse-positive {
    animation: impactPulsePositive 0.8s ease-out;
}

.impact-pulse-negative {
    animation: impactPulseNegative 0.8s ease-out;
}

/* ---------------------------------------------------------------------------
 * PHASE 2: TAB SWITCH UNDERLINE SLIDE
 * ---------------------------------------------------------------------------
 * When: User switches tabs in Deep Dive panel
 * Effect: Underline smoothly slides from old tab to new tab
 */

.panel-tab {
    position: relative;
    transition: color 0.2s ease;
}

.panel-tab.active::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: rgb(59, 130, 246);
    transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

/* ---------------------------------------------------------------------------
 * UTILITY: SMOOTH TRANSITIONS
 * ---------------------------------------------------------------------------
 * Applied to elements that need subtle state changes
 */

.smooth-transition {
    transition: all 0.3s ease;
}

.smooth-opacity {
    transition: opacity 0.3s ease;
}

/* ---------------------------------------------------------------------------
 * PITCH BAR ANIMATIONS (Strategic Hub)
 * ---------------------------------------------------------------------------
 * Staggered bar growth animation for Pitcher's Plan section
 */

.pitch-bar {
    transition: width 800ms cubic-bezier(0.4, 0.0, 0.2, 1);
}


/* ---------------------------------------------------------------------------
 * HEATMAP BOX ANIMATIONS (Hitter's Approach)
 * ---------------------------------------------------------------------------
 * Random box fill animation for 3x3 strike zone grid
 */

.heatmap-box {
    transition: opacity 200ms ease-in;
}

.hitter-approach-text {
    transition: opacity 400ms ease-in;
}

