// TetrisApp.cpp - Main application runtime split out from main.cpp.
//
// This file is intentionally "orchestration-heavy": it wires together SDL, audio,
// asset loading, and the state machine. Keep gameplay mechanics in the gameplay/
// and states/ modules.

#include "app/TetrisApp.h"

#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>

#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <mutex>
#include <random>
#include <sstream>
#include <string>
#include <thread>
#include <vector>

#include "app/AssetLoader.h"
#include "app/BackgroundManager.h"
#include "app/Fireworks.h"
#include "app/TextureLoader.h"

#include "audio/Audio.h"
#include "audio/MenuWrappers.h"
#include "audio/SoundEffect.h"
#include "audio/AudioManager.h"

#include "core/Config.h"
#include "core/Settings.h"
#include "core/state/StateManager.h"

#include "gameplay/core/Game.h"
#include "gameplay/coop/CoopGame.h"
#include "gameplay/coop/CoopAIController.h"
#include "gameplay/effects/LineEffect.h"

#include "graphics/effects/SpaceWarp.h"
#include "graphics/effects/Starfield.h"
#include "graphics/effects/Starfield3D.h"
#include "graphics/renderers/GameRenderer.h"
#include "graphics/renderers/RenderPrimitives.h"
#include "graphics/ui/Font.h"
#include "graphics/ui/HelpOverlay.h"

#include "network/CoopNetButtons.h"
#include "network/NetSession.h"

#include "persistence/Scores.h"

#include "states/LevelSelectorState.h"
#include "states/LoadingManager.h"
#include "states/LoadingState.h"
#include "states/MenuState.h"
#include "states/OptionsState.h"
#include "states/PlayingState.h"
#include "states/VideoState.h"
#include "states/State.h"

#include "ui/BottomMenu.h"
#include "../resources/AssetPaths.h"
#include "ui/MenuLayout.h"

#include "utils/ImagePathResolver.h"
#include "../resources/ResourceManager.h"

// ---------- Game config ----------
static constexpr int LOGICAL_W = 1200;
static constexpr int LOGICAL_H = 1000;
static constexpr int WELL_W = Game::COLS * Game::TILE;
static constexpr int WELL_H = Game::ROWS * Game::TILE;

#include "ui/UIConstants.h"

static const std::array<SDL_Color, PIECE_COUNT + 1> COLORS = {{
    SDL_Color{20, 20, 26, 255},  // 0 empty
    SDL_Color{0, 255, 255, 255}, // I
    SDL_Color{255, 255, 0, 255}, // O
    SDL_Color{160, 0, 255, 255}, // T
    SDL_Color{0, 255, 0, 255},   // S
    SDL_Color{255, 0, 0, 255},   // Z
    SDL_Color{0, 0, 255, 255},   // J
    SDL_Color{255, 160, 0, 255}, // L
}};

static std::string GetLevelStoryText(int level) {
    int lvl = std::clamp(level, 1, 100);

    // Milestones
    switch (lvl) {
        case 1: return "Launch log: training run, light debris ahead.";
        case 25: return "Checkpoint: dense field reported, shields ready.";
        case 50: return "Midway brief: hull stress rising, stay sharp.";
        case 75: return "Emergency corridor: comms unstable, proceed blind.";
        case 100: return "Final anomaly: unknown mass ahead, hold course.";
        default: break;
    }

    struct Pool { int minL, maxL; std::vector<std::string> lines; };
    static const std::vector<Pool> pools = {
        {1, 10, {
            "Departure logged: light debris, stay on vector.",
            "Training sector: minimal drift, keep sensors warm.",
            "Calm approach: verify thrusters and nav locks.",
            "Outer ring dust: watch for slow movers.",
            "Clear lanes ahead: focus on smooth rotations."
        }},
        {11, 25, {
            "Asteroid belt thickening; micro-impacts likely.",
            "Density rising: plot short burns only.",
            "Field report: medium fragments, unpredictable spin.",
            "Warning: overlapping paths, reduce horizontal drift.",
            "Rock chorus ahead; keep payload stable."
        }},
        {26, 40, {
            "Unstable sector: abandoned relays drifting erratic.",
            "Salvage echoes detected; debris wakes may tug.",
            "Hull groans recorded; inert structures nearby.",
            "Navigation buoys dark; trust instruments only.",
            "Magnetic static rising; expect odd rotations."
        }},
        {41, 60, {
            "Core corridor: heavy asteroids, minimal clearance.",
            "Impact risk high: armor checks recommended.",
            "Dense stone flow; time burns carefully.",
            "Grav eddies noted; blocks may drift late.",
            "Core shards are brittle; expect sudden splits."
        }},
        {61, 80, {
            "Critical zone: alarms pinned, route unstable.",
            "Emergency pattern: glide, then cut thrust.",
            "Sensors flare; debris ionized, visibility low.",
            "Thermals spiking; keep pieces tight and fast.",
            "Silent channel; assume worst-case collision."
        }},
        {81, 100, {
            "Unknown space: signals warp, gravity unreliable.",
            "Anomaly bloom ahead; shapes flicker unpredictably.",
            "Final drift: void sings through hull plates.",
            "Black sector: map useless, fly by instinct.",
            "Edge of chart: nothing responds, just move."
        }}
    };

    for (const auto& pool : pools) {
        if (lvl >= pool.minL && lvl <= pool.maxL && !pool.lines.empty()) {
            size_t idx = static_cast<size_t>((lvl - pool.minL) % pool.lines.size());
            return pool.lines[idx];
        }
    }

    return "Mission log update unavailable.";
}

struct TetrisApp::Impl {
    // Global collector for asset loading errors shown on the loading screen
    std::vector<std::string> assetLoadErrors;
    std::mutex assetLoadErrorsMutex;
    // Loading counters for progress UI and debug overlay
    std::atomic<int> totalLoadingTasks{0};
    std::atomic<int> loadedTasks{0};
    std::string currentLoadingFile;
    std::mutex currentLoadingMutex;

    // Intro/Menu shared state (wired into StateContext as pointers)
    double logoAnimCounter = 0.0;
    bool showSettingsPopup = false;
    bool showHelpOverlay = false;
    bool showExitConfirmPopup = false;
    int exitPopupSelectedButton = 1; // 0 = YES, 1 = NO
    bool musicEnabled = true;
    int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings
    bool isNewHighScore = false;
    std::string playerName;
    std::string player2Name;
    int highScoreEntryIndex = 0; // 0 = entering player1, 1 = entering player2
    bool helpOverlayPausedGame = false;

    SDL_Window* window = nullptr;
    SDL_Renderer* renderer = nullptr;

    AssetLoader assetLoader;
    std::unique_ptr<LoadingManager> loadingManager;
    std::unique_ptr<TextureLoader> textureLoader;
    resources::ResourceManager resourceManager;

    FontAtlas pixelFont;
    FontAtlas font;

    ScoreManager scores;
    std::atomic<bool> scoresLoadComplete{false};
    std::jthread scoreLoader;
    std::jthread menuTrackLoader;

    Starfield starfield;
    Starfield3D starfield3D;
    SpaceWarp spaceWarp;
    SpaceWarpFlightMode warpFlightMode = SpaceWarpFlightMode::Forward;
    bool warpAutoPilotEnabled = true;

    LineEffect lineEffect;

    SDL_Texture* logoTex = nullptr;
    SDL_Texture* logoSmallTex = nullptr;
    int logoSmallW = 0;
    int logoSmallH = 0;
    SDL_Texture* backgroundTex = nullptr;
    SDL_Texture* mainScreenTex = nullptr;
    int mainScreenW = 0;
    int mainScreenH = 0;

    SDL_Texture* blocksTex = nullptr;
    SDL_Texture* asteroidsTex = nullptr;
    SDL_Texture* scorePanelTex = nullptr;
    SDL_Texture* statisticsPanelTex = nullptr;
    SDL_Texture* nextPanelTex = nullptr;
    SDL_Texture* holdPanelTex = nullptr;

    BackgroundManager levelBackgrounds;
    int startLevelSelection = 0;

    // Music loading tracking
    int totalTracks = 0;
    int currentTrackLoading = 0;
    bool musicLoaded = false;
    bool musicStarted = false;
    bool musicLoadingStarted = false;

    // Loader control: execute incrementally on main thread to avoid SDL threading issues
    std::atomic_bool loadingStarted{false};
    std::atomic_bool loadingComplete{false};
    std::atomic<size_t> loadingStep{0};

    std::unique_ptr<Game> game;
    std::unique_ptr<CoopGame> coopGame;
    std::vector<std::string> singleSounds;
    std::vector<std::string> doubleSounds;
    std::vector<std::string> tripleSounds;
    std::vector<std::string> tetrisSounds;
    bool suppressLineVoiceForLevelUp = false;
    bool skipNextLevelUpJingle = false;

    // COOPERATE option: when true, right player is AI-controlled.
    bool coopVsAI = false;

    CoopAIController coopAI;

    AppState state = AppState::Loading;
    double loadingProgress = 0.0;
    Uint64 loadStart = 0;
    bool running = true;
    bool isFullscreen = false;
    bool leftHeld = false;
    bool rightHeld = false;
    bool p1LeftHeld = false;
    bool p1RightHeld = false;
    bool p2LeftHeld = false;
    bool p2RightHeld = false;
    double moveTimerMs = 0.0;
    double p1MoveTimerMs = 0.0;
    double p2MoveTimerMs = 0.0;

    // Network coop fixed-tick state (used only when ctx.coopNetEnabled is true)
    double coopNetAccMs = 0.0;
    uint32_t coopNetCachedTick = 0xFFFFFFFFu;
    uint8_t coopNetCachedButtons = 0;
    uint32_t coopNetLastHashSentTick = 0xFFFFFFFFu;
    double DAS = 170.0;
    double ARR = 40.0;
    SDL_Rect logicalVP{0, 0, LOGICAL_W, LOGICAL_H};
    float logicalScale = 1.f;
    Uint64 lastMs = 0;

    enum class MenuFadePhase { None, FadeOut, FadeIn };
    MenuFadePhase menuFadePhase = MenuFadePhase::None;
    double menuFadeClockMs = 0.0;
    float menuFadeAlpha = 0.0f;
    double MENU_PLAY_FADE_DURATION_MS = 450.0;
    AppState menuFadeTarget = AppState::Menu;

    enum class CountdownSource { MenuStart, ChallengeLevel };
    bool menuPlayCountdownArmed = false;
    bool gameplayCountdownActive = false;
    double gameplayCountdownElapsed = 0.0;
    int gameplayCountdownIndex = 0;
    double GAMEPLAY_COUNTDOWN_STEP_MS = 400.0;
    std::array<const char*, 4> GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" };
    CountdownSource gameplayCountdownSource = CountdownSource::MenuStart;
    int countdownLevel = 0;
    int countdownGoalAsteroids = 0;
    bool countdownAdvancesChallenge = false;
    bool challengeCountdownWaitingForSpace = false;
    double gameplayBackgroundClockMs = 0.0;
    std::string challengeStoryText;
    int challengeStoryLevel = 0;
    float challengeStoryAlpha = 0.0f;
    double challengeStoryClockMs = 0.0;

    // Challenge clear FX (celebratory board explosion before countdown)
    bool challengeClearFxActive = false;
    double challengeClearFxElapsedMs = 0.0;
    double challengeClearFxDurationMs = 0.0;
    int challengeClearFxNextLevel = 0;
    std::vector<int> challengeClearFxOrder;
    std::mt19937 challengeClearFxRng{std::random_device{}()};

    std::unique_ptr<StateManager> stateMgr;
    StateContext ctx{};
    std::unique_ptr<LoadingState> loadingState;
    std::unique_ptr<VideoState> videoState;
    std::unique_ptr<MenuState> menuState;
    std::unique_ptr<OptionsState> optionsState;
    std::unique_ptr<LevelSelectorState> levelSelectorState;
    std::unique_ptr<PlayingState> playingState;

    // Startup fade-in overlay (used after intro video).
    bool startupFadeActive = false;
    float startupFadeAlpha = 0.0f; // 0..1 black overlay strength
    double startupFadeClockMs = 0.0;
    static constexpr double STARTUP_FADE_IN_MS = 650.0;

    // Intro video path.
    std::string introVideoPath = "assets/videos/spacetris_intro.mp4";

    int init();
    void runLoop();
    void shutdown();
};

TetrisApp::TetrisApp()
    : impl_(std::make_unique<Impl>())
{
}

TetrisApp::~TetrisApp() = default;

int TetrisApp::run()
{
    const int initRc = impl_->init();
    if (initRc != 0) {
        impl_->shutdown();
        return initRc;
    }

    impl_->runLoop();
    impl_->shutdown();
    return 0;
}

int TetrisApp::Impl::init()
{
    // Initialize random seed for procedural effects
    srand(static_cast<unsigned int>(SDL_GetTicks()));

    // Load settings
    Settings::instance().load();

    // Sync shared variables with settings
    musicEnabled = Settings::instance().isMusicEnabled();
    playerName = Settings::instance().getPlayerName();
    if (playerName.empty()) playerName = "Player";

    // Apply sound settings to manager
    SoundEffectManager::instance().setEnabled(Settings::instance().isSoundEnabled());

    int sdlInitRes = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    if (sdlInitRes < 0)
    {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed: %s", SDL_GetError());
        return 1;
    }
    int ttfInitRes = TTF_Init();
    if (ttfInitRes < 0)
    {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "TTF_Init failed");
        SDL_Quit();
        return 1;
    }

    SDL_WindowFlags windowFlags = SDL_WINDOW_RESIZABLE;
    if (Settings::instance().isFullscreen()) {
        windowFlags |= SDL_WINDOW_FULLSCREEN;
    }

    window = SDL_CreateWindow("SpaceTris (SDL3)", LOGICAL_W, LOGICAL_H, windowFlags);
    if (!window)
    {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed: %s", SDL_GetError());
        TTF_Quit();
        SDL_Quit();
        return 1;
    }
    renderer = SDL_CreateRenderer(window, nullptr);
    if (!renderer)
    {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateRenderer failed: %s", SDL_GetError());
        SDL_DestroyWindow(window);
        window = nullptr;
        TTF_Quit();
        SDL_Quit();
        return 1;
    }
    SDL_SetRenderVSync(renderer, 1);

    if (const char* basePathRaw = SDL_GetBasePath()) {
        std::filesystem::path exeDir(basePathRaw);
        AssetPath::setBasePath(exeDir.string());
#if defined(__APPLE__)
        // On macOS bundles launched from Finder start in /, so re-root relative paths.
        std::error_code ec;
        std::filesystem::current_path(exeDir, ec);
        if (ec) {
            SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "Failed to set working directory to %s: %s",
                exeDir.string().c_str(), ec.message().c_str());
        }
#endif
    } else {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
            "SDL_GetBasePath() failed; asset lookups rely on current directory: %s",
            SDL_GetError());
    }

    // Asset loader (creates SDL_Textures on the main thread)
    assetLoader.init(renderer);
    // Wire resource manager into loader so textures are cached and reused
    assetLoader.setResourceManager(&resourceManager);
    loadingManager = std::make_unique<LoadingManager>(&assetLoader);

    // Legacy image loader (used only as a fallback when AssetLoader misses)
    textureLoader = std::make_unique<TextureLoader>(
        loadedTasks,
        currentLoadingFile,
        currentLoadingMutex,
        assetLoadErrors,
        assetLoadErrorsMutex);
    // Let legacy TextureLoader access the same resource cache
    textureLoader->setResourceManager(&resourceManager);

    // Load scores asynchronously but keep the worker alive until shutdown
    scoreLoader = std::jthread([this]() {
        scores.load();
        scoresLoadComplete.store(true, std::memory_order_release);
    });

    starfield.init(200, LOGICAL_W, LOGICAL_H);
    starfield3D.init(LOGICAL_W, LOGICAL_H, 200);
    spaceWarp.init(LOGICAL_W, LOGICAL_H, 420);
    spaceWarp.setFlightMode(warpFlightMode);
    warpAutoPilotEnabled = true;
    spaceWarp.setAutoPilotEnabled(true);

    // Initialize line clearing effects
    lineEffect.init(renderer);

    game = std::make_unique<Game>(startLevelSelection);
    game->setGravityGlobalMultiplier(Config::Gameplay::GRAVITY_SPEED_MULTIPLIER);
    game->reset(startLevelSelection);

    coopGame = std::make_unique<CoopGame>(startLevelSelection);

    // Define voice line banks for gameplay callbacks
    singleSounds = {"well_played", "smooth_clear", "great_move"};
    doubleSounds = {"nice_combo", "you_fire", "keep_that_ryhtm"};
    tripleSounds = {"impressive", "triple_strike"};
    tetrisSounds = {"amazing", "you_re_unstoppable", "boom_tetris", "wonderful"};
    suppressLineVoiceForLevelUp = false;

    auto playVoiceCue = [this](int linesCleared) {
        const std::vector<std::string>* bank = nullptr;
        switch (linesCleared) {
            case 1: bank = &singleSounds; break;
            case 2: bank = &doubleSounds; break;
            case 3: bank = &tripleSounds; break;
            default:
                if (linesCleared >= 4) {
                    bank = &tetrisSounds;
                }
                break;
        }
        if (bank && !bank->empty()) {
            SoundEffectManager::instance().playRandomSound(*bank, 1.0f);
        }
    };

    game->setSoundCallback([this, playVoiceCue](int linesCleared) {
        if (linesCleared <= 0) {
            return;
        }

        SoundEffectManager::instance().playSound("clear_line", 1.0f);

        if (!suppressLineVoiceForLevelUp) {
            playVoiceCue(linesCleared);
        }
        suppressLineVoiceForLevelUp = false;
    });

    // Keep co-op line-clear SFX behavior identical to classic.
    coopGame->setSoundCallback([this, playVoiceCue](int linesCleared) {
        if (linesCleared <= 0) {
            return;
        }

        SoundEffectManager::instance().playSound("clear_line", 1.0f);

        if (!suppressLineVoiceForLevelUp) {
            playVoiceCue(linesCleared);
        }
        suppressLineVoiceForLevelUp = false;
    });

    game->setLevelUpCallback([this](int /*newLevel*/) {
        if (skipNextLevelUpJingle) {
            skipNextLevelUpJingle = false;
        } else {
            SoundEffectManager::instance().playSound("new_level", 1.0f);
            SoundEffectManager::instance().playSound("lets_go", 1.0f);
        }
        suppressLineVoiceForLevelUp = true;
    });

    // Mirror single-player level-up audio/visual behavior for Coop sessions
    coopGame->setLevelUpCallback([this](int /*newLevel*/) {
        if (skipNextLevelUpJingle) {
            skipNextLevelUpJingle = false;
        } else {
            SoundEffectManager::instance().playSound("new_level", 1.0f);
            SoundEffectManager::instance().playSound("lets_go", 1.0f);
        }
        suppressLineVoiceForLevelUp = true;
    });

    game->setAsteroidDestroyedCallback([](AsteroidType /*type*/) {
        SoundEffectManager::instance().playSound("asteroid_destroy", 0.9f);
    });

    state = AppState::Loading;
    loadingProgress = 0.0;
    loadStart = SDL_GetTicks();
    running = true;
    isFullscreen = Settings::instance().isFullscreen();
    leftHeld = false;
    rightHeld = false;
    p1LeftHeld = p1RightHeld = p2LeftHeld = p2RightHeld = false;
    moveTimerMs = 0;
    p1MoveTimerMs = 0.0;
    p2MoveTimerMs = 0.0;
    DAS = 170.0;
    ARR = 40.0;
    logicalVP = SDL_Rect{0, 0, LOGICAL_W, LOGICAL_H};
    logicalScale = 1.f;
    lastMs = SDL_GetPerformanceCounter();

    menuFadePhase = MenuFadePhase::None;
    menuFadeClockMs = 0.0;
    menuFadeAlpha = 0.0f;
    MENU_PLAY_FADE_DURATION_MS = 450.0;
    menuFadeTarget = AppState::Menu;
    menuPlayCountdownArmed = false;
    gameplayCountdownActive = false;
    gameplayCountdownElapsed = 0.0;
    gameplayCountdownIndex = 0;
    GAMEPLAY_COUNTDOWN_STEP_MS = 400.0;
    GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" };
    gameplayBackgroundClockMs = 0.0;

    // Instantiate state manager
    stateMgr = std::make_unique<StateManager>(state);

    // Prepare shared context for states
    ctx = StateContext{};
    ctx.stateManager = stateMgr.get();
    ctx.game = game.get();
    ctx.coopGame = coopGame.get();
    ctx.scores = nullptr;
    ctx.starfield = &starfield;
    ctx.starfield3D = &starfield3D;
    ctx.font = &font;
    ctx.pixelFont = &pixelFont;
    ctx.lineEffect = &lineEffect;
    ctx.logoTex = logoTex;
    ctx.logoSmallTex = logoSmallTex;
    ctx.logoSmallW = logoSmallW;
    ctx.logoSmallH = logoSmallH;
    ctx.backgroundTex = nullptr;
    ctx.asteroidsTex = asteroidsTex;
    ctx.blocksTex = blocksTex;
    ctx.scorePanelTex = scorePanelTex;
    ctx.statisticsPanelTex = statisticsPanelTex;
    ctx.nextPanelTex = nextPanelTex;
    ctx.mainScreenTex = mainScreenTex;
    ctx.mainScreenW = mainScreenW;
    ctx.mainScreenH = mainScreenH;
    ctx.musicEnabled = &musicEnabled;
    ctx.coopVsAI = &coopVsAI;
    ctx.startLevelSelection = &startLevelSelection;
    ctx.hoveredButton = &hoveredButton;
    ctx.showSettingsPopup = &showSettingsPopup;
    ctx.showHelpOverlay = &showHelpOverlay;
    ctx.showExitConfirmPopup = &showExitConfirmPopup;
    ctx.exitPopupSelectedButton = &exitPopupSelectedButton;
    ctx.gameplayCountdownActive = &gameplayCountdownActive;
    ctx.menuPlayCountdownArmed = &menuPlayCountdownArmed;
    ctx.skipNextLevelUpJingle = &skipNextLevelUpJingle;
    ctx.challengeClearFxActive = &challengeClearFxActive;
    ctx.challengeClearFxElapsedMs = &challengeClearFxElapsedMs;
    ctx.challengeClearFxDurationMs = &challengeClearFxDurationMs;
    ctx.challengeClearFxOrder = &challengeClearFxOrder;
    ctx.challengeStoryText = &challengeStoryText;
    ctx.challengeStoryLevel = &challengeStoryLevel;
    ctx.challengeStoryAlpha = &challengeStoryAlpha;
    ctx.playerName = &playerName;
    ctx.fullscreenFlag = &isFullscreen;
    ctx.applyFullscreen = [this](bool enable) {
        SDL_SetWindowFullscreen(window, enable ? SDL_WINDOW_FULLSCREEN : 0);
        isFullscreen = enable;
    };
    ctx.queryFullscreen = [this]() -> bool {
        return (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0;
    };
    ctx.requestQuit = [this]() {
        running = false;
    };

    auto beginStateFade = [this](AppState targetState, bool armGameplayCountdown) {
        if (!ctx.stateManager) {
            return;
        }
        if (state == targetState) {
            return;
        }
        if (menuFadePhase != MenuFadePhase::None) {
            return;
        }

        menuFadePhase = MenuFadePhase::FadeOut;
        menuFadeClockMs = 0.0;
        menuFadeAlpha = 0.0f;
        menuFadeTarget = targetState;
        menuPlayCountdownArmed = armGameplayCountdown;
        gameplayCountdownActive = false;
        gameplayCountdownIndex = 0;
        gameplayCountdownElapsed = 0.0;

        if (!armGameplayCountdown) {
            if (game) {
                game->setPaused(false);
            }
        }
    };

    auto startMenuPlayTransition = [this, beginStateFade]() {
        if (!ctx.stateManager) {
            return;
        }
        if (state != AppState::Menu) {
            if (game && game->getMode() == GameMode::Cooperate && coopGame && coopVsAI) {
                coopAI.reset();
            }
            state = AppState::Playing;
            ctx.stateManager->setState(state);
            return;
        }

        if (game && game->getMode() == GameMode::Cooperate && coopGame && coopVsAI) {
            coopAI.reset();
        }
        beginStateFade(AppState::Playing, true);
    };
    ctx.startPlayTransition = startMenuPlayTransition;

    auto requestStateFade = [this, startMenuPlayTransition, beginStateFade](AppState targetState) {
        if (!ctx.stateManager) {
            return;
        }
        if (targetState == AppState::Playing) {
            startMenuPlayTransition();
            return;
        }
        beginStateFade(targetState, false);
    };
    ctx.requestFadeTransition = requestStateFade;

    ctx.startupFadeActive = &startupFadeActive;
    ctx.startupFadeAlpha = &startupFadeAlpha;

    loadingState = std::make_unique<LoadingState>(ctx);
    videoState = std::make_unique<VideoState>(ctx);
    menuState = std::make_unique<MenuState>(ctx);
    optionsState = std::make_unique<OptionsState>(ctx);
    levelSelectorState = std::make_unique<LevelSelectorState>(ctx);
    playingState = std::make_unique<PlayingState>(ctx);

    stateMgr->registerHandler(AppState::Loading, [this](const SDL_Event& e){ loadingState->handleEvent(e); });
    stateMgr->registerOnEnter(AppState::Loading, [this](){ loadingState->onEnter(); loadingStarted.store(true); });
    stateMgr->registerOnExit(AppState::Loading, [this](){ loadingState->onExit(); });

    stateMgr->registerHandler(AppState::Video, [this](const SDL_Event& e){ if (videoState) videoState->handleEvent(e); });
    stateMgr->registerOnEnter(AppState::Video, [this]() {
        if (!videoState) return;
        const bool ok = videoState->begin(renderer, introVideoPath);
        if (!ok) {
            SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Intro video unavailable; skipping to Menu");
            state = AppState::Menu;
            stateMgr->setState(state);
            return;
        }
        videoState->onEnter();
    });
    stateMgr->registerOnExit(AppState::Video, [this](){ if (videoState) videoState->onExit(); });

    stateMgr->registerHandler(AppState::Menu, [this](const SDL_Event& e){ menuState->handleEvent(e); });
    stateMgr->registerOnEnter(AppState::Menu, [this](){ menuState->onEnter(); });
    stateMgr->registerOnExit(AppState::Menu, [this](){ menuState->onExit(); });

    stateMgr->registerHandler(AppState::Options, [this](const SDL_Event& e){ optionsState->handleEvent(e); });
    stateMgr->registerOnEnter(AppState::Options, [this](){ optionsState->onEnter(); });
    stateMgr->registerOnExit(AppState::Options, [this](){ optionsState->onExit(); });

    stateMgr->registerHandler(AppState::LevelSelector, [this](const SDL_Event& e){ levelSelectorState->handleEvent(e); });
    stateMgr->registerOnEnter(AppState::LevelSelector, [this](){ levelSelectorState->onEnter(); });
    stateMgr->registerOnExit(AppState::LevelSelector, [this](){ levelSelectorState->onExit(); });

    stateMgr->registerHandler(AppState::Playing, [this](const SDL_Event& e){ playingState->handleEvent(e); });
    stateMgr->registerOnEnter(AppState::Playing, [this](){ playingState->onEnter(); });
    stateMgr->registerOnExit(AppState::Playing, [this](){ playingState->onExit(); });

    loadingState->onEnter();
    loadingStarted.store(true);

    return 0;
}

void TetrisApp::Impl::runLoop()
{
    auto ensureScoresLoaded = [this]() {
        if (scoreLoader.joinable()) {
            scoreLoader.join();
        }
        if (!ctx.scores) {
            ctx.scores = &scores;
        }
    };

    auto startMenuPlayTransition = [this]() {
        if (ctx.startPlayTransition) {
            ctx.startPlayTransition();
        }
    };

    auto requestStateFade = [this](AppState targetState) {
        if (ctx.requestFadeTransition) {
            ctx.requestFadeTransition(targetState);
        }
    };

    auto captureChallengeStory = [this](int level) {
        int lvl = std::clamp(level, 1, 100);
        challengeStoryLevel = lvl;
        challengeStoryText = GetLevelStoryText(lvl);
        challengeStoryClockMs = 0.0;
        challengeStoryAlpha = 0.0f;
    };

    auto startChallengeClearFx = [this](int nextLevel) {
        challengeClearFxOrder.clear();
        const auto& boardRef = game->boardRef();
        const auto& asteroidRef = game->asteroidCells();
        for (int idx = 0; idx < Game::COLS * Game::ROWS; ++idx) {
            if (boardRef[idx] != 0 || asteroidRef[idx].has_value()) {
                challengeClearFxOrder.push_back(idx);
            }
        }
        if (challengeClearFxOrder.empty()) {
            challengeClearFxOrder.reserve(Game::COLS * Game::ROWS);
            for (int idx = 0; idx < Game::COLS * Game::ROWS; ++idx) {
                challengeClearFxOrder.push_back(idx);
            }
        }
        // Seed FX RNG deterministically from the game's challenge seed so animations
        // are reproducible per-run and per-level. Fall back to a random seed if game absent.
        if (game) {
            challengeClearFxRng.seed(game->getChallengeSeedBase() + static_cast<uint32_t>(nextLevel));
        } else {
            challengeClearFxRng.seed(std::random_device{}());
        }
        std::shuffle(challengeClearFxOrder.begin(), challengeClearFxOrder.end(), challengeClearFxRng);

        challengeClearFxElapsedMs = 0.0;
        challengeClearFxDurationMs = std::clamp(800.0 + static_cast<double>(challengeClearFxOrder.size()) * 8.0, 900.0, 2600.0);
        challengeClearFxNextLevel = nextLevel;
        challengeClearFxActive = true;
        gameplayCountdownActive = false;
        gameplayCountdownElapsed = 0.0;
        gameplayCountdownIndex = 0;
        menuPlayCountdownArmed = false;
        if (game) {
            game->setPaused(true);
        }
        SoundEffectManager::instance().playSound("challenge_clear", 0.8f);
    };

    while (running)
    {
        if (!ctx.scores && scoresLoadComplete.load(std::memory_order_acquire)) {
            ensureScoresLoaded();
        }

        int winW = 0, winH = 0;
        SDL_GetWindowSize(window, &winW, &winH);

        logicalScale = std::min(winW / (float)LOGICAL_W, winH / (float)LOGICAL_H);
        if (logicalScale <= 0)
            logicalScale = 1.f;

        logicalVP.w = winW;
        logicalVP.h = winH;
        logicalVP.x = 0;
        logicalVP.y = 0;

        SDL_Event e;
        while (SDL_PollEvent(&e))
        {
            if (e.type == SDL_EVENT_QUIT || e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)
                running = false;
            else {
                const bool isUserInputEvent =
                    e.type == SDL_EVENT_KEY_DOWN ||
                    e.type == SDL_EVENT_KEY_UP ||
                    e.type == SDL_EVENT_TEXT_INPUT ||
                    e.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
                    e.type == SDL_EVENT_MOUSE_BUTTON_UP ||
                    e.type == SDL_EVENT_MOUSE_MOTION;

                if (!(showHelpOverlay && isUserInputEvent)) {
                    stateMgr->handleEvent(e);
                    state = stateMgr->getState();
                }

                if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
                    if (e.key.scancode == SDL_SCANCODE_M)
                    {
                        if (auto sys = AudioManager::get()) sys->toggleMute();
                        musicEnabled = !musicEnabled;
                        Settings::instance().setMusicEnabled(musicEnabled);
                    }
                    if (e.key.scancode == SDL_SCANCODE_N)
                    {
                        if (auto sys = AudioManager::get()) {
                            sys->skipToNextTrack();
                            if (!musicStarted && sys->getLoadedTrackCount() > 0) {
                                musicStarted = true;
                                musicEnabled = true;
                                Settings::instance().setMusicEnabled(true);
                            }
                        }
                    }
                    // K: Toggle sound effects (S is reserved for co-op movement)
                    if (e.key.scancode == SDL_SCANCODE_K)
                    {
                        SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled());
                        Settings::instance().setSoundEnabled(SoundEffectManager::instance().isEnabled());
                    }
                    const bool helpToggleKey =
                        (e.key.scancode == SDL_SCANCODE_F1 && state != AppState::Loading && state != AppState::Video && state != AppState::Menu);
                    if (helpToggleKey)
                    {
                        showHelpOverlay = !showHelpOverlay;
                        if (state == AppState::Playing) {
                            if (showHelpOverlay) {
                                if (!game->isPaused()) {
                                    game->setPaused(true);
                                    helpOverlayPausedGame = true;
                                } else {
                                    helpOverlayPausedGame = false;
                                }
                            } else if (helpOverlayPausedGame) {
                                game->setPaused(false);
                                helpOverlayPausedGame = false;
                            }
                        } else if (!showHelpOverlay) {
                            helpOverlayPausedGame = false;
                        }
                    }
                    if (e.key.scancode == SDL_SCANCODE_ESCAPE && showHelpOverlay) {
                        showHelpOverlay = false;
                        if (state == AppState::Playing && helpOverlayPausedGame) {
                            game->setPaused(false);
                        }
                        helpOverlayPausedGame = false;
                    }
                    if (e.key.key == SDLK_F11 || (e.key.key == SDLK_RETURN && (e.key.mod & SDL_KMOD_ALT)))
                    {
                        isFullscreen = !isFullscreen;
                        SDL_SetWindowFullscreen(window, isFullscreen ? SDL_WINDOW_FULLSCREEN : 0);
                        Settings::instance().setFullscreen(isFullscreen);
                    }
                    if (e.key.scancode == SDL_SCANCODE_F5)
                    {
                        warpAutoPilotEnabled = false;
                        warpFlightMode = SpaceWarpFlightMode::Forward;
                        spaceWarp.setFlightMode(warpFlightMode);
                        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp mode: forward");
                    }
                    if (e.key.scancode == SDL_SCANCODE_F6)
                    {
                        warpAutoPilotEnabled = false;
                        warpFlightMode = SpaceWarpFlightMode::BankLeft;
                        spaceWarp.setFlightMode(warpFlightMode);
                        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp mode: bank left");
                    }
                    if (e.key.scancode == SDL_SCANCODE_F7)
                    {
                        warpAutoPilotEnabled = false;
                        warpFlightMode = SpaceWarpFlightMode::BankRight;
                        spaceWarp.setFlightMode(warpFlightMode);
                        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp mode: bank right");
                    }
                    if (e.key.scancode == SDL_SCANCODE_F8)
                    {
                        warpAutoPilotEnabled = false;
                        warpFlightMode = SpaceWarpFlightMode::Reverse;
                        spaceWarp.setFlightMode(warpFlightMode);
                        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp mode: reverse");
                    }
                    if (e.key.scancode == SDL_SCANCODE_F9)
                    {
                        warpAutoPilotEnabled = true;
                        spaceWarp.setAutoPilotEnabled(true);
                        SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Space warp autopilot engaged");
                    }
                }

                if (!showHelpOverlay && state == AppState::GameOver && isNewHighScore && e.type == SDL_EVENT_TEXT_INPUT) {
                    // Support single-player and coop two-name entry
                    if (game && game->getMode() == GameMode::Cooperate && coopGame) {
                        if (highScoreEntryIndex == 0) {
                            if (playerName.length() < 12) playerName += e.text.text;
                        } else {
                            if (player2Name.length() < 12) player2Name += e.text.text;
                        }
                    } else {
                        if (playerName.length() < 12) playerName += e.text.text;
                    }
                }

                if (!showHelpOverlay && state == AppState::GameOver && e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
                    if (isNewHighScore) {
                        if (game && game->getMode() == GameMode::Cooperate && coopGame) {
                            if (coopVsAI) {
                                // One-name entry flow (CPU is LEFT, human enters RIGHT name)
                                if (e.key.scancode == SDL_SCANCODE_BACKSPACE) {
                                    if (!player2Name.empty()) player2Name.pop_back();
                                } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
                                    if (player2Name.empty()) player2Name = "P2";
                                    std::string combined = std::string("CPU") + " & " + player2Name;
                                    int leftScore = coopGame->score(CoopGame::PlayerSide::Left);
                                    int rightScore = coopGame->score(CoopGame::PlayerSide::Right);
                                    int combinedScore = leftScore + rightScore;
                                    ensureScoresLoaded();
                                    scores.submit(combinedScore, coopGame->lines(), coopGame->level(), coopGame->elapsed(), combined, "cooperate");
                                    Settings::instance().setPlayerName(player2Name);
                                    isNewHighScore = false;
                                    SDL_StopTextInput(window);
                                }
                            } else {
                                // Two-name entry flow
                                if (e.key.scancode == SDL_SCANCODE_BACKSPACE) {
                                    if (highScoreEntryIndex == 0 && !playerName.empty()) playerName.pop_back();
                                    else if (highScoreEntryIndex == 1 && !player2Name.empty()) player2Name.pop_back();
                                } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
                                    if (highScoreEntryIndex == 0) {
                                        if (playerName.empty()) playerName = "P1";
                                        highScoreEntryIndex = 1; // move to second name
                                    } else {
                                        if (player2Name.empty()) player2Name = "P2";
                                        // Submit combined name
                                        std::string combined = playerName + " & " + player2Name;
                                        int leftScore = coopGame->score(CoopGame::PlayerSide::Left);
                                        int rightScore = coopGame->score(CoopGame::PlayerSide::Right);
                                        int combinedScore = leftScore + rightScore;
                                        ensureScoresLoaded();
                                        scores.submit(combinedScore, coopGame->lines(), coopGame->level(), coopGame->elapsed(), combined, "cooperate");
                                        Settings::instance().setPlayerName(playerName);
                                        isNewHighScore = false;
                                        SDL_StopTextInput(window);
                                    }
                                }
                            }
                        } else {
                            if (e.key.scancode == SDL_SCANCODE_BACKSPACE && !playerName.empty()) {
                                playerName.pop_back();
                            } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
                                if (playerName.empty()) playerName = "PLAYER";
                                ensureScoresLoaded();
                                std::string gt = (game->getMode() == GameMode::Challenge) ? "challenge" : "classic";
                                scores.submit(game->score(), game->lines(), game->level(), game->elapsed(), playerName, gt);
                                Settings::instance().setPlayerName(playerName);
                                isNewHighScore = false;
                                SDL_StopTextInput(window);
                            }
                        }
                    } else {
                        if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
                            if (game->getMode() == GameMode::Challenge) {
                                game->startChallengeRun(1);
                                } else if (game->getMode() == GameMode::Cooperate) {
                                    game->setMode(GameMode::Cooperate);
                                    game->reset(startLevelSelection);
                            } else {
                                game->setMode(GameMode::Endless);
                                game->reset(startLevelSelection);
                            }
                            state = AppState::Playing;
                            stateMgr->setState(state);
                        } else if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
                            state = AppState::Menu;
                            stateMgr->setState(state);
                        }
                    }
                }

                if (!showHelpOverlay && e.type == SDL_EVENT_MOUSE_BUTTON_DOWN)
                {
                    float mx = (float)e.button.x, my = (float)e.button.y;
                    if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h)
                    {
                        float lx = (mx - logicalVP.x) / logicalScale, ly = (my - logicalVP.y) / logicalScale;
                        if (state == AppState::Menu)
                        {
                            if (showSettingsPopup) {
                                showSettingsPopup = false;
                            } else {
                                ui::MenuLayoutParams params{ LOGICAL_W, LOGICAL_H, winW, winH, logicalScale };

                                auto menuInput = ui::handleBottomMenuInput(params, e, lx, ly, hoveredButton, true);
                                hoveredButton = menuInput.hoveredIndex;

                                if (menuInput.activated) {
                                    switch (*menuInput.activated) {
                                        case ui::BottomMenuItem::Play:
                                            if (game) game->setMode(GameMode::Endless);
                                            startMenuPlayTransition();
                                            break;
                                        case ui::BottomMenuItem::Cooperate:
                                            if (menuState) {
                                                menuState->showCoopSetupPanel(true);
                                            }
                                            break;
                                        case ui::BottomMenuItem::Challenge:
                                            if (game) {
                                                game->setMode(GameMode::Challenge);
                                                // Suppress the initial level-up jingle when starting Challenge from menu
                                                skipNextLevelUpJingle = true;
                                                game->startChallengeRun(1);
                                            }
                                            startMenuPlayTransition();
                                            break;
                                        case ui::BottomMenuItem::Level:
                                            requestStateFade(AppState::LevelSelector);
                                            break;
                                        case ui::BottomMenuItem::Options:
                                            requestStateFade(AppState::Options);
                                            break;
                                        case ui::BottomMenuItem::Help:
                                            if (menuState) menuState->showHelpPanel(true);
                                            break;
                                        case ui::BottomMenuItem::About:
                                            if (menuState) menuState->showAboutPanel(true);
                                            break;
                                        case ui::BottomMenuItem::Exit:
                                            showExitConfirmPopup = true;
                                            exitPopupSelectedButton = 1;
                                            break;
                                    }
                                }

                                SDL_FRect settingsBtn{SETTINGS_BTN_X, SETTINGS_BTN_Y, SETTINGS_BTN_W, SETTINGS_BTN_H};
                                if (lx >= settingsBtn.x && lx <= settingsBtn.x + settingsBtn.w && ly >= settingsBtn.y && ly <= settingsBtn.y + settingsBtn.h)
                                {
                                    showSettingsPopup = true;
                                }
                            }
                        }
                        else if (state == AppState::LevelSelect)
                            startLevelSelection = (startLevelSelection + 1) % 20;
                        else if (state == AppState::GameOver) {
                            state = AppState::Menu;
                            stateMgr->setState(state);
                        }
                        else if (state == AppState::Playing && showExitConfirmPopup) {
                            float lx2 = (mx - logicalVP.x) / logicalScale;
                            float ly2 = (my - logicalVP.y) / logicalScale;
                            float contentW = LOGICAL_W * logicalScale;
                            float contentH = LOGICAL_H * logicalScale;
                            float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
                            float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
                            float localX = lx2 - contentOffsetX;
                            float localY = ly2 - contentOffsetY;

                            float popupW = 400, popupH = 200;
                            float popupX = (LOGICAL_W - popupW) / 2.0f;
                            float popupY = (LOGICAL_H - popupH) / 2.0f;
                            float btnW = 120.0f, btnH = 40.0f;
                            float yesX = popupX + popupW * 0.25f - btnW / 2.0f;
                            float noX = popupX + popupW * 0.75f - btnW / 2.0f;
                            float btnY = popupY + popupH - btnH - 20.0f;

                            if (localX >= popupX && localX <= popupX + popupW && localY >= popupY && localY <= popupY + popupH) {
                                if (localX >= yesX && localX <= yesX + btnW && localY >= btnY && localY <= btnY + btnH) {
                                    showExitConfirmPopup = false;
                                    game->reset(startLevelSelection);
                                    state = AppState::Menu;
                                    stateMgr->setState(state);
                                } else if (localX >= noX && localX <= noX + btnW && localY >= btnY && localY <= btnY + btnH) {
                                    showExitConfirmPopup = false;
                                    game->setPaused(false);
                                }
                            } else {
                                showExitConfirmPopup = false;
                                game->setPaused(false);
                            }
                        }
                        else if (state == AppState::Menu && showExitConfirmPopup) {
                            float contentW = LOGICAL_W * logicalScale;
                            float contentH = LOGICAL_H * logicalScale;
                            float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
                            float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
                            float popupW = 420.0f;
                            float popupH = 230.0f;
                            float popupX = (LOGICAL_W - popupW) * 0.5f + contentOffsetX;
                            float popupY = (LOGICAL_H - popupH) * 0.5f + contentOffsetY;
                            float btnW = 140.0f;
                            float btnH = 50.0f;
                            float yesX = popupX + popupW * 0.3f - btnW / 2.0f;
                            float noX = popupX + popupW * 0.7f - btnW / 2.0f;
                            float btnY = popupY + popupH - btnH - 30.0f;
                            bool insidePopup = lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH;
                            if (insidePopup) {
                                if (lx >= yesX && lx <= yesX + btnW && ly >= btnY && ly <= btnY + btnH) {
                                    showExitConfirmPopup = false;
                                    running = false;
                                } else if (lx >= noX && lx <= noX + btnW && ly >= btnY && ly <= btnY + btnH) {
                                    showExitConfirmPopup = false;
                                }
                            } else {
                                showExitConfirmPopup = false;
                            }
                        }
                    }
                }
                else if (!showHelpOverlay && e.type == SDL_EVENT_MOUSE_MOTION)
                {
                    float mx = (float)e.motion.x, my = (float)e.motion.y;
                    if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h)
                    {
                        float lx = (mx - logicalVP.x) / logicalScale, ly = (my - logicalVP.y) / logicalScale;
                        if (state == AppState::Menu && !showSettingsPopup)
                        {
                            ui::MenuLayoutParams params{ LOGICAL_W, LOGICAL_H, winW, winH, logicalScale };
                            auto menuInput = ui::handleBottomMenuInput(params, e, lx, ly, hoveredButton, true);
                            hoveredButton = menuInput.hoveredIndex;
                        }
                    }
                }
                else if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
                    if (gameplayCountdownActive && gameplayCountdownSource == CountdownSource::ChallengeLevel && challengeCountdownWaitingForSpace) {
                        if (e.key.scancode == SDL_SCANCODE_SPACE) {
                            challengeCountdownWaitingForSpace = false;
                            gameplayCountdownElapsed = 0.0;
                            gameplayCountdownIndex = 0;
                        } else if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
                            // Show quit popup, keep game paused, cancel countdown
                            if (!showExitConfirmPopup) {
                                showExitConfirmPopup = true;
                                exitPopupSelectedButton = 1; // default to NO
                            }
                            gameplayCountdownActive = false;
                            menuPlayCountdownArmed = false;
                            gameplayCountdownElapsed = 0.0;
                            gameplayCountdownIndex = 0;
                            countdownAdvancesChallenge = false;
                            challengeCountdownWaitingForSpace = false;
                            if (game) game->setPaused(true);
                        }
                    }
                }
            }
        }

        // State transitions can be triggered from render/update (e.g. menu network handshake).
        // Keep our cached `state` in sync every frame, not only when events occur.
        state = stateMgr->getState();

        Uint64 now = SDL_GetPerformanceCounter();
        double frameMs = double(now - lastMs) * 1000.0 / double(SDL_GetPerformanceFrequency());
        lastMs = now;
        if (frameMs > 100.0) frameMs = 100.0;
        gameplayBackgroundClockMs += frameMs;

        if (startupFadeActive) {
            if (startupFadeClockMs <= 0.0) {
                startupFadeClockMs = STARTUP_FADE_IN_MS;
                startupFadeAlpha = 1.0f;
            }
            startupFadeClockMs -= frameMs;
            if (startupFadeClockMs <= 0.0) {
                startupFadeClockMs = 0.0;
                startupFadeAlpha = 0.0f;
                startupFadeActive = false;
            } else {
                startupFadeAlpha = float(std::clamp(startupFadeClockMs / STARTUP_FADE_IN_MS, 0.0, 1.0));
            }
        }

        auto clearChallengeStory = [this]() {
            challengeStoryText.clear();
            challengeStoryLevel = 0;
            challengeStoryAlpha = 0.0f;
            challengeStoryClockMs = 0.0;
        };

        // Update challenge story fade/timeout; during countdown wait we keep it fully visible
        if (state == AppState::Playing && game && game->getMode() == GameMode::Challenge && !challengeStoryText.empty()) {
            if (gameplayCountdownActive && gameplayCountdownSource == CountdownSource::ChallengeLevel && challengeCountdownWaitingForSpace) {
                // Locked-visible while waiting
                challengeStoryAlpha = 1.0f;
            } else {
                const double fadeInMs = 320.0;
                const double holdMs = 3200.0;
                const double fadeOutMs = 900.0;
                const double totalMs = fadeInMs + holdMs + fadeOutMs;
                challengeStoryClockMs += frameMs;
                if (challengeStoryClockMs >= totalMs) {
                    clearChallengeStory();
                } else {
                    double a = 1.0;
                    if (challengeStoryClockMs < fadeInMs) {
                        a = challengeStoryClockMs / fadeInMs;
                    } else if (challengeStoryClockMs > fadeInMs + holdMs) {
                        double t = challengeStoryClockMs - (fadeInMs + holdMs);
                        a = std::max(0.0, 1.0 - t / fadeOutMs);
                    }
                    challengeStoryAlpha = static_cast<float>(std::clamp(a, 0.0, 1.0));
                }
            }
        } else {
            clearChallengeStory();
        }

        if (challengeClearFxActive) {
            challengeClearFxElapsedMs += frameMs;
            if (challengeClearFxElapsedMs >= challengeClearFxDurationMs) {
                challengeClearFxElapsedMs = challengeClearFxDurationMs;
                challengeClearFxActive = false;
                if (challengeClearFxNextLevel > 0) {
                    // Advance to the next challenge level immediately so the countdown shows the new board/asteroids
                    if (game) {
                        game->beginNextChallengeLevel();
                        game->setPaused(true);
                    }
                    gameplayCountdownSource = CountdownSource::ChallengeLevel;
                    countdownLevel = challengeClearFxNextLevel;
                    countdownGoalAsteroids = challengeClearFxNextLevel;
                    captureChallengeStory(countdownLevel);
                    countdownAdvancesChallenge = false; // already advanced
                    gameplayCountdownActive = true;
                    challengeCountdownWaitingForSpace = true;
                    menuPlayCountdownArmed = false;
                    gameplayCountdownElapsed = 0.0;
                    gameplayCountdownIndex = 0;
                    SoundEffectManager::instance().playSound("new_level", 1.0f);
                    skipNextLevelUpJingle = true;
                }
                challengeClearFxNextLevel = 0;
            }
        }

        const bool *ks = SDL_GetKeyboardState(nullptr);
        bool left = state == AppState::Playing && ks[SDL_SCANCODE_LEFT];
        bool right = state == AppState::Playing && ks[SDL_SCANCODE_RIGHT];
        bool down = state == AppState::Playing && ks[SDL_SCANCODE_DOWN];

        if (state == AppState::Playing)
            game->setSoftDropping(down && !game->isPaused());
        else
            game->setSoftDropping(false);

        int moveDir = 0;
        if (left && !right)
            moveDir = -1;
        else if (right && !left)
            moveDir = +1;

        if (moveDir != 0 && !game->isPaused())
        {
            if ((moveDir == -1 && leftHeld == false) || (moveDir == +1 && rightHeld == false))
            {
                game->move(moveDir);
                moveTimerMs = DAS;
            }
            else
            {
                moveTimerMs -= frameMs;
                if (moveTimerMs <= 0)
                {
                    game->move(moveDir);
                    moveTimerMs += ARR;
                }
            }
        }
        else
            moveTimerMs = 0;
        leftHeld = left;
        rightHeld = right;
        if (down && !game->isPaused())
            game->softDropBoost(frameMs);

        if (musicLoadingStarted && !musicLoaded) {
            if (auto sys = AudioManager::get()) {
                currentTrackLoading = sys->getLoadedTrackCount();
                if (sys->isLoadingComplete() || (totalTracks > 0 && currentTrackLoading >= totalTracks)) {
                    sys->shuffle();
                    musicLoaded = true;
                }
            } else {
                currentTrackLoading = 0;
            }
        }

        if (state == AppState::Playing)
        {
            const bool coopActive = game && game->getMode() == GameMode::Cooperate && coopGame;

            if (coopActive) {
                // Coop DAS/ARR handling (per-side)
                const bool* ks = SDL_GetKeyboardState(nullptr);

                auto handleSide = [&](CoopGame::PlayerSide side,
                                      bool leftHeldPrev,
                                      bool rightHeldPrev,
                                      double& timer,
                                      SDL_Scancode leftKey,
                                      SDL_Scancode rightKey,
                                      SDL_Scancode downKey) {
                    bool left = ks[leftKey];
                    bool right = ks[rightKey];
                    bool down = ks[downKey];

                    coopGame->setSoftDropping(side, down);

                    int moveDir = 0;
                    if (left && !right) moveDir = -1;
                    else if (right && !left) moveDir = +1;

                    if (moveDir != 0) {
                        if ((moveDir == -1 && !leftHeldPrev) || (moveDir == +1 && !rightHeldPrev)) {
                            coopGame->move(side, moveDir);
                            timer = DAS;
                        } else {
                            timer -= frameMs;
                            if (timer <= 0) {
                                coopGame->move(side, moveDir);
                                timer += ARR;
                            }
                        }
                    } else {
                        timer = 0.0;
                    }
                };

                if (game->isPaused()) {
                    // While paused, suppress all continuous input changes so pieces don't drift.
                    if (ctx.coopNetEnabled && ctx.coopNetSession) {
                        ctx.coopNetSession->poll(0);
                        ctx.coopNetStalled = false;
                    }
                    coopGame->setSoftDropping(CoopGame::PlayerSide::Left, false);
                    coopGame->setSoftDropping(CoopGame::PlayerSide::Right, false);
                    p1MoveTimerMs = 0.0;
                    p2MoveTimerMs = 0.0;
                    p1LeftHeld = false;
                    p1RightHeld = false;
                    p2LeftHeld = false;
                    p2RightHeld = false;
                } else {
                    const bool coopNetActive = ctx.coopNetEnabled && ctx.coopNetSession;

                    // If we just entered network co-op, reset per-session fixed-tick bookkeeping.
                    if (coopNetActive && coopNetCachedTick != 0xFFFFFFFFu && ctx.coopNetTick == 0u) {
                        coopNetAccMs = 0.0;
                        coopNetCachedTick = 0xFFFFFFFFu;
                        coopNetCachedButtons = 0;
                        coopNetLastHashSentTick = 0xFFFFFFFFu;
                        ctx.coopNetStalled = false;
                    }

                    // Define canonical key mappings for left and right players
                    const SDL_Scancode leftLeftKey = SDL_SCANCODE_A;
                    const SDL_Scancode leftRightKey = SDL_SCANCODE_D;
                    const SDL_Scancode leftDownKey = SDL_SCANCODE_S;

                    const SDL_Scancode rightLeftKey = SDL_SCANCODE_LEFT;
                    const SDL_Scancode rightRightKey = SDL_SCANCODE_RIGHT;
                    const SDL_Scancode rightDownKey = SDL_SCANCODE_DOWN;

                    if (coopNetActive) {
                        // Network co-op: fixed tick lockstep.
                        // Use a fixed dt so both peers simulate identically.
                        static constexpr double FIXED_DT_MS = 1000.0 / 60.0;
                        static constexpr uint32_t HASH_INTERVAL_TICKS = 60; // ~1s

                        ctx.coopNetSession->poll(0);

                        // If the connection drops during gameplay, abort back to menu.
                        if (ctx.coopNetSession->state() == NetSession::ConnState::Disconnected ||
                            ctx.coopNetSession->state() == NetSession::ConnState::Error) {
                            const std::string reason = (ctx.coopNetSession->state() == NetSession::ConnState::Error && !ctx.coopNetSession->lastError().empty())
                                                           ? (std::string("NET ERROR: ") + ctx.coopNetSession->lastError())
                                                           : std::string("NET DISCONNECTED");
                            SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[NET COOP] %s", reason.c_str());

                            ctx.coopNetUiStatusText = reason;
                            ctx.coopNetUiStatusRemainingMs = 6000.0;
                            ctx.coopNetEnabled = false;
                            ctx.coopNetStalled = false;
                            ctx.coopNetDesyncDetected = false;
                            ctx.coopNetTick = 0;
                            ctx.coopNetPendingButtons = 0;
                            if (ctx.coopNetSession) {
                                ctx.coopNetSession->shutdown();
                                ctx.coopNetSession.reset();
                            }

                            // Ensure we don't remain paused due to a previous net stall/desync.
                            if (game) {
                                game->setPaused(false);
                            }
                            state = AppState::Menu;
                            stateMgr->setState(state);
                            continue;
                        }

                        coopNetAccMs = std::min(coopNetAccMs + frameMs, FIXED_DT_MS * 8.0);

                        auto buildLocalButtons = [&]() -> uint8_t {
                            uint8_t b = 0;
                            if (ctx.coopNetLocalIsLeft) {
                                if (ks[leftLeftKey]) b |= coopnet::MoveLeft;
                                if (ks[leftRightKey]) b |= coopnet::MoveRight;
                                if (ks[leftDownKey]) b |= coopnet::SoftDrop;
                            } else {
                                if (ks[rightLeftKey]) b |= coopnet::MoveLeft;
                                if (ks[rightRightKey]) b |= coopnet::MoveRight;
                                if (ks[rightDownKey]) b |= coopnet::SoftDrop;
                            }
                            b |= ctx.coopNetPendingButtons;
                            ctx.coopNetPendingButtons = 0;
                            return b;
                        };

                        auto applyButtonsForSide = [&](CoopGame::PlayerSide side,
                                                       uint8_t buttons,
                                                       bool& leftHeldPrev,
                                                       bool& rightHeldPrev,
                                                       double& timer) {
                            const bool leftHeldNow = coopnet::has(buttons, coopnet::MoveLeft);
                            const bool rightHeldNow = coopnet::has(buttons, coopnet::MoveRight);
                            const bool downHeldNow = coopnet::has(buttons, coopnet::SoftDrop);

                            coopGame->setSoftDropping(side, downHeldNow);

                            int moveDir = 0;
                            if (leftHeldNow && !rightHeldNow) moveDir = -1;
                            else if (rightHeldNow && !leftHeldNow) moveDir = +1;

                            if (moveDir != 0) {
                                if ((moveDir == -1 && !leftHeldPrev) || (moveDir == +1 && !rightHeldPrev)) {
                                    coopGame->move(side, moveDir);
                                    timer = DAS;
                                } else {
                                    timer -= FIXED_DT_MS;
                                    if (timer <= 0.0) {
                                        coopGame->move(side, moveDir);
                                        timer += ARR;
                                    }
                                }
                            } else {
                                timer = 0.0;
                            }

                            if (coopnet::has(buttons, coopnet::RotCW)) {
                                coopGame->rotate(side, +1);
                            }
                            if (coopnet::has(buttons, coopnet::RotCCW)) {
                                coopGame->rotate(side, -1);
                            }
                            if (coopnet::has(buttons, coopnet::HardDrop)) {
                                SoundEffectManager::instance().playSound("hard_drop", 0.7f);
                                coopGame->hardDrop(side);
                            }
                            if (coopnet::has(buttons, coopnet::Hold)) {
                                coopGame->holdCurrent(side);
                            }

                            leftHeldPrev = leftHeldNow;
                            rightHeldPrev = rightHeldNow;
                        };

                        const char* roleStr = ctx.coopNetIsHost ? "HOST" : "CLIENT";

                        int safetySteps = 0;
                        bool advancedTick = false;
                        ctx.coopNetStalled = false;
                        while (coopNetAccMs >= FIXED_DT_MS && safetySteps++ < 8) {
                            const uint32_t tick = ctx.coopNetTick;

                            if (coopNetCachedTick != tick) {
                                coopNetCachedTick = tick;
                                coopNetCachedButtons = buildLocalButtons();
                                if (!ctx.coopNetSession->sendLocalInput(tick, coopNetCachedButtons)) {
                                    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                                                "[NET COOP][%s] sendLocalInput failed (tick=%u)",
                                                roleStr,
                                                tick);
                                }
                            }

                            auto remoteButtonsOpt = ctx.coopNetSession->getRemoteButtons(tick);
                            if (!remoteButtonsOpt.has_value()) {
                                if (!ctx.coopNetStalled) {
                                    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
                                                "[NET COOP][%s] STALL begin waitingForTick=%u",
                                                roleStr,
                                                tick);
                                }
                                ctx.coopNetStalled = true;
                                break; // lockstep stall
                            }

                            const uint8_t remoteButtons = remoteButtonsOpt.value();
                            const bool localIsLeft = ctx.coopNetLocalIsLeft;

                            if (localIsLeft) {
                                applyButtonsForSide(CoopGame::PlayerSide::Left, coopNetCachedButtons, p1LeftHeld, p1RightHeld, p1MoveTimerMs);
                                applyButtonsForSide(CoopGame::PlayerSide::Right, remoteButtons, p2LeftHeld, p2RightHeld, p2MoveTimerMs);
                            } else {
                                applyButtonsForSide(CoopGame::PlayerSide::Right, coopNetCachedButtons, p2LeftHeld, p2RightHeld, p2MoveTimerMs);
                                applyButtonsForSide(CoopGame::PlayerSide::Left, remoteButtons, p1LeftHeld, p1RightHeld, p1MoveTimerMs);
                            }

                            coopGame->tickGravity(FIXED_DT_MS);
                            coopGame->updateVisualEffects(FIXED_DT_MS);

                            if ((tick % HASH_INTERVAL_TICKS) == 0 && coopNetLastHashSentTick != tick) {
                                coopNetLastHashSentTick = tick;
                                const uint64_t hash = coopGame->computeStateHash();
                                if (!ctx.coopNetSession->sendStateHash(tick, hash)) {
                                    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                                                "[NET COOP][%s] sendStateHash failed (tick=%u hash=0x%016llX)",
                                                roleStr,
                                                tick,
                                                (unsigned long long)hash);
                                }
                                auto rh = ctx.coopNetSession->takeRemoteHash(tick);
                                if (rh.has_value() && rh.value() != hash) {
                                    ctx.coopNetDesyncDetected = true;
                                    ctx.coopNetUiStatusText = "NET DESYNC";
                                    ctx.coopNetUiStatusRemainingMs = 8000.0;
                                    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                                                 "[NET COOP][%s] DESYNC detected at tick=%u local=0x%016llX remote=0x%016llX",
                                                 roleStr,
                                                 tick,
                                                 (unsigned long long)hash,
                                                 (unsigned long long)rh.value());
                                    game->setPaused(true);
                                }
                            }

                            ctx.coopNetTick++;
                            advancedTick = true;
                            coopNetAccMs -= FIXED_DT_MS;
                        }

                        if (advancedTick) {
                            if (ctx.coopNetStalled) {
                                SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
                                            "[NET COOP][%s] STALL end atTick=%u",
                                            roleStr,
                                            ctx.coopNetTick);
                            }
                            ctx.coopNetStalled = false;
                        }
                    } else if (!coopVsAI) {
                        // Standard two-player: left uses WASD, right uses arrow keys
                        handleSide(CoopGame::PlayerSide::Left, p1LeftHeld, p1RightHeld, p1MoveTimerMs, leftLeftKey, leftRightKey, leftDownKey);
                        handleSide(CoopGame::PlayerSide::Right, p2LeftHeld, p2RightHeld, p2MoveTimerMs, rightLeftKey, rightRightKey, rightDownKey);

                        p1LeftHeld = ks[leftLeftKey];
                        p1RightHeld = ks[leftRightKey];
                        p2LeftHeld = ks[rightLeftKey];
                        p2RightHeld = ks[rightRightKey];
                    } else {
                        // Coop vs CPU: AI controls LEFT, human controls RIGHT (arrow keys).
                        // Handle continuous input for the human on the right side.
                        handleSide(CoopGame::PlayerSide::Right, p2LeftHeld, p2RightHeld, p2MoveTimerMs, rightLeftKey, rightRightKey, rightDownKey);

                        // Mirror the human soft-drop to the AI-controlled left board so both fall together.
                        const bool pRightSoftDrop = ks[rightDownKey];
                        coopGame->setSoftDropping(CoopGame::PlayerSide::Left, pRightSoftDrop);

                        // Reset left continuous timers/held flags (AI handles movement)
                        p1MoveTimerMs = 0.0;
                        p1LeftHeld = false;
                        p1RightHeld = false;

                        // Update AI for the left side
                        coopAI.update(*coopGame, CoopGame::PlayerSide::Left, frameMs);
                        // Update human-held flags for right-side controls so DAS/ARR state is tracked
                        p2LeftHeld = ks[rightLeftKey];
                        p2RightHeld = ks[rightRightKey];
                    }

                    if (!coopNetActive) {
                        coopGame->tickGravity(frameMs);
                        coopGame->updateVisualEffects(frameMs);
                    }
                }

                if (coopGame->isGameOver()) {
                    // Compute combined coop stats for Game Over
                    int leftScore = coopGame->score(CoopGame::PlayerSide::Left);
                    int rightScore = coopGame->score(CoopGame::PlayerSide::Right);
                    int combinedScore = leftScore + rightScore;
                    if (combinedScore > 0) {
                        isNewHighScore = true;
                        if (coopVsAI) {
                            // AI is left, prompt human (right) for name
                            playerName = "CPU";
                            player2Name.clear();
                            highScoreEntryIndex = 1; // enter P2 (human)
                        } else {
                            playerName.clear();
                            player2Name.clear();
                            highScoreEntryIndex = 0;
                        }
                        SDL_StartTextInput(window);
                    } else {
                        isNewHighScore = false;
                        ensureScoresLoaded();
                        // When AI is present, label should indicate CPU left and human right
                        scores.submit(combinedScore, coopGame->lines(), coopGame->level(), coopGame->elapsed(), coopVsAI ? "CPU & P2" : "P1 & P2", "cooperate");
                    }
                    state = AppState::GameOver;
                    stateMgr->setState(state);

                    if (ctx.coopNetSession) {
                        ctx.coopNetSession->shutdown();
                        ctx.coopNetSession.reset();
                    }
                    ctx.coopNetEnabled = false;
                }

            } else {
                if (!game->isPaused()) {
                    game->tickGravity(frameMs);
                    game->updateElapsedTime();

                    if (lineEffect.isActive()) {
                        if (lineEffect.update(frameMs / 1000.0f)) {
                            game->clearCompletedLines();
                        }
                    }
                }
                if (game->isGameOver())
                {
                    if (game->score() > 0) {
                        isNewHighScore = true;
                        playerName.clear();
                        SDL_StartTextInput(window);
                    } else {
                        isNewHighScore = false;
                        ensureScoresLoaded();
                        {
                            std::string gt = (game->getMode() == GameMode::Challenge) ? "challenge" : "classic";
                            scores.submit(game->score(), game->lines(), game->level(), game->elapsed(), "PLAYER", gt);
                        }
                    }
                    state = AppState::GameOver;
                    stateMgr->setState(state);
                }
            }
        }
        else if (state == AppState::Loading)
        {
            static int queuedTextureCount = 0;
            if (loadingStarted.load() && !loadingComplete.load()) {
                static bool queuedTextures = false;
                static std::vector<std::string> queuedPaths;
                if (!queuedTextures) {
                    queuedTextures = true;
                    constexpr int baseTasks = 25;
                    totalLoadingTasks.store(baseTasks);
                    loadedTasks.store(0);
                    {
                        std::lock_guard<std::mutex> lk(assetLoadErrorsMutex);
                        assetLoadErrors.clear();
                    }
                    {
                        std::lock_guard<std::mutex> lk(currentLoadingMutex);
                        currentLoadingFile.clear();
                    }

                    if (auto sys = AudioManager::get()) {
                        sys->init();
                        totalTracks = 0;
                        for (int i = 1; i <= 100; ++i) {
                            char base[128];
                            std::snprintf(base, sizeof(base), "assets/music/music%03d", i);
                            std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" });
                            if (path.empty()) break;
                            sys->addTrackAsync(path);
                            totalTracks++;
                        }
                        totalLoadingTasks.store(baseTasks + totalTracks);
                        if (totalTracks > 0) {
                            sys->startBackgroundLoading();
                            musicLoadingStarted = true;
                        } else {
                            musicLoaded = true;
                        }
                    } else {
                        totalTracks = 0;
                        totalLoadingTasks.store(baseTasks + totalTracks);
                        musicLoaded = true;
                    }

                    pixelFont.init(AssetPath::resolveWithBase(Assets::FONT_ORBITRON), 22);
                    loadedTasks.fetch_add(1);
                    font.init(AssetPath::resolveWithBase(Assets::FONT_EXO2), 20);
                    loadedTasks.fetch_add(1);

                    queuedPaths = {
                        Assets::LOGO,
                        Assets::LOGO,
                        Assets::MAIN_SCREEN,
                        Assets::BLOCKS_SPRITE,
                        Assets::PANEL_SCORE,
                        Assets::PANEL_STATS,
                        Assets::NEXT_PANEL,
                        Assets::HOLD_PANEL,
                        Assets::ASTEROID_SPRITE
                    };
                    for (auto &p : queuedPaths) {
                        loadingManager->queueTexture(p);
                    }
                    queuedTextureCount = static_cast<int>(queuedPaths.size());

                    SoundEffectManager::instance().init();
                    loadedTasks.fetch_add(1);

                    const std::vector<std::string> audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level","asteroid_destroy","challenge_clear"};
                    for (const auto &id : audioIds) {
                        std::string basePath = "assets/music/" + (id == "hard_drop"
                            ? "hard_drop_001"
                            : (id == "challenge_clear"
                                ? "GONG0"
                                : (id == "asteroid_destroy"
                                    ? "asteroid-destroy"
                                    : id)));
                        {
                            std::lock_guard<std::mutex> lk(currentLoadingMutex);
                            currentLoadingFile = basePath;
                        }
                        std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" });
                        if (!resolved.empty()) {
                            SoundEffectManager::instance().loadSound(id, resolved);
                        }
                        loadedTasks.fetch_add(1);
                        {
                            std::lock_guard<std::mutex> lk(currentLoadingMutex);
                            currentLoadingFile.clear();
                        }
                    }
                }

                bool texturesDone = loadingManager->update();
                if (texturesDone) {
                    logoTex = assetLoader.getTexture(Assets::LOGO);
                    logoSmallTex = assetLoader.getTexture(Assets::LOGO);
                    mainScreenTex = assetLoader.getTexture(Assets::MAIN_SCREEN);
                    blocksTex = assetLoader.getTexture(Assets::BLOCKS_SPRITE);
                    asteroidsTex = assetLoader.getTexture(Assets::ASTEROID_SPRITE);
                    scorePanelTex = assetLoader.getTexture(Assets::PANEL_SCORE);
                    statisticsPanelTex = assetLoader.getTexture(Assets::PANEL_STATS);
                    nextPanelTex = assetLoader.getTexture(Assets::NEXT_PANEL);
                    holdPanelTex = assetLoader.getTexture(Assets::HOLD_PANEL);

                    // texture retrieval diagnostics removed

                    auto ensureTextureSize = [&](SDL_Texture* tex, int& outW, int& outH) {
                        if (!tex) return;
                        if (outW > 0 && outH > 0) return;
                        float w = 0.0f, h = 0.0f;
                        if (SDL_GetTextureSize(tex, &w, &h)) {
                            outW = static_cast<int>(std::lround(w));
                            outH = static_cast<int>(std::lround(h));
                        }
                    };

                    ensureTextureSize(logoSmallTex, logoSmallW, logoSmallH);
                    ensureTextureSize(mainScreenTex, mainScreenW, mainScreenH);

                    auto legacyLoad = [&](const std::string& p, SDL_Texture*& outTex, int* outW = nullptr, int* outH = nullptr) {
                        if (!outTex) {
                            SDL_Texture* loaded = textureLoader->loadFromImage(renderer, p, outW, outH);
                            if (loaded) {
                                outTex = loaded;
                                assetLoader.adoptTexture(p, loaded);
                            }
                        }
                    };

                    legacyLoad(Assets::LOGO, logoTex);
                    legacyLoad(Assets::LOGO, logoSmallTex, &logoSmallW, &logoSmallH);
                    legacyLoad(Assets::MAIN_SCREEN, mainScreenTex, &mainScreenW, &mainScreenH);
                    legacyLoad(Assets::BLOCKS_SPRITE, blocksTex);
                    legacyLoad(Assets::ASTEROID_SPRITE, asteroidsTex);
                    legacyLoad(Assets::PANEL_SCORE, scorePanelTex);
                    legacyLoad(Assets::PANEL_STATS, statisticsPanelTex);
                    legacyLoad(Assets::NEXT_PANEL, nextPanelTex);
                    legacyLoad(Assets::HOLD_PANEL, holdPanelTex);

                    if (!blocksTex) {
                        blocksTex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 630, 90);
                        SDL_SetRenderTarget(renderer, blocksTex);
                        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
                        SDL_RenderClear(renderer);
                        for (int i = 0; i < PIECE_COUNT; ++i) {
                            SDL_Color c = COLORS[i + 1];
                            SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
                            SDL_FRect rect{(float)(i * 90), 0, 90, 90};
                            SDL_RenderFillRect(renderer, &rect);
                        }
                        SDL_SetRenderTarget(renderer, nullptr);

                        // Ensure the generated fallback texture is cleaned up with other assets.
                        assetLoader.adoptTexture(Assets::BLOCKS_SPRITE, blocksTex);
                    }

                    if (musicLoaded) {
                        loadingComplete.store(true);
                    }
                }
            }

            const int totalTasks = totalLoadingTasks.load(std::memory_order_acquire);
            const int musicDone = std::min(totalTracks, currentTrackLoading);
            int doneTasks = loadedTasks.load(std::memory_order_acquire) + musicDone;
            if (queuedTextureCount > 0) {
                float texProg = loadingManager->getProgress();
                int texDone = static_cast<int>(std::floor(texProg * queuedTextureCount + 0.5f));
                if (texDone > queuedTextureCount) texDone = queuedTextureCount;
                doneTasks += texDone;
            }
            if (doneTasks > totalTasks) doneTasks = totalTasks;
            if (totalTasks > 0) {
                loadingProgress = std::min(1.0, double(doneTasks) / double(totalTasks));
                if (loadingProgress >= 1.0 && musicLoaded) {
                    startupFadeActive = false;
                    startupFadeAlpha = 0.0f;
                    startupFadeClockMs = 0.0;

                    if (std::filesystem::exists(introVideoPath)) {
                        state = AppState::Video;
                    } else {
                        state = AppState::Menu;
                    }
                    stateMgr->setState(state);
                }
            } else {
                double assetProgress = 0.2;
                double musicProgress = 0.0;
                if (totalTracks > 0) {
                    musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7);
                } else {
                    if (auto sys = AudioManager::get()) {
                        if (sys->isLoadingComplete()) {
                            musicProgress = 0.7;
                        } else if (sys->getLoadedTrackCount() > 0) {
                            musicProgress = 0.35;
                        } else {
                            Uint32 elapsedMs = SDL_GetTicks() - static_cast<Uint32>(loadStart);
                            if (elapsedMs > 1500) {
                                musicProgress = 0.7;
                                musicLoaded = true;
                            } else {
                                musicProgress = 0.0;
                            }
                        }
                    } else {
                        Uint32 elapsedMs = SDL_GetTicks() - static_cast<Uint32>(loadStart);
                        if (elapsedMs > 1500) {
                            musicProgress = 0.7;
                            musicLoaded = true;
                        } else {
                            musicProgress = 0.0;
                        }
                    }
                }
                double timeProgress = std::min(0.1, (now - loadStart) / 500.0);
                loadingProgress = std::min(1.0, assetProgress + musicProgress + timeProgress);
                if (loadingProgress > 0.99) loadingProgress = 1.0;
                if (!musicLoaded && timeProgress >= 0.1) loadingProgress = 1.0;
                if (loadingProgress >= 1.0 && musicLoaded) {
                    startupFadeActive = false;
                    startupFadeAlpha = 0.0f;
                    startupFadeClockMs = 0.0;

                    if (std::filesystem::exists(introVideoPath)) {
                        state = AppState::Video;
                    } else {
                        state = AppState::Menu;
                    }
                    stateMgr->setState(state);
                }
            }
        }

        if (state == AppState::Menu || state == AppState::Playing)
        {
            if (!musicStarted && musicLoaded)
            {
                static bool menuTrackLoaded = false;
                if (!menuTrackLoaded) {
                    if (menuTrackLoader.joinable()) {
                        menuTrackLoader.join();
                    }
                    menuTrackLoader = std::jthread([]() {
                        std::string menuTrack = AssetPath::resolveWithExtensions("assets/music/Every Block You Take", { ".mp3" });
                        if (!menuTrack.empty()) {
                            if (auto sys = AudioManager::get()) sys->setMenuTrack(menuTrack);
                        } else {
                            SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Menu track not found (Every Block You Take)");
                        }
                    });
                    menuTrackLoaded = true;
                }

                if (state == AppState::Menu) {
                    if (auto sys = AudioManager::get()) sys->playMenuMusic();
                } else {
                    if (auto sys = AudioManager::get()) sys->playGameMusic();
                }
                musicStarted = true;
            }
        }

        static AppState previousState = AppState::Loading;
        if (state != previousState && musicStarted) {
            if (state == AppState::Menu && previousState == AppState::Playing) {
                if (auto sys = AudioManager::get()) sys->playMenuMusic();
            } else if (state == AppState::Playing && previousState == AppState::Menu) {
                if (auto sys = AudioManager::get()) sys->playGameMusic();
            }
        }
        previousState = state;

        if (state == AppState::Loading) {
            starfield3D.update(float(frameMs / 1000.0f));
            starfield3D.resize(winW, winH);
        } else {
            starfield.update(float(frameMs / 1000.0f), logicalVP.x * 2 + logicalVP.w, logicalVP.y * 2 + logicalVP.h);
        }

        if (state == AppState::Menu) {
            spaceWarp.resize(winW, winH);
            spaceWarp.update(float(frameMs / 1000.0f));
        }

        levelBackgrounds.update(float(frameMs));

        if (state == AppState::Menu) {
            logoAnimCounter += frameMs * 0.0008;
        }

        switch (stateMgr->getState()) {
            case AppState::Loading:
                loadingState->update(frameMs);
                break;
            case AppState::Video:
                if (videoState) videoState->update(frameMs);
                break;
            case AppState::Menu:
                menuState->update(frameMs);
                break;
            case AppState::Options:
                optionsState->update(frameMs);
                break;
            case AppState::LevelSelector:
                levelSelectorState->update(frameMs);
                break;
            case AppState::Playing:
                playingState->update(frameMs);
                break;
            default:
                break;
        }

        if (state == AppState::Playing && game && game->getMode() == GameMode::Challenge && !gameplayCountdownActive && !challengeClearFxActive) {
            int queuedLevel = game->consumeQueuedChallengeLevel();
            if (queuedLevel > 0) {
                startChallengeClearFx(queuedLevel);
            }
        }

        ctx.logoTex = logoTex;
        ctx.logoSmallTex = logoSmallTex;
        ctx.logoSmallW = logoSmallW;
        ctx.logoSmallH = logoSmallH;
        ctx.backgroundTex = backgroundTex;
        ctx.blocksTex = blocksTex;
        ctx.asteroidsTex = asteroidsTex;
        ctx.scorePanelTex = scorePanelTex;
        ctx.statisticsPanelTex = statisticsPanelTex;
        ctx.nextPanelTex = nextPanelTex;
        ctx.holdPanelTex = holdPanelTex;
        ctx.mainScreenTex = mainScreenTex;
        ctx.mainScreenW = mainScreenW;
        ctx.mainScreenH = mainScreenH;

        if (menuFadePhase == MenuFadePhase::FadeOut) {
            menuFadeClockMs += frameMs;
            menuFadeAlpha = std::min(1.0f, float(menuFadeClockMs / MENU_PLAY_FADE_DURATION_MS));
            if (menuFadeClockMs >= MENU_PLAY_FADE_DURATION_MS) {
                if (state != menuFadeTarget) {
                    state = menuFadeTarget;
                    stateMgr->setState(state);
                }

                if (menuFadeTarget == AppState::Playing) {
                    gameplayCountdownSource = (game && game->getMode() == GameMode::Challenge)
                        ? CountdownSource::ChallengeLevel
                        : CountdownSource::MenuStart;
                    countdownLevel = game ? game->challengeLevel() : 1;
                    countdownGoalAsteroids = countdownLevel;
                    if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
                        captureChallengeStory(countdownLevel);
                        challengeCountdownWaitingForSpace = true;
                    } else {
                        challengeStoryText.clear();
                        challengeStoryLevel = 0;
                        challengeCountdownWaitingForSpace = false;
                    }
                    countdownAdvancesChallenge = false;
                    menuPlayCountdownArmed = true;
                    gameplayCountdownActive = false;
                    gameplayCountdownIndex = 0;
                    gameplayCountdownElapsed = 0.0;
                    game->setPaused(true);
                } else {
                    menuPlayCountdownArmed = false;
                    gameplayCountdownActive = false;
                    gameplayCountdownIndex = 0;
                    gameplayCountdownElapsed = 0.0;
                    challengeCountdownWaitingForSpace = false;
                    game->setPaused(false);
                }
                menuFadePhase = MenuFadePhase::FadeIn;
                menuFadeClockMs = MENU_PLAY_FADE_DURATION_MS;
                menuFadeAlpha = 1.0f;
            }
        } else if (menuFadePhase == MenuFadePhase::FadeIn) {
            menuFadeClockMs -= frameMs;
            menuFadeAlpha = std::max(0.0f, float(menuFadeClockMs / MENU_PLAY_FADE_DURATION_MS));
            if (menuFadeClockMs <= 0.0) {
                menuFadePhase = MenuFadePhase::None;
                menuFadeClockMs = 0.0;
                menuFadeAlpha = 0.0f;
            }
        }

        if (menuFadePhase == MenuFadePhase::None && menuPlayCountdownArmed && !gameplayCountdownActive && state == AppState::Playing) {
            gameplayCountdownSource = (game && game->getMode() == GameMode::Challenge)
                ? CountdownSource::ChallengeLevel
                : CountdownSource::MenuStart;
            countdownLevel = game ? game->challengeLevel() : 1;
            countdownGoalAsteroids = countdownLevel;
            if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
                captureChallengeStory(countdownLevel);
                challengeCountdownWaitingForSpace = true;
            } else {
                challengeStoryText.clear();
                challengeStoryLevel = 0;
                challengeCountdownWaitingForSpace = false;
            }
            countdownAdvancesChallenge = false;
            gameplayCountdownActive = true;
            menuPlayCountdownArmed = false;
            gameplayCountdownElapsed = 0.0;
            gameplayCountdownIndex = 0;
            game->setPaused(true);
        }

        if (gameplayCountdownActive && state == AppState::Playing) {
            if (!challengeCountdownWaitingForSpace || gameplayCountdownSource != CountdownSource::ChallengeLevel) {
                gameplayCountdownElapsed += frameMs;
                if (gameplayCountdownElapsed >= GAMEPLAY_COUNTDOWN_STEP_MS) {
                    gameplayCountdownElapsed -= GAMEPLAY_COUNTDOWN_STEP_MS;
                    ++gameplayCountdownIndex;
                    if (gameplayCountdownIndex >= static_cast<int>(GAMEPLAY_COUNTDOWN_LABELS.size())) {
                        gameplayCountdownActive = false;
                        gameplayCountdownElapsed = 0.0;
                        gameplayCountdownIndex = 0;
                        if (gameplayCountdownSource == CountdownSource::ChallengeLevel && countdownAdvancesChallenge && game) {
                            game->beginNextChallengeLevel();
                        }
                        countdownAdvancesChallenge = false;
                        game->setPaused(false);
                    }
                }
            }
        }

        if (state != AppState::Playing && gameplayCountdownActive) {
            gameplayCountdownActive = false;
            menuPlayCountdownArmed = false;
            gameplayCountdownElapsed = 0.0;
            gameplayCountdownIndex = 0;
            countdownAdvancesChallenge = false;
            challengeCountdownWaitingForSpace = false;
            game->setPaused(false);
        }

        if (state != AppState::Playing && challengeClearFxActive) {
            challengeClearFxActive = false;
            challengeClearFxElapsedMs = 0.0;
            challengeClearFxDurationMs = 0.0;
            challengeClearFxNextLevel = 0;
            challengeClearFxOrder.clear();
        }

        SDL_SetRenderViewport(renderer, nullptr);
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderClear(renderer);

        if (state == AppState::Playing) {
            int bgLevel = std::clamp(game->level(), 0, 32);
            levelBackgrounds.queueLevelBackground(renderer, bgLevel);
            levelBackgrounds.render(renderer, winW, winH, static_cast<float>(gameplayBackgroundClockMs));
        } else if (state == AppState::Loading) {
            starfield3D.draw(renderer);
        } else if (state == AppState::Menu) {
            spaceWarp.draw(renderer, 1.0f);
        } else if (state == AppState::LevelSelector || state == AppState::Options) {
            // No background texture
        } else {
            starfield.draw(renderer);
        }

        SDL_SetRenderViewport(renderer, &logicalVP);
        SDL_SetRenderScale(renderer, logicalScale, logicalScale);

        switch (state)
        {
        case AppState::Loading:
        {
            float contentScale = logicalScale;
            float contentW = LOGICAL_W * contentScale;
            float contentH = LOGICAL_H * contentScale;
            float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
            float contentOffsetY = (winH - contentH) * 0.5f / contentScale;

            auto drawRect = [&](float x, float y, float w, float h, SDL_Color c)
            { RenderPrimitives::fillRect(renderer, x + contentOffsetX, y + contentOffsetY, w, h, c); };

            const bool isLimitedHeight = LOGICAL_H < 450;
            const float logoHeight = logoTex ? (isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f) : 0;
            const float loadingTextHeight = 20;
            const float barHeight = 20;
            const float barPaddingVertical = isLimitedHeight ? 15 : 35;
            const float percentTextHeight = 24;
            const float spacingBetweenElements = isLimitedHeight ? 5 : 15;

            const float totalContentHeight = logoHeight +
                                           (logoHeight > 0 ? spacingBetweenElements : 0) +
                                           loadingTextHeight +
                                           barPaddingVertical +
                                           barHeight +
                                           spacingBetweenElements +
                                           percentTextHeight;

            float currentY = (LOGICAL_H - totalContentHeight) / 2.0f;

            if (logoTex)
            {
                const int lw = 872, lh = 273;
                const float maxLogoWidth = std::min(LOGICAL_W * 0.9f, 600.0f);
                const float availableHeight = isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f;
                const float availableWidth = maxLogoWidth;

                const float scaleFactorWidth = availableWidth / static_cast<float>(lw);
                const float scaleFactorHeight = availableHeight / static_cast<float>(lh);
                const float scaleFactor = std::min(scaleFactorWidth, scaleFactorHeight);

                const float displayWidth = lw * scaleFactor;
                const float displayHeight = lh * scaleFactor;
                const float logoX = (LOGICAL_W - displayWidth) / 2.0f;

                SDL_FRect dst{logoX + contentOffsetX, currentY + contentOffsetY, displayWidth, displayHeight};
                SDL_RenderTexture(renderer, logoTex, nullptr, &dst);

                currentY += displayHeight + spacingBetweenElements;
            }

            const char* loadingText = "LOADING";
            float textWidth = strlen(loadingText) * 12.0f;
            float textX = (LOGICAL_W - textWidth) / 2.0f;
            pixelFont.draw(renderer, textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255, 204, 0, 255});

            currentY += loadingTextHeight + barPaddingVertical;

            const int barW = 400, barH = 20;
            const int bx = (LOGICAL_W - barW) / 2;

            drawRect(bx - 3, currentY - 3, barW + 6, barH + 6, {68, 68, 80, 255});
            drawRect(bx, currentY, barW, barH, {34, 34, 34, 255});
            drawRect(bx, currentY, int(barW * loadingProgress), barH, {255, 204, 0, 255});

            currentY += barH + spacingBetweenElements;

            int percentage = int(loadingProgress * 100);
            char percentText[16];
            std::snprintf(percentText, sizeof(percentText), "%d%%", percentage);

            float percentWidth = strlen(percentText) * 12.0f;
            float percentX = (LOGICAL_W - percentWidth) / 2.0f;
            pixelFont.draw(renderer, percentX + contentOffsetX, currentY + contentOffsetY, percentText, 1.5f, {255, 204, 0, 255});

            {
                std::lock_guard<std::mutex> lk(assetLoadErrorsMutex);
                const int maxShow = 5;
                int count = static_cast<int>(assetLoadErrors.size());
                if (count > 0) {
                    int start = std::max(0, count - maxShow);
                    float errY = currentY + spacingBetweenElements + 8.0f;

                    std::string latest = assetLoadErrors.back();
                    std::string shortTitle = "SpaceTris - Missing assets";
                    if (!latest.empty()) {
                        std::string trimmed = latest;
                        if (trimmed.size() > 48) trimmed = trimmed.substr(0, 45) + "...";
                        shortTitle += ": ";
                        shortTitle += trimmed;
                    }
                    SDL_SetWindowTitle(window, shortTitle.c_str());

                    FILE* tf = fopen("tetris_trace.log", "a");
                    if (tf) {
                        fprintf(tf, "Loading error: %s\n", assetLoadErrors.back().c_str());
                        fclose(tf);
                    }

                    for (int i = start; i < count; ++i) {
                        const std::string& msg = assetLoadErrors[i];
                        std::string display = msg;
                        if (display.size() > 80) display = display.substr(0, 77) + "...";
                        pixelFont.draw(renderer, 80 + contentOffsetX, errY + contentOffsetY, display.c_str(), 0.85f, {255, 100, 100, 255});
                        errY += 20.0f;
                    }
                }
            }

            if (Settings::instance().isDebugEnabled()) {
                std::string cur;
                {
                    std::lock_guard<std::mutex> lk(currentLoadingMutex);
                    cur = currentLoadingFile;
                }
                char buf[128];
                int loaded = loadedTasks.load();
                int total = totalLoadingTasks.load();
                std::snprintf(buf, sizeof(buf), "Loaded: %d / %d", loaded, total);
                float debugX = 20.0f + contentOffsetX;
                float debugY = LOGICAL_H - 48.0f + contentOffsetY;
                pixelFont.draw(renderer, debugX, debugY, buf, 0.9f, SDL_Color{200,200,200,255});
                if (!cur.empty()) {
                    std::string display = "Loading: ";
                    display += cur;
                    if (display.size() > 80) display = display.substr(0,77) + "...";
                    pixelFont.draw(renderer, debugX, debugY + 18.0f, display.c_str(), 0.85f, SDL_Color{200,180,120,255});
                }
            }
        }
        break;
        case AppState::Video:
            if (videoState) {
                videoState->render(renderer, logicalScale, logicalVP);
            }
            break;
        case AppState::Menu:
            if (!mainScreenTex) {
                mainScreenTex = assetLoader.getTexture(Assets::MAIN_SCREEN);
            }
            if (!mainScreenTex) {
                SDL_Texture* loaded = textureLoader->loadFromImage(renderer, Assets::MAIN_SCREEN, &mainScreenW, &mainScreenH);
                if (loaded) {
                    assetLoader.adoptTexture(Assets::MAIN_SCREEN, loaded);
                    mainScreenTex = loaded;
                }
            }
            if (menuState) {
                menuState->drawMainButtonNormally = false;
                menuState->render(renderer, logicalScale, logicalVP);
            }
            if (mainScreenTex) {
                SDL_SetRenderViewport(renderer, nullptr);
                SDL_SetRenderScale(renderer, 1.f, 1.f);
                float texW = mainScreenW > 0 ? static_cast<float>(mainScreenW) : 0.0f;
                float texH = mainScreenH > 0 ? static_cast<float>(mainScreenH) : 0.0f;
                if (texW <= 0.0f || texH <= 0.0f) {
                    float iwf = 0.0f, ihf = 0.0f;
                    if (!SDL_GetTextureSize(mainScreenTex, &iwf, &ihf)) {
                        iwf = ihf = 0.0f;
                    }
                    texW = iwf;
                    texH = ihf;
                }
                if (texW > 0.0f && texH > 0.0f) {
                    const float drawH = static_cast<float>(winH);
                    const float scale = drawH / texH;
                    const float drawW = texW * scale;
                    SDL_FRect dst{
                        (winW - drawW) * 0.5f,
                        0.0f,
                        drawW,
                        drawH
                    };
                    SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND);
                    SDL_SetTextureScaleMode(mainScreenTex, SDL_SCALEMODE_LINEAR);
                    SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst);
                }
                SDL_SetRenderViewport(renderer, &logicalVP);
                SDL_SetRenderScale(renderer, logicalScale, logicalScale);
            }
            if (menuState) {
                menuState->renderMainButtonTop(renderer, logicalScale, logicalVP);
            }
            break;
        case AppState::Options:
            optionsState->render(renderer, logicalScale, logicalVP);
            break;
        case AppState::LevelSelector:
            levelSelectorState->render(renderer, logicalScale, logicalVP);
            break;
        case AppState::LevelSelect:
        {
            const std::string title = "SELECT LEVEL";
            int tW = 0, tH = 0;
            font.measure(title, 2.5f, tW, tH);
            float titleX = (LOGICAL_W - (float)tW) / 2.0f;
            font.draw(renderer, titleX, 80, title, 2.5f, SDL_Color{255, 220, 0, 255});

            char buf[64];
            std::snprintf(buf, sizeof(buf), "LEVEL: %d", startLevelSelection);
            font.draw(renderer, LOGICAL_W * 0.5f - 80, 180, buf, 2.0f, SDL_Color{200, 240, 255, 255});
            font.draw(renderer, LOGICAL_W * 0.5f - 180, 260, "ARROWS CHANGE  ENTER=OK  ESC=BACK", 1.2f, SDL_Color{200, 200, 220, 255});
        }
            break;
        case AppState::Playing:
            playingState->render(renderer, logicalScale, logicalVP);
            break;
        case AppState::GameOver:
            GameRenderer::renderPlayingState(
                renderer,
                game.get(),
                &pixelFont,
                &lineEffect,
                blocksTex,
                asteroidsTex,
                ctx.statisticsPanelTex,
                scorePanelTex,
                nextPanelTex,
                holdPanelTex,
                false,
                (float)LOGICAL_W,
                (float)LOGICAL_H,
                logicalScale,
                (float)winW,
                (float)winH
            );

            {
                SDL_SetRenderViewport(renderer, nullptr);
                SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
                SDL_FRect fullWin{0.f, 0.f, (float)winW, (float)winH};
                SDL_RenderFillRect(renderer, &fullWin);

                SDL_SetRenderViewport(renderer, &logicalVP);
                SDL_SetRenderScale(renderer, logicalScale, logicalScale);

                float contentScale = logicalScale;
                float contentW = LOGICAL_W * contentScale;
                float contentH = LOGICAL_H * contentScale;
                float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
                float contentOffsetY = (winH - contentH) * 0.5f / contentScale;

                float boxW = 500.0f;
                float boxH = 350.0f;
                float boxX = (LOGICAL_W - boxW) * 0.5f;
                float boxY = (LOGICAL_H - boxH) * 0.5f;

                SDL_SetRenderDrawColor(renderer, 20, 25, 35, 255);
                SDL_FRect boxRect{boxX + contentOffsetX, boxY + contentOffsetY, boxW, boxH};
                SDL_RenderFillRect(renderer, &boxRect);

                SDL_SetRenderDrawColor(renderer, 100, 120, 200, 255);
                SDL_FRect borderRect{boxX + contentOffsetX - 3, boxY + contentOffsetY - 3, boxW + 6, boxH + 6};
                SDL_RenderFillRect(renderer, &borderRect);
                SDL_SetRenderDrawColor(renderer, 20, 25, 35, 255);
                SDL_RenderFillRect(renderer, &boxRect);

                ensureScoresLoaded();
                // Choose display values based on mode (single-player vs coop)
                int displayScore = 0;
                int displayLines = 0;
                int displayLevel = 0;
                if (game && game->getMode() == GameMode::Cooperate && coopGame) {
                    int leftScore = coopGame->score(CoopGame::PlayerSide::Left);
                    int rightScore = coopGame->score(CoopGame::PlayerSide::Right);
                    displayScore = leftScore + rightScore;
                    displayLines = coopGame->lines();
                    displayLevel = coopGame->level();
                } else if (game) {
                    displayScore = game->score();
                    displayLines = game->lines();
                    displayLevel = game->level();
                }

                bool realHighScore = scores.isHighScore(displayScore);
                const char* title = realHighScore ? "NEW HIGH SCORE!" : "GAME OVER";
                int tW=0, tH=0; pixelFont.measure(title, 2.0f, tW, tH);
                pixelFont.draw(renderer, boxX + (boxW - tW) * 0.5f + contentOffsetX, boxY + 40 + contentOffsetY, title, 2.0f, realHighScore ? SDL_Color{255, 220, 0, 255} : SDL_Color{255, 60, 60, 255});

                char scoreStr[64];
                snprintf(scoreStr, sizeof(scoreStr), "SCORE: %d", displayScore);
                int sW=0, sH=0; pixelFont.measure(scoreStr, 1.2f, sW, sH);
                pixelFont.draw(renderer, boxX + (boxW - sW) * 0.5f + contentOffsetX, boxY + 100 + contentOffsetY, scoreStr, 1.2f, {255, 255, 255, 255});

                if (isNewHighScore) {
                    const bool isCoopEntry = (game && game->getMode() == GameMode::Cooperate && coopGame);
                    const char* enterName = isCoopEntry ? "ENTER NAMES:" : "ENTER NAME:";
                    int enW=0, enH=0; pixelFont.measure(enterName, 1.0f, enW, enH);
                    if (!isCoopEntry) {
                        pixelFont.draw(renderer, boxX + (boxW - enW) * 0.5f + contentOffsetX, boxY + 160 + contentOffsetY, enterName, 1.0f, {200, 200, 220, 255});
                    }

                    const float inputW = isCoopEntry ? 260.0f : 300.0f;
                    const float inputH = 40.0f;
                    const float inputX = boxX + (boxW - inputW) * 0.5f;
                    const float inputY = boxY + 200.0f;

                    const float nameScale = 1.2f;
                    const bool showCursor = ((SDL_GetTicks() / 500) % 2) == 0;

                    int metricsW = 0, metricsH = 0;
                    pixelFont.measure("A", nameScale, metricsW, metricsH);
                    if (metricsH == 0) metricsH = 24;

                    // Single name entry (non-coop) --- keep original behavior
                    if (!isCoopEntry) {
                        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
                        SDL_FRect inputRect{inputX + contentOffsetX, inputY + contentOffsetY, inputW, inputH};
                        SDL_RenderFillRect(renderer, &inputRect);
                        SDL_SetRenderDrawColor(renderer, 255, 220, 0, 255);
                        SDL_RenderRect(renderer, &inputRect);

                        int nameW = 0, nameH = 0;
                        if (!playerName.empty()) pixelFont.measure(playerName, nameScale, nameW, nameH);
                        else nameH = metricsH;

                        float textX = inputX + (inputW - static_cast<float>(nameW)) * 0.5f + contentOffsetX;
                        float textY = inputY + (inputH - static_cast<float>(metricsH)) * 0.5f + contentOffsetY;

                        if (!playerName.empty()) pixelFont.draw(renderer, textX, textY, playerName, nameScale, {255,255,255,255});

                        if (showCursor) {
                            int cursorW = 0, cursorH = 0; pixelFont.measure("_", nameScale, cursorW, cursorH);
                            float cursorX = playerName.empty() ? inputX + (inputW - static_cast<float>(cursorW)) * 0.5f + contentOffsetX : textX + static_cast<float>(nameW);
                            float cursorY = inputY + (inputH - static_cast<float>(cursorH)) * 0.5f + contentOffsetY;
                            pixelFont.draw(renderer, cursorX, cursorY, "_", nameScale, {255,255,255,255});
                        }

                        const char* hint = "PRESS ENTER TO SUBMIT";
                        int hW=0, hH=0; pixelFont.measure(hint, 0.8f, hW, hH);
                        pixelFont.draw(renderer, boxX + (boxW - hW) * 0.5f + contentOffsetX, boxY + 280 + contentOffsetY, hint, 0.8f, {150, 150, 150, 255});
                    } else {
                        // Coop: prompt sequentially. First ask Player 1, then ask Player 2 after Enter.
                        const bool askingP1 = (highScoreEntryIndex == 0);
                        const char* label = askingP1 ? "PLAYER 1:" : "PLAYER 2:";
                        int labW=0, labH=0; pixelFont.measure(label, 1.0f, labW, labH);
                        pixelFont.draw(renderer, boxX + (boxW - labW) * 0.5f + contentOffsetX, boxY + 160 + contentOffsetY, label, 1.0f, {200,200,220,255});

                        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
                        SDL_FRect rect{inputX + contentOffsetX, inputY + contentOffsetY, inputW, inputH};
                        SDL_RenderFillRect(renderer, &rect);
                        SDL_SetRenderDrawColor(renderer, 255, 220, 0, 255);
                        SDL_RenderRect(renderer, &rect);

                        const std::string &activeName = askingP1 ? playerName : player2Name;
                        int nameW = 0, nameH = 0;
                        if (!activeName.empty()) pixelFont.measure(activeName, nameScale, nameW, nameH);
                        else nameH = metricsH;

                        float textX = inputX + (inputW - static_cast<float>(nameW)) * 0.5f + contentOffsetX;
                        float textY = inputY + (inputH - static_cast<float>(metricsH)) * 0.5f + contentOffsetY;
                        if (!activeName.empty()) pixelFont.draw(renderer, textX, textY, activeName, nameScale, {255,255,255,255});

                        if (showCursor) {
                            int cursorW=0, cursorH=0; pixelFont.measure("_", nameScale, cursorW, cursorH);
                            float cursorX = activeName.empty() ? inputX + (inputW - static_cast<float>(cursorW)) * 0.5f + contentOffsetX : textX + static_cast<float>(nameW);
                            float cursorY = inputY + (inputH - static_cast<float>(cursorH)) * 0.5f + contentOffsetY;
                            pixelFont.draw(renderer, cursorX, cursorY, "_", nameScale, {255,255,255,255});
                        }

                        const char* hint = askingP1 ? "PRESS ENTER FOR NEXT NAME" : "PRESS ENTER TO SUBMIT";
                        int hW=0, hH=0; pixelFont.measure(hint, 0.8f, hW, hH);
                        pixelFont.draw(renderer, boxX + (boxW - hW) * 0.5f + contentOffsetX, boxY + 300 + contentOffsetY, hint, 0.8f, {150, 150, 150, 255});
                    }

                } else {
                    char linesStr[64];
                    snprintf(linesStr, sizeof(linesStr), "LINES: %d", game->lines());
                    int lW=0, lH=0; pixelFont.measure(linesStr, 1.2f, lW, lH);
                    pixelFont.draw(renderer, boxX + (boxW - lW) * 0.5f + contentOffsetX, boxY + 140 + contentOffsetY, linesStr, 1.2f, {255, 255, 255, 255});

                    char levelStr[64];
                    snprintf(levelStr, sizeof(levelStr), "LEVEL: %d", game->level());
                    int lvW=0, lvH=0; pixelFont.measure(levelStr, 1.2f, lvW, lvH);
                    pixelFont.draw(renderer, boxX + (boxW - lvW) * 0.5f + contentOffsetX, boxY + 180 + contentOffsetY, levelStr, 1.2f, {255, 255, 255, 255});

                    const char* instr = "PRESS ENTER TO RESTART";
                    int iW=0, iH=0; pixelFont.measure(instr, 0.9f, iW, iH);
                    pixelFont.draw(renderer, boxX + (boxW - iW) * 0.5f + contentOffsetX, boxY + 260 + contentOffsetY, instr, 0.9f, {255, 220, 0, 255});

                    const char* instr2 = "PRESS ESC FOR MENU";
                    int iW2=0, iH2=0; pixelFont.measure(instr2, 0.9f, iW2, iH2);
                    pixelFont.draw(renderer, boxX + (boxW - iW2) * 0.5f + contentOffsetX, boxY + 290 + contentOffsetY, instr2, 0.9f, {255, 220, 0, 255});
                }
            }
            break;
        }

        if (menuFadeAlpha > 0.0f) {
            SDL_SetRenderViewport(renderer, nullptr);
            SDL_SetRenderScale(renderer, 1.f, 1.f);
            SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
            Uint8 alpha = Uint8(std::clamp(menuFadeAlpha, 0.0f, 1.0f) * 255.0f);
            SDL_SetRenderDrawColor(renderer, 0, 0, 0, alpha);
            SDL_FRect fadeRect{0.f, 0.f, (float)winW, (float)winH};
            SDL_RenderFillRect(renderer, &fadeRect);
            SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
            SDL_SetRenderViewport(renderer, &logicalVP);
            SDL_SetRenderScale(renderer, logicalScale, logicalScale);
        }

        if (gameplayCountdownActive && state == AppState::Playing) {
            SDL_SetRenderViewport(renderer, nullptr);
            SDL_SetRenderScale(renderer, 1.f, 1.f);
            SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

            int cappedIndex = std::min(gameplayCountdownIndex, static_cast<int>(GAMEPLAY_COUNTDOWN_LABELS.size()) - 1);
            const char* label = GAMEPLAY_COUNTDOWN_LABELS[cappedIndex];
            bool isFinalCue = (cappedIndex == static_cast<int>(GAMEPLAY_COUNTDOWN_LABELS.size()) - 1);
            float textScale = isFinalCue ? 4.5f : 5.0f;
            int textW = 0, textH = 0;
            pixelFont.measure(label, textScale, textW, textH);

            float textX = (winW - static_cast<float>(textW)) * 0.5f;
            float textY = (winH - static_cast<float>(textH)) * 0.5f;
            if (gameplayCountdownSource == CountdownSource::ChallengeLevel) {
                char levelBuf[32];
                std::snprintf(levelBuf, sizeof(levelBuf), "LEVEL %d", countdownLevel);
                int lvlW = 0, lvlH = 0;
                float lvlScale = 2.5f;
                pixelFont.measure(levelBuf, lvlScale, lvlW, lvlH);
                float levelX = (winW - static_cast<float>(lvlW)) * 0.5f;
                float levelY = winH * 0.32f;
                pixelFont.draw(renderer, levelX, levelY, levelBuf, lvlScale, SDL_Color{140, 210, 255, 255});

                char goalBuf[64];
                std::snprintf(goalBuf, sizeof(goalBuf), "ASTEROIDS: %d", countdownGoalAsteroids);
                int goalW = 0, goalH = 0;
                float goalScale = 1.7f;
                pixelFont.measure(goalBuf, goalScale, goalW, goalH);
                float goalX = (winW - static_cast<float>(goalW)) * 0.5f;
                float goalY = levelY + static_cast<float>(lvlH) + 14.0f;
                pixelFont.draw(renderer, goalX, goalY, goalBuf, goalScale, SDL_Color{220, 245, 255, 255});

                // Optional story/briefing line
                if (!challengeStoryText.empty() && challengeStoryAlpha > 0.0f) {
                    SDL_Color storyColor{170, 230, 255, static_cast<Uint8>(std::lround(255.0f * challengeStoryAlpha))};
                    SDL_Color shadowColor{0, 0, 0, static_cast<Uint8>(std::lround(160.0f * challengeStoryAlpha))};

                    auto drawCenteredWrapped = [&](const std::string& text, float y, float maxWidth, float scale) {
                        std::istringstream iss(text);
                        std::string word;
                        std::string line;
                        float cursorY = y;
                        int lastH = 0;
                        while (iss >> word) {
                            std::string candidate = line.empty() ? word : (line + " " + word);
                            int candidateW = 0, candidateH = 0;
                            pixelFont.measure(candidate, scale, candidateW, candidateH);
                            if (candidateW > maxWidth && !line.empty()) {
                                int lineW = 0, lineH = 0;
                                pixelFont.measure(line, scale, lineW, lineH);
                                float lineX = (winW - static_cast<float>(lineW)) * 0.5f;
                                pixelFont.draw(renderer, lineX + 1.0f, cursorY + 1.0f, line, scale, shadowColor);
                                pixelFont.draw(renderer, lineX, cursorY, line, scale, storyColor);
                                cursorY += lineH + 6.0f;
                                line = word;
                                lastH = lineH;
                            } else {
                                line = candidate;
                                lastH = candidateH;
                            }
                        }
                        if (!line.empty()) {
                            int w = 0, h = 0;
                            pixelFont.measure(line, scale, w, h);
                            float lineX = (winW - static_cast<float>(w)) * 0.5f;
                            pixelFont.draw(renderer, lineX + 1.0f, cursorY + 1.0f, line, scale, shadowColor);
                            pixelFont.draw(renderer, lineX, cursorY, line, scale, storyColor);
                            cursorY += h + 6.0f;
                        }
                        return cursorY;
                    };

                    float storyStartY = goalY + static_cast<float>(goalH) + 22.0f;
                    float usedY = drawCenteredWrapped(challengeStoryText, storyStartY, std::min<float>(winW * 0.7f, 720.0f), 1.0f);
                    float promptY = usedY + 10.0f;
                    if (challengeCountdownWaitingForSpace) {
                        const char* prompt = "PRESS SPACE";
                        int pW = 0, pH = 0;
                        float pScale = 1.35f;
                        pixelFont.measure(prompt, pScale, pW, pH);
                        float px = (winW - static_cast<float>(pW)) * 0.5f;
                        pixelFont.draw(renderer, px + 2.0f, promptY + 2.0f, prompt, pScale, SDL_Color{0, 0, 0, 200});
                        pixelFont.draw(renderer, px, promptY, prompt, pScale, SDL_Color{255, 220, 40, 255});
                        promptY += pH + 14.0f;
                    }
                    textY = promptY + 10.0f;
                } else {
                    if (challengeCountdownWaitingForSpace) {
                        const char* prompt = "PRESS SPACE";
                        int pW = 0, pH = 0;
                        float pScale = 1.35f;
                        pixelFont.measure(prompt, pScale, pW, pH);
                        float px = (winW - static_cast<float>(pW)) * 0.5f;
                        float py = goalY + static_cast<float>(goalH) + 18.0f;
                        pixelFont.draw(renderer, px + 2.0f, py + 2.0f, prompt, pScale, SDL_Color{0, 0, 0, 200});
                        pixelFont.draw(renderer, px, py, prompt, pScale, SDL_Color{255, 220, 40, 255});
                        textY = py + pH + 24.0f;
                    } else {
                        textY = goalY + static_cast<float>(goalH) + 38.0f;
                    }
                }
            } else {
                textY = winH * 0.38f;
            }
            if (!(gameplayCountdownSource == CountdownSource::ChallengeLevel && challengeCountdownWaitingForSpace)) {
                SDL_Color textColor = isFinalCue ? SDL_Color{255, 230, 90, 255} : SDL_Color{255, 255, 255, 255};
                pixelFont.draw(renderer, textX, textY, label, textScale, textColor);
            }

            SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
        }

        if (showHelpOverlay) {
            SDL_SetRenderViewport(renderer, &logicalVP);
            SDL_SetRenderScale(renderer, logicalScale, logicalScale);
            float contentOffsetX = 0.0f;
            float contentOffsetY = 0.0f;
            if (logicalScale > 0.0f) {
                float scaledW = LOGICAL_W * logicalScale;
                float scaledH = LOGICAL_H * logicalScale;
                contentOffsetX = (winW - scaledW) * 0.5f / logicalScale;
                contentOffsetY = (winH - scaledH) * 0.5f / logicalScale;
            }
            HelpOverlay::Render(renderer, pixelFont, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY);
        }

        if (startupFadeActive && startupFadeAlpha > 0.0f) {
            SDL_SetRenderViewport(renderer, nullptr);
            SDL_SetRenderScale(renderer, 1.0f, 1.0f);
            SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
            const Uint8 a = (Uint8)std::clamp((int)std::lround(startupFadeAlpha * 255.0f), 0, 255);
            SDL_SetRenderDrawColor(renderer, 0, 0, 0, a);
            SDL_FRect full{0.f, 0.f, (float)winW, (float)winH};
            SDL_RenderFillRect(renderer, &full);
            SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
        }

        SDL_RenderPresent(renderer);
        SDL_SetRenderScale(renderer, 1.f, 1.f);
    }
}

void TetrisApp::Impl::shutdown()
{
    Settings::instance().save();

    // BackgroundManager owns its own textures.
    levelBackgrounds.reset();

    // All textures are owned by AssetLoader (including legacy fallbacks adopted above).
    logoTex = nullptr;
    logoSmallTex = nullptr;
    backgroundTex = nullptr;
    mainScreenTex = nullptr;
    blocksTex = nullptr;
    scorePanelTex = nullptr;
    statisticsPanelTex = nullptr;
    nextPanelTex = nullptr;

    if (scoreLoader.joinable()) {
        scoreLoader.join();
        if (!ctx.scores) {
            ctx.scores = &scores;
        }
    }
    if (menuTrackLoader.joinable()) {
        menuTrackLoader.join();
    }

    lineEffect.shutdown();
    if (auto sys = AudioManager::get()) sys->shutdown();
    SoundEffectManager::instance().shutdown();

    // Destroy textures before tearing down the renderer/window.
    assetLoader.shutdown();

    pixelFont.shutdown();
    font.shutdown();

    TTF_Quit();

    if (renderer) {
        SDL_DestroyRenderer(renderer);
        renderer = nullptr;
    }
    if (window) {
        SDL_DestroyWindow(window);
        window = nullptr;
    }
    SDL_Quit();
}
