JIT
Grid - Untitled
Self-playing side-view pinball in a single HTML file. Pure CSS/JS, no assets. Features chrome ball physics, shadows, screen shake, and flashing bumpers. Auto-flippers play through 3 balls, updating the score until a 'GAME OVER' fade.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CSS Pinball</title> <style> :root { --table-width: 600; --table-height: 800; --table-aspect-ratio: var(--table-width) / var(--table-height); --ball-size: 25px; --flipper-width: 120px; --flipper-height: 20px; --bumper-size: 60px; }
    html, body {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100%;
        overflow: hidden;
        background-color: #1a1a2e;
        font-family: 'Courier New', Courier, monospace;
        color: #e0e1dd;
    }

    body {
        display: flex;
        justify-content: center;
        align-items: center;
    }

    .game-container {
        width: 100vmin;
        height: calc(100vmin / var(--table-aspect-ratio));
        max-width: calc(100vh * var(--table-aspect-ratio));
        max-height: 100vh;
        position: relative;
        transform-style: preserve-3d;
    }

    @keyframes screen-shake {
        0%, 100% { transform: translate(0, 0) rotate(0); }
        10% { transform: translate(-2px, -3px) rotate(-0.5deg); }
        20% { transform: translate(3px, 1px) rotate(0.5deg); }
        30% { transform: translate(-1px, 2px) rotate(-0.5deg); }
        40% { transform: translate(2px, -1px) rotate(0.5deg); }
        50% { transform: translate(-3px, 1px) rotate(0); }
    }

    .shake {
        animation: screen-shake 0.3s ease-in-out;
    }
    
    .pinball-table {
        width: 100%;
        height: 100%;
        background: linear-gradient(145deg, #2c3e50, #34495e);
        border: 15px solid #7f8c8d;
        border-bottom: none;
        box-shadow: inset 0 0 20px rgba(0,0,0,0.5), 0 10px 30px rgba(0,0,0,0.7);
        border-radius: 40px 40px 0 0;
        position: relative;
        overflow: hidden;
    }

    .ball {
        position: absolute;
        width: var(--ball-size);
        height: var(--ball-size);
        border-radius: 50%;
        background: radial-gradient(circle at 30% 30%, #ffffff, #silver, #7f8c8d);
        box-shadow: inset -5px -5px 10px rgba(0,0,0,0.3);
        filter: drop-shadow(3px 3px 5px rgba(0,0,0,0.6));
        z-index: 10;
    }
    
    .flipper {
        position: absolute;
        width: var(--flipper-width);
        height: var(--flipper-height);
        background: #e74c3c;
        border: 2px solid #c0392b;
        border-radius: var(--flipper-height);
        bottom: 60px;
        z-index: 5;
        box-shadow: 0 5px 10px rgba(0,0,0,0.4);
        transition: transform 0.07s ease-out;
    }

    #flipper-left {
        left: 28%;
        transform-origin: 10% 50%;
        transform: translateX(-50%) rotate(30deg);
    }
    #flipper-left.active {
        transform: translateX(-50%) rotate(-30deg);
    }

    #flipper-right {
        right: 28%;
        transform-origin: 90% 50%;
        transform: translateX(50%) rotate(-30deg);
    }
    #flipper-right.active {
        transform: translateX(50%) rotate(30deg);
    }

    .bumper {
        position: absolute;
        width: var(--bumper-size);
        height: var(--bumper-size);
        background: radial-gradient(circle, #f1c40f, #f39c12);
        border-radius: 50%;
        box-shadow: inset 0 0 10px #e67e22, 0 0 15px rgba(243, 156, 18, 0.5);
        z-index: 4;
    }

    @keyframes bumper-flash {
        0%, 100% {
            transform: scale(1);
            background: radial-gradient(circle, #f1c40f, #f39c12);
            box-shadow: inset 0 0 10px #e67e22, 0 0 15px rgba(243, 156, 18, 0.5);
        }
        50% {
            transform: scale(1.1);
            background: radial-gradient(circle, #fff, #f1c40f);
            box-shadow: inset 0 0 15px #f39c12, 0 0 40px rgba(255, 255, 255, 0.9);
        }
    }

    .bumper.flash {
        animation: bumper-flash 0.15s linear;
    }

    .bumper1 { top: 25%; left: 50%; transform: translateX(-50%); }
    .bumper2 { top: 40%; left: 25%; transform: translateX(-50%); }
    .bumper3 { top: 40%; left: 75%; transform: translateX(-50%); }

    .ui-display {
        position: absolute;
        top: 20px;
        left: 50%;
        transform: translateX(-50%);
        background: rgba(0,0,0,0.5);
        padding: 10px 20px;
        border-radius: 10px;
        text-align: center;
        font-size: 24px;
        letter-spacing: 2px;
        border: 2px solid #4a4a68;
        z-index: 20;
    }

    .game-over-overlay {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0,0,0,0.7);
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 8vw;
        color: #e74c3c;
        text-shadow: 0 0 20px #c0392b;
        opacity: 0;
        visibility: hidden;
        transition: opacity 1.5s ease-in, visibility 1.5s;
        z-index: 100;
    }

    .game-over-overlay.show {
        opacity: 1;
        visibility: visible;
    }
    
    .side-wall {
        position: absolute;
        background: #8e44ad;
        box-shadow: inset 0 0 10px #2c3e50;
    }
    #wall-left { top: 30%; left: 5%; width: 5px; height: 55%; transform: rotate(10deg); }
    #wall-right { top: 30%; right: 5%; width: 5px; height: 55%; transform: rotate(-10deg); }
    .gutter {
        position: absolute;
        bottom: 0;
        width: 100%;
        height: 5px;
        background: transparent;
    }

</style>
</head> <body> <div class="game-container" id="game-container"> <div class="pinball-table" id="pinball-table"> <div class="ui-display"> <span id="score-display">SCORE: 0</span><br> <span id="balls-display">BALLS: 3</span> </div> <div class="ball" id="ball"></div>
        <div class="flipper" id="flipper-left"></div>
        <div class="flipper" id="flipper-right"></div>
        
        <div class="bumper bumper1" data-score="100"></div>
        <div class="bumper bumper2" data-score="100"></div>
        <div class="bumper bumper3" data-score="100"></div>

        <div class="side-wall" id="wall-left"></div>
        <div class="side-wall" id="wall-right"></div>

        <div class="gutter" id="gutter"></div>
    </div>
    <div class="game-over-overlay" id="game-over">GAME OVER</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
    const gameContainer = document.getElementById('game-container');
    const table = document.getElementById('pinball-table');
    const ballElement = document.getElementById('ball');
    const flipperLeft = document.getElementById('flipper-left');
    const flipperRight = document.getElementById('flipper-right');
    const bumpers = Array.from(document.querySelectorAll('.bumper'));
    const scoreDisplay = document.getElementById('score-display');
    const ballsDisplay = document.getElementById('balls-display');
    const gameOverDisplay = document.getElementById('game-over');

    let tableRect;
    let scaleFactor = 1;

    const ball = {
        x: 0, y: 0,
        vx: 0, vy: 0,
        radius: 0
    };

    const physics = {
        gravity: 0.25,
        friction: 0.998,
        flipperForce: -15,
        bumperForce: 1.2
    };

    let score = 0;
    let ballsRemaining = 3;
    let isGameOver = false;
    let ballInPlay = false;

    function updateDimensions() {
        tableRect = table.getBoundingClientRect();
        const originalWidth = 600; // From CSS var --table-width
        scaleFactor = tableRect.width / originalWidth;
        ball.radius = (parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--ball-size')) / 2) * scaleFactor;
    }

    function init() {
        updateDimensions();
        resetGame();
        window.addEventListener('resize', updateDimensions);
        gameLoop();
    }

    function resetGame() {
        score = 0;
        ballsRemaining = 3;
        isGameOver = false;
        ballInPlay = false;
        gameOverDisplay.classList.remove('show');
        updateUI();
        resetBall();
        setTimeout(launchBall, 1000);
    }

    function resetBall() {
        ball.x = tableRect.width * 0.9;
        ball.y = tableRect.height * 0.7;
        ball.vx = 0;
        ball.vy = 0;
        ballInPlay = false;
    }
    
    function launchBall() {
        if (isGameOver || ballInPlay) return;
        ballInPlay = true;
        ball.vy = -15 * scaleFactor;
        ball.vx = -3 * scaleFactor;
    }

    function updateUI() {
        scoreDisplay.textContent = `SCORE: ${score}`;
        ballsDisplay.textContent = `BALLS: ${ballsRemaining}`;
    }

    function gameOver() {
        isGameOver = true;
        gameOverDisplay.classList.add('show');
        setTimeout(resetGame, 5000);
    }

    function loseBall() {
        ballsRemaining--;
        ballInPlay = false;
        if (ballsRemaining < 0) {
            ballsRemaining = 0;
            gameOver();
        } else {
            updateUI();
            resetBall();
            setTimeout(launchBall, 2000);
        }
    }
    
    function activateFlipper(flipper) {
        if (flipper.cooldown) return;
        flipper.classList.add('active');
        flipper.cooldown = true;
        setTimeout(() => {
            flipper.classList.remove('active');
        }, 150);
         setTimeout(() => {
            flipper.cooldown = false;
        }, 300);
    }

    function autoPlay() {
        const ballBottom = ball.y + ball.radius;
        
        const flipperLeftRect = flipperLeft.getBoundingClientRect();
        const flipperLeftY = flipperLeftRect.top - tableRect.top + flipperLeftRect.height / 2;
        
        const flipperRightRect = flipperRight.getBoundingClientRect();
        const flipperRightY = flipperRightRect.top - tableRect.top + flipperRightRect.height / 2;

        if (ball.vy > 0) { // Ball is moving down
            // Left flipper AI
            if (ballBottom > flipperLeftY - 50 * scaleFactor && ballBottom < flipperLeftY + 20 * scaleFactor) {
                if (ball.x > flipperLeftRect.left - tableRect.left && ball.x < flipperLeftRect.right - tableRect.left + 20 * scaleFactor) {
                    activateFlipper(flipperLeft);
                }
            }
            // Right flipper AI
            if (ballBottom > flipperRightY - 50 * scaleFactor && ballBottom < flipperRightY + 20 * scaleFactor) {
                if (ball.x < flipperRightRect.right - tableRect.left && ball.x > flipperRightRect.left - tableRect.left - 20 * scaleFactor) {
                    activateFlipper(flipperRight);
                }
            }
        }
    }

    function handleCollisions() {
        // Wall collisions
        if (ball.x - ball.radius < 0) {
            ball.x = ball.radius;
            ball.vx *= -0.8;
        }
        if (ball.x + ball.radius > tableRect.width) {
            ball.x = tableRect.width - ball.radius;
            ball.vx *= -0.8;
        }
        if (ball.y - ball.radius < 0) {
            ball.y = ball.radius;
            ball.vy *= -0.8;
        }
        // Gutter
        if (ball.y + ball.radius > tableRect.height) {
            loseBall();
            return;
        }

        // Bumper collisions
        bumpers.forEach(bumper => {
            const bumperRect = bumper.getBoundingClientRect();
            const bumperX = bumperRect.left - tableRect.left + bumperRect.width / 2;
            const bumperY = bumperRect.top - tableRect.top + bumperRect.height / 2;
            const bumperRadius = bumperRect.width / 2;
            
            const dx = ball.x - bumperX;
            const dy = ball.y - bumperY;
            const distance = Math.sqrt(dx * dx + dy * dy);

            if (distance < ball.radius + bumperRadius) {
                if (!bumper.classList.contains('flash')) {
                    const scoreValue = parseInt(bumper.getAttribute('data-score')) || 50;
                    score += scoreValue;
                    updateUI();
                    bumper.classList.add('flash');
                    setTimeout(() => bumper.classList.remove('flash'), 150);
                    gameContainer.classList.remove('shake');
                    void gameContainer.offsetWidth; // Trigger reflow
                    gameContainer.classList.add('shake');
                }
                
                const angle = Math.atan2(dy, dx);
                const magnitude = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
                
                ball.vx = Math.cos(angle) * magnitude * physics.bumperForce;
                ball.vy = Math.sin(angle) * magnitude * physics.bumperForce;

                // Move ball out of bumper
                const overlap = ball.radius + bumperRadius - distance;
                ball.x += Math.cos(angle) * overlap;
                ball.y += Math.sin(angle) * overlap;
            }
        });

        // Flipper collisions
        [flipperLeft, flipperRight].forEach(flipper => {
            if (flipper.classList.contains('active')) {
                const flipperRect = flipper.getBoundingClientRect();
                const flipperY = flipperRect.top - tableRect.top;

                if (ball.y + ball.radius > flipperY && ball.y - ball.radius < flipperY + flipperRect.height) {
                    if (ball.x > flipperRect.left - tableRect.left && ball.x < flipperRect.right - tableRect.left) {
                       ball.vy = physics.flipperForce * scaleFactor;
                       if (flipper.id === 'flipper-left') {
                            ball.vx += (ball.x - (flipperRect.left - tableRect.left)) / (flipperRect.width) * 5;
                       } else {
                            ball.vx -= ((flipperRect.right - tableRect.left) - ball.x) / (flipperRect.width) * 5;
                       }
                       gameContainer.classList.remove('shake');
                       void gameContainer.offsetWidth; // Trigger reflow
                       gameContainer.classList.add('shake');
                    }
                }
            }
        });
    }
    
    function gameLoop() {
        if (isGameOver) {
             requestAnimationFrame(gameLoop);
             return;
        }

        if (ballInPlay) {
            ball.vy += physics.gravity * scaleFactor;
            ball.vx *= physics.friction;
            ball.vy *= physics.friction;

            ball.x += ball.vx;
            ball.y += ball.vy;

            handleCollisions();
            autoPlay();
        }

        ballElement.style.transform = `translate(${ball.x - ball.radius}px, ${ball.y - ball.radius}px)`;
        
        requestAnimationFrame(gameLoop);
    }

    init();
});
</script>
</body> </html>*
Updated files
v1