// Mario Bookmarklet Script - V6.5 (Comprehensive Comments)
(function() {
    // Prevent multiple instances of Mario on the page
    if (document.getElementById('pageMarioContainer')) {
        const existingMarioContainer = document.getElementById('pageMarioContainer');
        // If an instance exists but is hidden, show it. Otherwise, do nothing.
        if (existingMarioContainer.style.display === 'none') {
            existingMarioContainer.style.display = 'block';
        }
        return;
    }

    // --- CONSTANTS ---
    const SCALE_FACTOR = 5; // Visual and physics scaling factor. Mario appears 2x larger.

    /**
     * Parses the 5-digit hexadecimal physics values from "A Complete Guide to SMB's Physics Engine".
     * Format: BSSs.sss (Blocks, Pixels, Subpixels, SubSubpixels, SubSubSubpixels)
     * where each unit is 1/16th of the previous, starting from Pixels for sub-units.
     * Blocks are 16 pixels.
     * This function converts the guide's values into a single "guide pixel" unit for our physics.
     * @param {string} hexStr - The 5-digit hexadecimal string (e.g., "01900").
     * @returns {number} The value in "guide pixel" units.
     */
    function parsePhysicsValue(hexStr) {
        const b = parseInt(hexStr.charAt(0), 16);  // Blocks (guide refers to this as 0 for vel/accel)
        const p = parseInt(hexStr.charAt(1), 16);  // Pixels (s.-pixels)
        const s = parseInt(hexStr.charAt(2), 16);  // Subpixels (ss.-pixels, 1/16th of a pixel)
        const ss = parseInt(hexStr.charAt(3), 16); // SubSubpixels (1/256th of a pixel)
        const sss = parseInt(hexStr.charAt(4), 16);// SubSubSubpixels (1/4096th of a pixel)
        // The guide's "Blocks" digit (first char) for velocities/accelerations is usually 0.
        // Our interpretation: 1 Block = 16 Pixels.
        return (b * 16) + p + (s / 16.0) + (ss / 256.0) + (sss / 4096.0);
    }

    // Physics parameters, converted from the guide's hex values.
    // These represent values like player speed (Player_X_Speed = $57, Player_Y_Speed = $9f),
    // acceleration/friction (FrictionAdderLow = $0702), and jump forces (VerticalForce = $0709)
    // as described abstractly in SMBDIS.ASM.
    const PHYSICS = {
        maxWalkSpeed: parsePhysicsValue("01900"),       // Approx 1.56 guide pixels/frame
        walkingAcceleration: parsePhysicsValue("00098"), // Approx 0.037 guide pixels/frame^2
        maxRunSpeed: parsePhysicsValue("02900"),         // Approx 2.56 guide pixels/frame
        runningAcceleration: parsePhysicsValue("000E4"),  // Approx 0.056 guide pixels/frame^2
        releaseDeceleration: parsePhysicsValue("000D0"), // Approx 0.05 guide pixels/frame^2 (friction)
        skiddingDeceleration: parsePhysicsValue("001A0"),// Approx 0.1 guide pixels/frame^2

        // Jump velocities are negative because positive Y is downwards.
        jumpInitialVelocitySlow: -parsePhysicsValue("04000"), // For horizontal speeds (absVx) < speedThresholdFast (02500)
        jumpInitialVelocityFast: -parsePhysicsValue("05000"), // For absVx >= speedThresholdFast (02500)

        // Gravity values (positive Y is downwards) based on speed category at jump initiation.
        // Corresponds to "Holding 'A' Gravity" and "Falling Gravity" in the guide.
        gravityA_Held_Slow: parsePhysicsValue("00200"),      // absVx < speedThresholdMedium ("Less than 01000")
        gravityA_Held_Medium: parsePhysicsValue("001E0"),    // speedThresholdMedium <= absVx < speedThresholdFast ("01000 - 024FF")
        gravityA_Held_Fast: parsePhysicsValue("00280"),      // absVx >= speedThresholdFast ("02500 or greater")

        gravityFalling_Slow: parsePhysicsValue("00700"),
        gravityFalling_Medium: parsePhysicsValue("00600"),
        gravityFalling_Fast: parsePhysicsValue("00900"),
        
        terminalVelocity: parsePhysicsValue("04800"),        // Max falling speed, approx 4.5 guide pixels/frame

        // Speed thresholds from the guide for jump/gravity categorization.
        speedThresholdMedium: parsePhysicsValue("01000"),
        speedThresholdFast: parsePhysicsValue("02500")
    };

    // Sprite URLs (base64 encoded images)
    const SPRITES = {
        standing: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAATxJREFUSImlVbuNwzAMfRYyQ2rBG7i4LhPktnB78AyBZzCuzRbJBOlSaAPBdYY4XRFIICXSlp1XERI/jx9RDRTcOhvys283N/nZocYQAL4GA/Tluak2VsAoaQ4kxHRYCvfTnORL1wIARueTDADH3rOgidvPgHDpWqYcMTqf5Ne1Da9rGwoHmpHmNDoxMfpiwhkLiqKNIPnnhlT+nd4NYClIUST6FKyiMZU1I9qJYjRvnQ3S4IzOJ9oURQ3upxmYLJsJADg/LDDMIXfCQtV0I9cx2kWtk+o50JwU1To/rGok3Zk90SkLcRLXmDAG0qE0B9pSaejTBIDn9LcYMXek76pKmHzDbMGx9w1jsEZf0knRtyxUkKXaLBlHpaV7tYj0F5J+pIiPu3CIEYqRdlwx3w/iTtzFYO9jUp/zVvwDv6eCnFqRE4sAAAAASUVORK5CYII=)',
        crouch: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAR9JREFUSIntVLENwjAQvESZgdrKBi7SMYHZIm3kGRAzRLTZAiZIR5ENotQMgSmQLf/7baCj4KqP3/d/Z38M/PHHL6AqJS9aOb52WDbCaT4lAkBna6Cna/VXZAGJhVwBCYdlqxIL1/0W4qNuAQCnZQ0xAOz6NTQmugYLd9Qt2exxWtYQ36fW3afWEQuDhYu7Sp35N3K3IFmQlJxHVMGCmRXMrMgGXkhCFcv3ha77rUjy2PVregtcKj+DuAkANHH3UqEc5PEqwMwqxIOFEwvk/HtyPGziGdzGBwyUlCJkSBb4hnc5oiBI1y8VMTpbo0M6WLX39cm980ZmVi8LnMy7S2uEc9HK+T+sNBeDRdjn343kb/Q4j/SxyeXfTmJJDQA8AfHeg3BDDfSlAAAAAElFTkSuQmCC)',
        jump: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAURJREFUSImlVbuRgzAQfdJQA7GGDggucwVcF6QeamBcg0apuzAVkF1ABwzxFXFc4uVWq4+F70Vi2fd2pX0IhZP4vjc7ret+Vfod8m1Zj+cqlfxozZ56RyJj20CdIX4MYcOewKuq02X7n0BMrIoFAWBsG4DtlUAHSDg2dR2wj23jJccgc5JjpEopQYoX+0C2TlB4tp+qliICgLPwnXhb1oAQE+Uxb4y5TmJdOQv10ok02m42gZGchap4ZQCY8JfUzSZKJDIAaE6W4MTpsqGbTZBTNEZJpOqeQIkLY11oiFPna55Ma/lJ61RVTiZSNxt82Z+wgxwZQEAKBOp+VSXJEtcBu677VdFleZaMd24kaSqde1mCijuR2zbmuqiADBCxVCj5YyHi57IpZM4nKSBBQlj8ePZjOkgZHB04C+VwfgrI3Qcl+AX6GpjZUk8DmAAAAABJRU5ErkJggg==)',
        skid: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAWdJREFUSImdVbGRgzAQ3NO4ho8ZOlDgjAqeLpx6qMHzNTCk7gIq+MwBHXgUuwjrA8/hlZAE/o0Eul3tnY5DkMFoK5/ba2cnuj7o4tzBA8D3b5Xj4diZ1+L0fmeYvAdfp7vwswDA41p7APiZ7wCAi62hzzlHmoZRMhNj28fOYGrcsgbVyOy1zg7b2Yk6ELZ/sTVu/fNdLHqfq4fhID3h1j+XICbrvuJxrX3gQAs2NS4gx0QWDa5ktJVXchyYEhp6iDCZT8+dyPkHDvj0VN7cDxo39JADIsSn6c3EZMWuPpgal0wNcRFBbc2Ii5d0MNrKj7byardESjqIv3/txi2hrAAjlz9SjfSpyKqIPFxSs4CFgk7MiZTE2tnJaiZ+iqATSx/P1LjAhQ6UVSunBFmIRzpSRSz1w9Cv4+XcwevG3p8JI/gvtLOT1H3nyIuD3KYWrSiAjStM5b1KYSuohIAYO9kjHEyk/zj5A99XxHiqsqoTAAAAAElFTkSuQmCC)',
        walk: [
            'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAUJJREFUSInFVblxwzAQXGBcg2MOOkCgzBWAXSjloAaNatA4ZRd2BczRAYcxm4AD+TCHH1bijUBSt3d7uyKB/4ZoPfzSk0/vze6Iat5GCwHgYiVwje/JPxUXkEmoEZQwu0NkEr4/jnC+aQUAuLs9nAHg/bqHxtFci4W/aRX9mHB3ezifq/LnqnxGUCuqkZ6r8oFgsehq51MQijaC6U8L+fnzASH5Ra1LaXxCMYm0zBbIiYxgsfBmm8I1DxC3jxDtgIp5FvB4XpPEFE0b8Rsss01Vl8IELRuJxGh4kkRyIglce4kEAC54LreZxBZRiiwHPaQ5qS5xdIqoa+1dwG012xS91qr/hdFpujloTQMAIvW/ZyU9JxlDEqhribxLwLsShpc4u0PAAUbXY54RpF8eFBYHVyAYTWIK+UrxYuHJPflqZ8IPpxyJWPji/hAAAAAASUVORK5CYII=)',
            'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAATpJREFUSImlVLENwjAQPCxmoI6yQYp0TBC2oEWZAWWGiDZbwATpKLJBlJohCAWydbb/HSOuchz//b3v38Cf2Gk/7lWxhnunaYnO73MCAaBuDXCO9012sAJPkkYgwZbjlfA4Lm59rUoAQDfNbg0Ah/PsJXXaLi3Wa1V6hy26aXbr11Cur6FcIwItSCO1JMZmTxYcqGBENoLqDwN5feu/Bhj+kLJI8hnejdpStoLYiag1w16wTRTapypoxsL1QzMWAPWHLZVhtGAOtEQS9CYXINmdTaCp2CR4HBfUrVEncpOALZXsdQSpi2KENkcKcoiYRCxBI3n272hPHCYm6TAnSZ0C6cVlcIPx2eQscBCDW1q1MSdYVMAqQpLkMEnZm7HwLk+aBdUFzn7rsdPezUiSdFCS/hNBikhlzlXyAfB+icjL2GvBAAAAAElFTkSuQmCC)',
            'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAU5JREFUSImllLGRgzAQRb801EDMqAMCZ66A64LUQw0eamCc0sVdBWQO6EDjmCKsC3zLrKSVhH0/Ehrt213tRwoJfbeNC/e+1ocK96ojgQBwGjTQx/v6cHBCXkkpgCRqx2vh5/zY19fWAADG1e5rAKh76yXda7sMcNfWeIdJ42r39TYbt83GRYBUUApKEE3Zsw0HVXBFYwTrPwzk69v0GoDXgpRFKp/Lu1FqpRTEJxFZE8GdECwcXxIQmom80S2N9y9ss3F1b5XYAqlbGhEi+kAaJQ9MnTvsAwJyGHJOzInGPa42DwizhZDbBKVT5fPg06D3b3obyIkqdfM58XFWketa4D49ixDSR5dIugxwEeBodmpdgT0OJQB/8kgafz9K3Vt1n57ioZwiK3dL8xakknzAIaWxFn3AZy4liwAQ/v1sBSGZLHpU/zIS+JP2aRW/G1GlQvb7go8AAAAASUVORK5CYII=)'
        ]
    };

    // Sound effect URLs and player
    const SOUND_BASE_URL = 'https://raw.githubusercontent.com/mroyale/assets/refs/heads/legacy/audio/sfx/';
    const SOUNDS = {
        jumpBig: SOUND_BASE_URL + 'jump1.mp3',
        jumpSmall: SOUND_BASE_URL + 'jump0.mp3', // Will be used if mario.isBig is false
        bump: SOUND_BASE_URL + 'bump.mp3',
        loaded: SOUND_BASE_URL + 'item.mp3',    // Sound for Mario appearing on the page
        // Sounds for future implementation
        die: SOUND_BASE_URL + 'dead.mp3',
        powerupCollect: SOUND_BASE_URL + 'powerup.mp3'
    };

    /**
     * Plays a sound effect.
     * @param {string} soundKey - The key for the sound in the SOUNDS object.
     * @param {number} [volume=0.05] - Volume level (0.0 to 1.0).
     */
    function playSound(soundKey, volume = 0.05) { // Adjusted default volume
        if (SOUNDS[soundKey]) {
            const audio = new Audio(SOUNDS[soundKey]);
            audio.volume = volume; // Set volume BEFORE playing
            audio.play().catch(e => console.warn("Sound play failed for " + soundKey + ":", e.message));
        }
    }
    
    // Durations for each frame of the walk animation based on speed
    const ANIM_DURATIONS = {
        run_max: 2,    // When at/near max run speed
        run_accel: 3,  // When accelerating to run or running at medium speed
        walk_max: 4,   // When at/near max walk speed
        walk_accel: 7  // When walking slowly or accelerating to walk
    };

    // Mario's state object
    const mario = {
        el: null, // The DOM element for Mario
        // x, y are bottom-center of Mario in physics units (viewport-relative)
        x: (window.innerWidth / SCALE_FACTOR) / 2,
        y: (window.innerHeight / SCALE_FACTOR) - 5, 
        previousY: 0,       // Mario's y position at the start of the previous frame (for platform landing)
        vx: 0, vy: 0,       // Velocities in physics units per frame
        baseWidth: 16,      // Physics width
        standingBaseHeight: 32, // Physics hitbox height when standing (Big Mario)
        crouchingHitboxHeight: 16, // Physics hitbox height when crouching (Big Mario)
        currentHitboxHeight: 32,   // Current physics hitbox height, changes with crouching
        visualHeight: 32,      // Visual height of the div (fixed for Big Mario, matches standing)
        onGround: false,       // True if Mario is on a surface (floor or platform)
        facingRight: true,     // Direction Mario is visually facing
        isRunning: false,      // True if the run key ('Z') is held
        isBig: true,           // True if Mario is in his "Big" state (always true for now)
        isCrouching: false,    // True if Mario is currently crouching
        isSkidding: false,     // True if Mario is in a skidding state
        jumpCategory: 'slow',  // Determines jump physics ('slow', 'medium', 'fast') based on speed at takeoff
        jumpStartedCrouched: false, // True if a jump was initiated while crouching
        jumpKeyPressedLastFrame: false, // Tracks if jump key was pressed in the previous frame to prevent auto-jump
        
        // Animation state
        currentSpriteKey: 'standing', // Key for SPRITES object (e.g., 'standing', 'walk', 'jump')
        animationFrameIndex: 0,    // Current frame index for multi-frame animations (like walk)
        animationTimer: 0         // Timer to control duration of the current animation frame
    };
    mario.currentHitboxHeight = mario.standingBaseHeight;
    mario.visualHeight = mario.standingBaseHeight;
    mario.previousY = mario.y;

    // Input handling state
    const keys = {};
    let lastScrollY = window.scrollY; // Tracks last vertical page scroll position
    let lastScrollX = window.scrollX; // Tracks last horizontal page scroll position

    /**
     * Handles key down events.
     * Sets the corresponding key state and prevents default browser action for game keys.
     */
    function handleKeyDown(e) {
        keys[e.code] = true; 
        // Prevent default browser behavior (like page scrolling) for game control keys
        if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Space", "KeyX", "KeyZ"].includes(e.code)) {
            e.preventDefault();
        }
    }
    /**
     * Handles key up events.
     */
    function handleKeyUp(e) { keys[e.code] = false; }

    /**
     * Initializes Mario, sets up the DOM element, and starts the game loop.
     */
    function init() {
        // Create a container for Mario to live in (helps with z-index and positioning context)
        const container = document.createElement('div'); container.id = 'pageMarioContainer';
        container.style.cssText = 'position:fixed; left:0; top:0; width:100%; height:100%; pointer-events:none; z-index:99999;';
        
        // Create Mario's DOM element
        mario.el = document.createElement('div'); mario.el.id = 'pageMario';
        mario.el.style.position = 'absolute'; 
        mario.el.style.backgroundImage = SPRITES.standing; // Set initial sprite
        mario.el.style.backgroundRepeat = 'no-repeat'; 
        mario.el.style.backgroundSize = '100% 100%'; // Stretch the 16x32 sprite to fill the div
        mario.el.style.imageRendering = 'pixelated';  // For crisp pixel art
        // CSS hack to promote to its own compositing layer, can help prevent visual artifacts/smearing
        mario.el.style.transform = 'translateZ(0px)'; 
        
        // Set visual dimensions based on base size and scale factor
        mario.el.style.width = (mario.baseWidth * SCALE_FACTOR) + 'px';
        mario.el.style.height = (mario.visualHeight * SCALE_FACTOR) + 'px'; // Visual height is Big Mario's standing height
        
        container.appendChild(mario.el); document.body.appendChild(container);
        
        // Add event listeners for keyboard input
        document.addEventListener('keydown', handleKeyDown);
        document.addEventListener('keyup', handleKeyUp);

        // Initialize scroll tracking and Mario's starting position (viewport-relative)
        lastScrollY = window.scrollY; lastScrollX = window.scrollX;
        mario.y = (window.innerHeight / SCALE_FACTOR) - 5; // Bottom edge 5 physics units above viewport bottom
        mario.x = (window.innerWidth / SCALE_FACTOR) / 2;   // Horizontal center of viewport
        mario.previousY = mario.y;
        
        playSound('loaded'); // Play sound when Mario is initialized
        gameLoop(); // Start the main game loop
    }

    let gameLoopId = null; // ID for canceling the animation frame
    let platformQueryCounter = 0; // Counter to limit frequency of DOM queries for platforms
    const PLATFORM_QUERY_INTERVAL = 5; // Query for platforms every 5 frames
    let currentPlatforms = []; // Cached list of potential platforms

    /**
     * Main game loop, calls update and render.
     */
    function gameLoop() { 
        update(); 
        render(); 
        gameLoopId = requestAnimationFrame(gameLoop); 
    }

    /**
     * Updates Mario's physics, state, and handles collisions.
     */
    function update() {
        // --- Handle Page Scroll ---
        // Adjust Mario's viewport-relative physics position to compensate for page scrolling.
        // This makes him appear to stay in place relative to the page content.
        const currentScrollY = window.scrollY; const scrollDeltaY = currentScrollY - lastScrollY;
        if (scrollDeltaY !== 0) { const scaledScrollDelta = scrollDeltaY / SCALE_FACTOR; mario.y -= scaledScrollDelta; }
        lastScrollY = currentScrollY;
        const currentScrollX = window.scrollX; const scrollDeltaX = currentScrollX - lastScrollX;
        if (scrollDeltaX !== 0) { const scaledScrollDelta = scrollDeltaX / SCALE_FACTOR; mario.x -= scaledScrollDelta; }
        lastScrollX = currentScrollX;

        // Store Mario's y position from *before* this frame's physics are applied (but *after* scroll compensation).
        // Crucial for one-way platform collision detection.
        mario.previousY = mario.y; 
        
        // --- Read Inputs ---
        const onLeft = keys['ArrowLeft']; const onRight = keys['ArrowRight']; const onDown = keys['ArrowDown'];
        const onJumpKey = keys['KeyX']; const onRun = keys['KeyZ'];

        mario.isRunning = onRun;
        mario.isSkidding = false; // Reset skid state at the beginning of each frame

        // --- Crouching Logic (Big Mario only) ---
        // SMBDIS.ASM: CrouchingFlag = $0714, PlayerSize = $0754
        if (mario.isBig) {
            if (mario.onGround) { // Can only change crouch state if on the ground
                if (onDown && !mario.isCrouching) { // Pressing Down: Start crouching
                    mario.isCrouching = true; 
                    mario.currentHitboxHeight = mario.crouchingHitboxHeight; 
                } else if (!onDown && mario.isCrouching && !mario.jumpStartedCrouched) { // Releasing Down: Stop crouching
                    // TODO: Implement ceiling check here to prevent standing up into a block
                    mario.isCrouching = false; 
                    mario.currentHitboxHeight = mario.standingBaseHeight; 
                }
            }
            // Handle landing from a crouch-jump
            if (mario.onGround && mario.jumpStartedCrouched) {
                 mario.jumpStartedCrouched = false; // Reset flag
                 // Re-evaluate crouch state based on whether Down is still held
                 if (onDown) { mario.isCrouching = true; mario.currentHitboxHeight = mario.crouchingHitboxHeight;}
                 else { mario.isCrouching = false; mario.currentHitboxHeight = mario.standingBaseHeight; }
            }
        }

        // --- Horizontal Movement ---
        // Inspired by assembly logic for acceleration, max speed, and friction.
        // SMBDIS.ASM: Player_X_Speed = $57, RunningSpeed = $0703, FrictionAdderLow = $0702
        let targetVx = 0;
        let currentMaxSpeed = mario.isRunning ? PHYSICS.maxRunSpeed : PHYSICS.maxWalkSpeed;
        let currentAcceleration = mario.isRunning ? PHYSICS.runningAcceleration : PHYSICS.walkingAcceleration;
        // Cannot initiate new horizontal movement if crouching on the ground
        const canMoveHorizontally = !(mario.isCrouching && mario.onGround);

        if (canMoveHorizontally) {
            if (onLeft) { targetVx = -currentMaxSpeed; } 
            if (onRight) { targetVx = currentMaxSpeed; }
            if (onLeft && onRight) { targetVx = mario.facingRight ? currentMaxSpeed : -currentMaxSpeed; } // If both, maintain current direction

            // Mid-Air Direction Lock: Only change facing direction if on ground and a new direction is input.
            if (mario.onGround) {
                if (onLeft && !onRight) mario.facingRight = false; 
                if (onRight && !onLeft) mario.facingRight = true;
            }
        }
        
        if (targetVx !== 0 && canMoveHorizontally) {
            // Skidding: If trying to move opposite to current velocity and not stationary.
            if (Math.sign(targetVx) !== Math.sign(mario.vx) && mario.vx !== 0) {
                mario.vx += Math.sign(targetVx) * PHYSICS.skiddingDeceleration; 
                mario.isSkidding = true;
                // During skid, if on ground, face the new direction of movement intention.
                if (mario.onGround) {
                    if (onLeft) mario.facingRight = false;
                    if (onRight) mario.facingRight = true;
                }
            } else { // Normal acceleration
                mario.vx += Math.sign(targetVx) * currentAcceleration; 
            }
            // Cap speed
            if (targetVx > 0) mario.vx = Math.min(mario.vx, targetVx); 
            else mario.vx = Math.max(mario.vx, targetVx);
        } else { // Decelerating (friction)
            const decel = PHYSICS.releaseDeceleration;
            if (Math.abs(mario.vx) > decel) mario.vx -= Math.sign(mario.vx) * decel; 
            else mario.vx = 0;
        }

        // --- Vertical Movement (Jumping) ---
        // SMBDIS.ASM: VerticalForce = $0709 (initial jump), VerticalForceDown = $070a (gravity)
        // Jump only if key is newly pressed (transition from not pressed to pressed)
        if (mario.onGround && onJumpKey && !mario.jumpKeyPressedLastFrame) {
            const currentAbsSpeed = Math.abs(mario.vx); // Used to determine jump height category
            // Select initial jump velocity based on current horizontal speed (from guide)
            if (currentAbsSpeed >= PHYSICS.speedThresholdFast) { 
                mario.vy = PHYSICS.jumpInitialVelocityFast; mario.jumpCategory = 'fast'; 
            } else { 
                mario.vy = PHYSICS.jumpInitialVelocitySlow; // For both slow and medium initial jump height
                mario.jumpCategory = (currentAbsSpeed >= PHYSICS.speedThresholdMedium) ? 'medium' : 'slow'; 
            }
            if (mario.isCrouching) mario.jumpStartedCrouched = true; // Flag if jump started from crouch
            mario.onGround = false; // Mario is now in the air
            playSound(mario.isBig ? 'jumpBig' : 'jumpSmall'); // Play appropriate jump sound
        }
        mario.jumpKeyPressedLastFrame = onJumpKey; // Store jump key state for next frame
        
        // If crouch-jumping, ensure hitbox and crouching state remain small/active
        if (mario.jumpStartedCrouched) { 
            mario.currentHitboxHeight = mario.crouchingHitboxHeight; 
            mario.isCrouching = true; 
        }

        // --- Apply Gravity ---
        if (!mario.onGround) {
            let appliedGravity, heldGravity;
            // Select gravity values based on speed category at the *start* of the jump
            if (mario.jumpCategory === 'fast') { heldGravity = PHYSICS.gravityA_Held_Fast; appliedGravity = PHYSICS.gravityFalling_Fast; }
            else if (mario.jumpCategory === 'medium') { heldGravity = PHYSICS.gravityA_Held_Medium; appliedGravity = PHYSICS.gravityFalling_Medium; }
            else { heldGravity = PHYSICS.gravityA_Held_Slow; appliedGravity = PHYSICS.gravityFalling_Slow; }
            
            // Apply less gravity if jump key is held and Mario is rising (variable jump height)
            if (onJumpKey && mario.vy < 0) mario.vy += heldGravity; 
            else mario.vy += appliedGravity; // Apply normal falling gravity
        }
        // Apply terminal velocity
        if (mario.vy > PHYSICS.terminalVelocity) mario.vy = PHYSICS.terminalVelocity;

        // --- Update Position from Velocities ---
        mario.x += mario.vx; 
        mario.y += mario.vy; 
        // Assume Mario is in the air until a collision with ground/platform sets onGround = true
        mario.onGround = false; 

        // --- Platform Collision Detection ---
        platformQueryCounter++;
        if (platformQueryCounter >= PLATFORM_QUERY_INTERVAL) {
            platformQueryCounter = 0;
            const allElements = document.querySelectorAll('div, p, span, h1, h2, h3, h4, h5, h6, li, a, button, img, article, section, header, footer, nav, main, td, th, label, input, textarea');
            currentPlatforms = [];
            for (const el of allElements) { 
                 const rect = el.getBoundingClientRect(); // Viewport-relative screen pixels
                // Filter elements to be potential platforms
                if (rect.width > 10 && rect.height > 2 && // Minimum dimensions
                    rect.width < window.innerWidth * 1.5 && rect.height < window.innerHeight * 0.75 && // Avoid huge layout blocks
                    el.offsetParent !== null) { // Element is rendered
                    const style = window.getComputedStyle(el);
                    if (style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) > 0.1) {
                        currentPlatforms.push({ // Store platform coords in physics units
                            top: (rect.top / SCALE_FACTOR), bottom: (rect.bottom / SCALE_FACTOR),
                            left: (rect.left / SCALE_FACTOR), right: (rect.right / SCALE_FACTOR), el: el
                        });
                    }
                }
            }
            currentPlatforms.sort((a, b) => a.top - b.top); // Sort by top edge
        }
        
        let landedOnPlatformThisFrame = false;
        for (const platform of currentPlatforms) {
            const marioLeft = mario.x - (mario.baseWidth / 2); 
            const marioRight = mario.x + (mario.baseWidth / 2);
            // mario.y is current bottom, mario.previousY is previous bottom (both viewport-relative physics units)
            // platform.top is also viewport-relative physics units
            const landingTolerance = Math.max(5, Math.abs(mario.vy) * 1.1); // Tolerance for landing if falling fast

            // One-way platform collision conditions:
            if (mario.vy >= 0 && // Moving down or stationary
                mario.previousY <= platform.top && // Previous bottom was at or above platform's top
                mario.y >= platform.top &&         // Current bottom is at or below platform's top
                mario.y <= platform.top + landingTolerance && // Current bottom is not too far below (passed through)
                marioRight > platform.left &&      // Horizontal overlap
                marioLeft < platform.right) {

                mario.y = platform.top; // Snap to platform top
                mario.vy = 0;
                mario.onGround = true;
                landedOnPlatformThisFrame = true;
                // Handle crouch state on landing
                if (mario.isBig && mario.jumpStartedCrouched) {
                     mario.jumpStartedCrouched = false;
                     if (onDown) { mario.isCrouching = true; mario.currentHitboxHeight = mario.crouchingHitboxHeight;}
                     else { mario.isCrouching = false; mario.currentHitboxHeight = mario.standingBaseHeight; }
                }
                break; // Stop checking other platforms for this frame
            }
        }
        
        // --- Viewport Floor Collision ---
        const worldFloor = (window.innerHeight / SCALE_FACTOR); // Viewport bottom in physics units
        if (!landedOnPlatformThisFrame && mario.y >= worldFloor) {
            mario.y = worldFloor; 
            mario.vy = 0; 
            mario.onGround = true;
            // Handle crouch state on landing
            if (mario.isBig && mario.jumpStartedCrouched) {
                mario.jumpStartedCrouched = false;
                if (onDown) { mario.isCrouching = true; mario.currentHitboxHeight = mario.crouchingHitboxHeight;}
                else { mario.isCrouching = false; mario.currentHitboxHeight = mario.standingBaseHeight; }
           }
        }
        
        // --- Skid on Landing Logic ---
        // If just landed (onGround is true now) and wasn't already skidding from input,
        // and horizontal velocity is opposite to facing direction.
        if (mario.onGround && !mario.isSkidding && mario.vx !== 0 && 
            ((mario.facingRight && mario.vx < 0) || (!mario.facingRight && mario.vx > 0)) 
           ) {
             mario.isSkidding = true;
        }

        // --- Viewport Edge Collisions (Ceiling, Left/Right Walls) ---
        const worldRightEdge = (window.innerWidth / SCALE_FACTOR); 
        const worldLeftEdge = 0; 
        const worldCeiling = 0; // Viewport top in physics units

        // Ceiling collision (uses current hitbox height)
        if (mario.y - mario.currentHitboxHeight < worldCeiling) {
            mario.y = worldCeiling + mario.currentHitboxHeight; // Correct position
            if (mario.vy < 0) { // If was moving up
                mario.vy = 0; // Bonk
                playSound('bump');
            }
        }
        // Side wall collisions
        if (mario.x - (mario.baseWidth / 2) < worldLeftEdge) { mario.x = worldLeftEdge + (mario.baseWidth / 2); mario.vx = 0; }
        if (mario.x + (mario.baseWidth / 2) > worldRightEdge) { mario.x = worldRightEdge - (mario.baseWidth / 2); mario.vx = 0; }

        // --- Animation State Logic ---
        // SMBDIS.ASM: PlayerAnimTimer = $0781, PlayerAnimCtrl = $070d
        if (mario.isCrouching || mario.jumpStartedCrouched) {
            mario.currentSpriteKey = 'crouch';
        } else if (!mario.onGround) {
            mario.currentSpriteKey = 'jump';
        } else if (mario.isSkidding) {
            mario.currentSpriteKey = 'skid';
        } else if (mario.vx !== 0) { // Walking or Running
            mario.currentSpriteKey = 'walk';
            mario.animationTimer--;
            if (mario.animationTimer <= 0) {
                mario.animationFrameIndex = (mario.animationFrameIndex + 1) % 3; // Cycle Walk1, Walk2, Walk3 (0, 1, 2)
                
                const absVx = Math.abs(mario.vx);
                // Set animation frame duration based on current speed
                if (absVx >= PHYSICS.maxRunSpeed * 0.85) { // Approx max run speed
                    mario.animationTimer = ANIM_DURATIONS.run_max;
                } else if (absVx > PHYSICS.maxWalkSpeed * 0.90) { // Running or fast walk (threshold between walk and run_accel)
                    mario.animationTimer = ANIM_DURATIONS.run_accel;
                } else if (absVx > PHYSICS.maxWalkSpeed * 0.50) { // Medium to max walk speed
                    mario.animationTimer = ANIM_DURATIONS.walk_max;
                } else if (absVx > 0.01) { // Slow walk / just starting
                    mario.animationTimer = ANIM_DURATIONS.walk_accel;
                }
                 // If vx becomes 0, next state will be 'standing', resetting animTimer explicitly isn't critical here
            }
        } else { // Idle
            mario.currentSpriteKey = 'standing';
            mario.animationFrameIndex = 0; // Reset walk frame index when idle
        }
    }

    /**
     * Renders Mario on the screen based on his current state.
     */
    function render() {
        if (!mario.el) return;

        // Div visual height is always Big Mario's standing height
        const renderDivHeight = mario.visualHeight * SCALE_FACTOR; 
        const renderDivWidth = mario.baseWidth * SCALE_FACTOR;

        // Calculate CSS top-left from Mario's physics bottom-center coordinates
        const renderX = (mario.x - mario.baseWidth / 2) * SCALE_FACTOR;
        // Mario's y is bottom, visualHeight is the full height of the sprite/div
        const renderY = (mario.y - mario.visualHeight) * SCALE_FACTOR; 

        mario.el.style.left = renderX + 'px';
        mario.el.style.top = renderY + 'px';
        mario.el.style.width = renderDivWidth + 'px';
        mario.el.style.height = renderDivHeight + 'px';
        
        // Flip sprite based on direction, and include translateZ for layer promotion
        const scaleXValue = mario.facingRight ? '1' : '-1';
        mario.el.style.transform = `scaleX(${scaleXValue}) translateZ(0px)`;

        // Select current sprite image
        let currentSpriteImage = SPRITES.standing; // Default
        if (mario.currentSpriteKey === 'walk') {
            currentSpriteImage = SPRITES.walk[mario.animationFrameIndex];
        } else if (SPRITES[mario.currentSpriteKey]) { // For single-frame states like jump, crouch, skid
            currentSpriteImage = SPRITES[mario.currentSpriteKey];
        }
        
        mario.el.style.backgroundImage = currentSpriteImage;
        // Each sprite is 16x32. The div will be sized to show this.
        // '100% 100%' makes the sprite image fill the div.
        mario.el.style.backgroundSize = '100% 100%';
    }
    
    init(); // Start Page Mario!
})();