Ham Kaynak
max / ๐ŸŽธ Songsterr Ultimate (Premium Unlocked)

// ==UserScript==
// @name           ๐ŸŽธ Songsterr Ultimate (Premium Unlocked)
// @name:en        ๐ŸŽธ Songsterr Ultimate (Premium Unlocked)
// @name:fr        ๐ŸŽธ Songsterr Ultime (Premium Dรฉbloquรฉ)
// @name:es        ๐ŸŽธ Songsterr Definitivo (Premium Desbloqueado)
// @name:de        ๐ŸŽธ Songsterr Ultimativ (Premium Freigeschaltet)
// @name:it        ๐ŸŽธ Songsterr Definitivo (Premium Sbloccato)
// @name:pt        ๐ŸŽธ Songsterr Supremo (Premium Desbloqueado)
// @name:pt-BR     ๐ŸŽธ Songsterr Supremo (Full Premium)
// @name:nl        ๐ŸŽธ Songsterr Ultiem (Premium Ontgrendeld)
// @name:pl        ๐ŸŽธ Songsterr Ostateczny (Premium Odblokowany)
// @name:ru        ๐ŸŽธ Songsterr ะะฑัะพะปัŽั‚ะฝั‹ะน (ะŸั€ะตะผะธัƒะผ ะ ะฐะทะฑะปะพะบะธั€ะพะฒะฐะฝ)
// @name:ja        ๐ŸŽธ Songsterr ็ฉถๆฅต (ใƒ—ใƒฌใƒŸใ‚ขใƒ ่งฃ้™ค)
// @name:ko        ๐ŸŽธ Songsterr ๊ถ๊ทน (ํ”„๋ฆฌ๋ฏธ์—„ ์–ธ๋ฝ)
// @name:sv        ๐ŸŽธ Songsterr Ultimat (Premium Upplรฅst)
// @name:da        ๐ŸŽธ Songsterr Ultimativ (Premium Lรฅst Op)
// @namespace    http://tampermonkey.net/
// @version      4.0.0
// @description                    Unlocks all Plus features (Speed, Loop, Solo, Mute, no pauses) + Native Export (.gp & .midi). (Tested on Zen Browser)
// @description:fr                 Dรฉbloque toutes les fonctionnalitรฉs Plus (Vitesse, Loop, Solo, Mute, sans pauses) + Tรฉlรฉchargement natif (.gp & .midi). (Testรฉ sur Zen Browser)
// @description:es                 Desbloquea todas las funciones Plus (Velocidad, Bucle, Solo, Mute, sin pausas) + Descarga nativa (.gp y .midi). (Probado en Zen Browser)
// @description:de                 Schaltet alle Plus-Features frei (Geschwindigkeit, Loop, Solo, Mute, ohne Pausen) + Nativer Download (.gp & .midi). (Getestet auf Zen Browser)
// @description:it                 Sblocca tutte le funzioni Plus (Velocitร , Loop, Solo, Mute, senza pause) + Download nativo (.gp e .midi). (Testato su Zen Browser)
// @description:pt                 Desbloqueia todos os recursos Plus (Velocidade, Loop, Solo, Mute, sem pausas) + Download nativo (.gp e .midi). (Testado no Zen Browser)
// @description:pt-BR              Desbloqueia tudo do Plus (Velocidade, Loop, Solo, Mute, sem pausas) + Export nativo (.gp & .midi). (Testado no Zen Browser)
// @description:nl                 Ontgrendelt alle Plus-functies (Snelheid, Loop, Solo, Mute, geen pauzes) + Native download (.gp & .midi). (Getest op Zen Browser)
// @description:pl                 Odblokowuje wszystkie funkcje Plus (Prฤ™dkoล›ฤ‡, Pฤ™tla, Solo, Mute, bez przerw) + Natywny eksport (.gp i .midi). (Testowane na Zen Browser)
// @description:ru                 ะ ะฐะทะฑะปะพะบะธั€ัƒะตั‚ ะฒัะต ั„ัƒะฝะบั†ะธะธ Plus (ะกะบะพั€ะพัั‚ัŒ, ะŸะตั‚ะปั, ะกะพะปะพ, Mute, ะฑะตะท ะฟะฐัƒะท) + ะะฐั‚ะธะฒะฝั‹ะน ัะบัะฟะพั€ั‚ (.gp ะธ .midi). (ะŸั€ะพั‚ะตัั‚ะธั€ะพะฒะฐะฝะพ ะฒ Zen Browser)
// @description:ja                 Plusใฎๅ…จๆฉŸ่ƒฝ๏ผˆ้€Ÿๅบฆใ€ใƒซใƒผใƒ—ใ€ใ‚ฝใƒญใ€ใƒŸใƒฅใƒผใƒˆใ€็„กๅœๆญข๏ผ‰ใ‚’่งฃ้™ค + ใƒใ‚คใƒ†ใ‚ฃใƒ–ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰๏ผˆ.gp & .midi๏ผ‰ใ€‚(Zen Browserใงใƒ†ใ‚นใƒˆๆธˆใฟ)
// @description:ko                 ๋ชจ๋“  Plus ๊ธฐ๋Šฅ ํ•ด์ œ (์†๋„, ๋ฃจํ”„, ์†”๋กœ, ๋ฎคํŠธ, ๋ฉˆ์ถค ์—†์Œ) + ๋„ค์ดํ‹ฐ๋ธŒ ๋‹ค์šด๋กœ๋“œ (.gp & .midi). (Zen Browser์—์„œ ํ…Œ์ŠคํŠธ๋จ)
// @description:sv                 Lรฅser upp alla Plus-funktioner (Hastighet, Loop, Solo, Mute, inga pauser) + Naturlig export (.gp & .midi). (Testat pรฅ Zen Browser)
// @description:da                 Lรฅser alle Plus-funktioner op (Hastighed, Loop, Solo, Mute, ingen pauser) + Naturlig download (.gp & .midi). (Testet pรฅ Zen Browser)
// @author       Goulagman
// @supportURL   https://github.com/GoulagmanYt/Songsterr-Plus-Ultimate-Unlocker-
// @match        *://www.songsterr.com/*
// @require      https://cdn.jsdelivr.net/npm/@coderline/alphatab@1.8.1/dist/alphaTab.min.js
// @connect      dqsljvtekg760.cloudfront.net
// @connect      d3d3l6a6rcgkaf.cloudfront.net
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // LOGGING SYSTEM with Toggle
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const LOG_KEY = 'songsterr_debug_logging';
  let loggingEnabled = false;
  try {
    loggingEnabled = localStorage.getItem(LOG_KEY) === 'true';
  } catch (e) {}

  // Wrapper for console logging - supports different prefixes
  function sgdLog(level, prefix, ...args) {
    if (!loggingEnabled) return;
    const fullPrefix = prefix ? `[${prefix}]` : '[SGD]';
    if (level === 'error') console.error(fullPrefix, ...args);
    else if (level === 'warn') console.warn(fullPrefix, ...args);
    else console.log(fullPrefix, ...args);
  }

  // Toggle logging function
  window.toggleSgdLogging = function() {
    loggingEnabled = !loggingEnabled;
    try {
      localStorage.setItem(LOG_KEY, loggingEnabled);
    } catch (e) {}
    console.log(`๐ŸŽธ Songsterr Ultimate โ€” Logging ${loggingEnabled ? 'ENABLED' : 'DISABLED'}`);
    updateLogToggleUI();
  };

  // Update toggle button UI
  function updateLogToggleUI() {
    const btn = document.getElementById('sgd-log-toggle');
    if (btn) {
      btn.innerHTML = `<span style="font-size: 16px;">๐Ÿ“‹</span> <span style="font-weight: 700;">Logging</span> <span style="font-size: 14px;">${loggingEnabled ? 'ON' : 'OFF'}</span>`;
      btn.title = loggingEnabled ? 'Debug logging enabled (click to disable)' : 'Debug logging disabled (click to enable)';
      btn.classList.toggle('active', loggingEnabled);
      // Colors are handled by CSS classes (.active / :not(.active))
    }
  }

  // Inject logging toggle into Gl5687 div
  function injectLogToggle() {
    const targetDiv = document.querySelector('.Gl5687');
    if (!targetDiv || document.getElementById('sgd-log-toggle')) return;

    // Style the target div to allow centering
    targetDiv.style.cssText = `
      display: flex !important;
      align-items: center !important;
      justify-content: center !important;
      width: 100% !important;
      height: 100% !important;
    `;

    const btn = document.createElement('button');
    btn.id = 'sgd-log-toggle';
    btn.className = 'sgd-log-toggle-btn';
    btn.innerHTML = `<span style="font-size: 16px;">๐Ÿ“‹</span> <span style="font-weight: 700;">Logging</span> <span style="font-size: 14px;">${loggingEnabled ? 'ON' : 'OFF'}</span>`;
    btn.title = loggingEnabled ? 'Debug logging enabled (click to disable)' : 'Debug logging disabled (click to enable)';
    btn.addEventListener('click', window.toggleSgdLogging);

    // Style the button - dark theme matching Songsterr
    btn.style.cssText = `
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      padding: 8px 16px;
      border: 1px solid ${loggingEnabled ? '#16a34a' : '#dc2626'};
      border-radius: 6px;
      background: ${loggingEnabled ? '#166534' : '#7f1d1d'};
      color: #fff;
      font-size: 14px;
      font-weight: 500;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      cursor: pointer;
      transition: all 0.15s ease;
      min-width: 120px;
      height: 36px;
    `;

    btn.addEventListener('mouseenter', () => {
      btn.style.background = loggingEnabled ? '#15803d' : '#991b1b';
    });
    btn.addEventListener('mouseleave', () => {
      btn.style.background = loggingEnabled ? '#166534' : '#7f1d1d';
    });

    targetDiv.appendChild(btn);
  }

  // Watch for Gl5687 div to appear
  const logToggleObserver = new MutationObserver(() => {
    injectLogToggle();
  });
  logToggleObserver.observe(document.documentElement, { childList: true, subtree: true });

  // Initial log
  console.log('๐ŸŽธ Songsterr Ultimate โ€” Active v4.0.0', loggingEnabled ? '(Debug logging ON)' : '');

  // Replace all console.log throughout the script with sgdLog
  // (This will be done via find/replace in subsequent edits)

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // YOUTUBE AUDIO-ONLY SYSTEM (contribution ใƒ‘ใƒ—ใƒชใ‚ซ)
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  (function() {
    'use strict';

    // To avoid audio stopping issues (browser power-saving features, etc.),
    // we don't use display: none for parent elements either. Instead we use
    // size 0, transparency, and absolute positioning to completely
    // remove them from visual view while keeping them functional.
    const style = document.createElement('style');
    style.id = 'paprika-yt-styles';
    style.textContent = `
        /* Hide iframe itself (exists in DOM, audio continues playing) */
        .songsterr-yt-hidden-iframe {
            opacity: 0 !important;
            width: 0px !important;
            height: 0px !important;
            pointer-events: none !important;
            position: absolute !important;
            z-index: -9999 !important;
            border: none !important;
        }
        
        /* Hide iframe parent wrapper/background frame elements */
        .songsterr-yt-hidden-wrapper {
            opacity: 0 !important;
            width: 0px !important;
            height: 0px !important;
            min-width: 0px !important;
            min-height: 0px !important;
            margin: 0 !important;
            padding: 0 !important;
            border: none !important;
            overflow: hidden !important;
            background: transparent !important;
            position: absolute !important;
            pointer-events: none !important;
            z-index: -9999 !important;
        }
        
        /* YouTube Audio-Only toggle button - dark theme */
        #yt-toggle-btn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 36px;
            height: 36px;
            border-radius: 6px;
            border: 1px solid #3a3a3a;
            background: #2a2a2a;
            color: #a5a5a5;
            cursor: pointer;
            font-size: 16px;
            transition: all 0.15s ease;
        }
        #yt-toggle-btn:hover { background: #3a3a3a; color: #e5e5e5; }
        #yt-toggle-btn.audio-only { background: #16a34a; color: #fff; border-color: #22c55e; }
        #yt-toggle-btn.audio-only:hover { background: #15803d; border-color: #16a34a; }
    `;
    
    if (document.head) {
        document.head.appendChild(style);
    } else {
        document.addEventListener('DOMContentLoaded', () => document.head.appendChild(style));
    }

    // Audio-only mode state (persistent via localStorage)
    const YT_AUDIO_KEY = 'songsterr_yt_audio_only';
    let ytAudioOnlyMode = false;
    try {
        ytAudioOnlyMode = localStorage.getItem(YT_AUDIO_KEY) === 'true';
    } catch (e) {}

    /**
     * Function to detect and hide YouTube iframes and multi-level frames (black backgrounds, etc.)
     */
    function hideYouTubeIframes() {
        if (!ytAudioOnlyMode) return; // Only hide if audio-only mode is activated
        
        const iframes = document.querySelectorAll('iframe');
        const keywords = /(player|video|youtube)/i;
        
        iframes.forEach(iframe => {
            const src = iframe.src || '';
            if (src.includes('youtube.com') || src.includes('youtu.be')) {
                
                if (!iframe.classList.contains('songsterr-yt-hidden-iframe')) {
                    // 1. Make iframe itself invisible
                    iframe.classList.add('songsterr-yt-hidden-iframe');
                    
                    // 2. Process parent elements across multiple levels (up to 5 levels)
                    let currentParent = iframe.parentElement;
                    let level = 0;
                    
                    while (currentParent && level < 5) {
                        // Stop when reaching body or html
                        if (currentParent.tagName === 'BODY' || currentParent.tagName === 'HTML') {
                            break;
                        }

                        const className = typeof currentParent.className === 'string' ? currentParent.className : '';
                        const idName = currentParent.id || '';
                        
                        // Immediate wrappers (1-2 level parents) often have fixed size or black backgrounds, so hide unconditionally.
                        // For higher levels (3-5 levels), hide only if class name or ID contains player/video/youtube etc.
                        if (level < 2 || keywords.test(className) || keywords.test(idName)) {
                            currentParent.classList.add('songsterr-yt-hidden-wrapper');
                        }
                        
                        currentParent = currentParent.parentElement;
                        level++;
                    }
                }
            }
        });
    }

    // Function to show/hide iframes based on mode
    function updateYtVisibility() {
        if (ytAudioOnlyMode) {
            hideYouTubeIframes();
        } else {
            // Video mode: remove hiding classes
            document.querySelectorAll('.songsterr-yt-hidden-iframe').forEach(el => {
                el.classList.remove('songsterr-yt-hidden-iframe');
            });
            document.querySelectorAll('.songsterr-yt-hidden-wrapper').forEach(el => {
                el.classList.remove('songsterr-yt-hidden-wrapper');
            });
        }
        // Update button
        const btn = document.getElementById('yt-toggle-btn');
        if (btn) {
            btn.innerHTML = ytAudioOnlyMode ? '๐ŸŽต' : '๐ŸŽฌ';
            btn.title = ytAudioOnlyMode ? 'Audio-only mode (click to show video)' : 'Video visible (click for audio-only)';
            btn.classList.toggle('audio-only', ytAudioOnlyMode);
        }
    }

    // Expose toggle function globally
    window.toggleYtAudioOnly = function() {
        ytAudioOnlyMode = !ytAudioOnlyMode;
        try {
            localStorage.setItem(YT_AUDIO_KEY, ytAudioOnlyMode);
        } catch (e) {}
        updateYtVisibility();
    };

    // Initialize state
    hideYouTubeIframes();

    // Setup MutationObserver to watch for DOM changes
    const observer = new MutationObserver((mutations) => {
        let shouldCheck = false;

        for (const mutation of mutations) {
            if (mutation.addedNodes.length > 0) {
                shouldCheck = true;
                break;
            }
            if (mutation.type === 'attributes' && mutation.target.tagName === 'IFRAME') {
                shouldCheck = true;
                break;
            }
        }
        
        if (shouldCheck) {
            hideYouTubeIframes();
        }
    });

    function startObserving() {
        if (!document.body) return;
        
        hideYouTubeIframes();
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['src']
        });
        sgdLog('log', 'Songsterr YT Hider', 'Started monitoring (ใƒ‘ใƒ—ใƒชใ‚ซ็‰ˆ + toggle)');
    }

    if (document.body) {
        startObserving();
    } else {
        document.addEventListener('DOMContentLoaded', startObserving);
    }
  })();

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // AUTOSCROLL FIX (contribution ใƒ‘ใƒ—ใƒชใ‚ซ)
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  (function() {
    'use strict';

    sgdLog('log', 'Songsterr Native Restore', 'Initializing native auto-scroll recovery...');

    // ==========================================
    // 1. Disable CSS interference (restore scroll container)
    // ==========================================
    // SongsterrUltimate injects `body, html { overflow: auto !important; }` into <style>,
    // but this completely breaks Songsterr's coordinate calculations for scrolling.
    // We surgically remove only this specific harmful CSS rule (while keeping
    // other Ultimate UI fixes intact).
    function fixCSS() {
        const styles = document.querySelectorAll('style');
        styles.forEach(style => {
            const cssText = style.textContent;
            // Check if it contains the specific harmful CSS rule
            if (cssText.includes('body, html') && cssText.includes('overflow: auto !important')) {
                // Safely remove only the lines blocking auto-scroll (keep other Ultimate UI fix CSS)
                style.textContent = cssText.replace(/body,\s*html\s*\{\s*overflow:\s*auto\s*!important;\s*\}/g, '');
            }
        });
        
        // As extra safety, force return to native behavior from highest priority inline styles
        // Return to normal browser default 'visible' (or 'initial') to restore scrolling in window class
        document.body.style.setProperty('overflow', 'visible', 'important');
        document.documentElement.style.setProperty('overflow', 'visible', 'important');
    }

    // ==========================================
    // 2. Prevent React state and DOM desync
    // ==========================================
    // The Ultimate script uses setInterval(1000ms) to forcefully remove
    // the `disabled` attribute. But when React still thinks "still initializing
    // so disabled=true" and only the DOM is enabled, clicking causes a
    // deadlock (desync) where React's event handlers don't respond.
    // We isolate the Autoscroll button from Ultimate's periodic execution
    // tool and return 100% native React management.
    function protectButton() {
        // Look for native buttons with data-id containing 'Autoscroll'
        const autoscrollBtns = document.querySelectorAll('[data-id*="Autoscroll"]');
        
        autoscrollBtns.forEach(btn => {
            const origId = btn.getAttribute('data-id');
                // Rename to avoid being caught by Ultimate's monitoring selector `[data-id*="Autoscroll"]`
                // (React itself doesn't use data-id for onClick etc. operations, so functionality remains)
                if (origId && origId.includes('Autoscroll') && !origId.includes('Auto-Scroll')) {
                    const safeId = origId.replace('Autoscroll', 'Auto-Scroll'); 
                btn.setAttribute('data-id', safeId);
                sgdLog('log', 'Songsterr Native Restore', 'Protected Autoscroll button from external setInterval overwrites.');
                
                // Attribute change notification to React tree (promotes re-rendering and event listener activation)
                // Direct state manipulation is avoided; we use fake events for safe indirect stimulation
                btn.dispatchEvent(new MouseEvent('focus', { bubbles: true }));
                btn.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
                }
        });
    }

    // ==========================================
    // 3. Execution timing and continuous monitoring (MutationObserver)
    // ==========================================
    // Due to SPA configuration where DOM is dynamically rewritten, we capture
    // UI rendering timing and apply fixes.
    const observer = new MutationObserver(() => {
        let shouldFix = false;
        for (const mutation of mutations) {
            // Execute only when new nodes (buttons or style tags) are added
            if (mutation.addedNodes.length > 0) {
                shouldFix = true;
                break;
            }
        }
        
        if (shouldFix) {
            fixCSS();
            protectButton();
        }
    });

    // Start monitoring
    observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: false // Prevent unnecessary loopback by not tracking attribute changes
    });

    // Initialization timing (fail-safe to absorb UI rendering timing fluctuations)
    setTimeout(() => {
        fixCSS();
        protectButton();
    }, 500);
    setTimeout(() => {
        fixCSS();
        protectButton();
    }, 2000);
  })();

  // Remove Autoscroll from list to avoid conflicts with ใƒ‘ใƒ—ใƒชใ‚ซ's fix
  const PLUS_DATA_IDS = ['Speed', 'Loop', 'Solo', 'Print'];
  // Removes the cached Redux state to force a clean session and prevent
  // the "free" profile from being loaded from localStorage on startup.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  try { localStorage.removeItem('persist:root'); } catch (e) {}

  // Reference to the real window object (bypasses Tampermonkey's sandbox isolation)
  const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 1. "MAGIC" PLUS PROFILE
  // A random 9-digit ID is generated each session to bypass the server-
  // side daily download quota (HTTP 429 Too Many Requests).
  // The profile object mirrors exactly what Songsterr's /auth/profile
  // endpoint returns for a real Plus subscriber.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const MAGIC_ID = Math.floor(Math.random() * 900000000) + 100000000;

  const MAGIC_PROFILE = {
    id                    : MAGIC_ID,
    uid                   : MAGIC_ID,
    email                 : `plususer${MAGIC_ID}@songsterr.com`,
    name                  : 'Plus User (Unlocked)',
    plan                  : 'plus',
    hasPlus               : true,
    permissions           : [],
    subscription          : { plan: { id: 'plus' } },
    bonusPurchasedFeatures: [],
    signature             : 'patched_signature',
    hadPlusBeforeSE       : true
  };

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 2. NETWORK INTERCEPTION - The heart of the exploit!
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // We hook fetch() very early (document-start) to intercept:
  //   A. /auth/profile โ†’ return our fake Plus profile so React thinks
  //      we have an active subscription (unlocks Speed, Loop, Solo)
  //   B. /api/songs/* or /api/tab/* โ†’ cache revision data for our
  //      download system (CDN is protected now)
  //   C. sentry/logs/analytics/useraudio โ†’ silently block telemetry
  //
// IMPORTANT NOTE: We DON'T intercept /api/edits/download because our
// GP7/MIDI download system is far superior to native .gp5 export
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const fetchOriginal = targetWindow.fetch;

  // Cache to store intercepted revision data from Songsterr's API
  // This replaces the need to fetch directly from the protected CDN
  window.__SGD_REVISION_CACHE = new Map();
  
  // Cache size limit to prevent memory leaks (max 50 songs)
  const CACHE_SIZE_LIMIT = 50;
  
  // Cache management helper
  function manageCacheSize() {
    if (window.__SGD_REVISION_CACHE.size >= CACHE_SIZE_LIMIT) {
      // Remove oldest entries (first 10)
      const entries = Array.from(window.__SGD_REVISION_CACHE.entries());
      for (let i = 0; i < 10 && i < entries.length; i++) {
        window.__SGD_REVISION_CACHE.delete(entries[i][0]);
      }
      sgdLog('log', 'SGD', `Cache cleanup: removed 10 old entries, size: ${window.__SGD_REVISION_CACHE.size}`);
    }
  }

  const fetchHooked = async function (resource, options) {
    // Determine whether resource is a Request object or a plain URL string
    const isReqObj = typeof resource === 'object' && resource instanceof Request;
    const url = isReqObj ? resource.url : (resource || '');

    // --- A. PROFILE SPOOFING ---
    // Songsterr calls this endpoint to check subscription status.
    // We respond with our forged Plus profile JSON.
    if (url.includes('/auth/profile')) {
      return new Response(JSON.stringify(MAGIC_PROFILE), {
        status : 200,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // --- B. TAB DATA INTERCEPTION ---
    // Intercept Songsterr's API calls to get revision data
    // This is needed because the CDN now requires authentication
    if (url.includes('/api/songs/') || url.includes('/api/tab/') || url.includes('/api/song/')) {
      try {
        const response = await fetchOriginal(resource, options);
        // Clone the response so we can read it without consuming it
        const clonedResponse = response.clone();
        const data = await clonedResponse.json().catch(() => null);
        
        // Cache the data if it contains revision information
        if (data && (data.revisions || data.revision || data.songId || data.id)) {
          const songId = data.songId || data.id || extractSongIdFromUrl(url);
          if (songId) {
            manageCacheSize(); // Prevent memory leaks
            window.__SGD_REVISION_CACHE.set(String(songId), data);
            sgdLog('log', 'SGD', 'Cached revision data for song:', songId);
          }
        }
        return response;
      } catch (err) {
        return fetchOriginal(resource, options);
      }
    }

    // --- C. TELEMETRY BLOCKING ---
    // Silently absorb outgoing analytics and error-logging requests.
    if (url.match(/(sentry|logs|analytics|useraudio)/i)) {
      return new Response('{}', { status: 200 });
    }

    // All other requests pass through unchanged
    return fetchOriginal(resource, options);
  };

  // Helper to extract song ID from API URL
  function extractSongIdFromUrl(url) {
    const matches = url.match(/\/s(\d+)(?:\/|$)/) || url.match(/\/songs?\/(\d+)/);
    return matches ? matches[1] : null;
  }

  // Stealth mode: toString() returns the original function's source to
  // defeat any integrity checks that compare fetch.toString().
  fetchHooked.toString = () => fetchOriginal.toString();

  // Robust injection with protection against replacement
  try {
    Object.defineProperty(targetWindow, 'fetch', {
      value      : fetchHooked,
      writable   : false,  // Prevent replacement
      configurable: false  // Prevent redefinition
    });
    sgdLog('log', 'SGD', 'โœ… Fetch hook installed with protection');
  } catch (e) {
    sgdLog('error', 'SGD', 'Failed to install protected fetch hook:', e);
    targetWindow.fetch = fetchHooked; // Fallback for older browsers
  }

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 3. DOM STATE INJECTION
  // Songsterr stores its full Redux store as JSON inside
  // <script id="state"> on every page. React reads this element during
  // hydration to populate its initial state. We watch for the element
  // with a MutationObserver and patch it before React reads it,
  // injecting hasPlus:true and our fake profile so the app believes
  // the user is subscribed from the very first render.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const stateObserver = new MutationObserver(() => {
    const el = document.getElementById('state');
    if (!el) return;
    try {
      const text = el.textContent.trim();
      if (!text) return;
      const data = JSON.parse(text);

      if (!data.user) data.user = {};
      data.user.hasPlus    = true;
      data.user.isLoggedIn = true;
      data.user.profile    = MAGIC_PROFILE;
      // Suppress the GDPR/CCPA consent banner
      data.consent = { loading: false, suite: 'tcf', view: 'none' };

      const patched = JSON.stringify(data);
      if (el.textContent !== patched) el.textContent = patched;
    } catch (e) {
      sgdLog('warn', 'SGD', 'Failed to parse state JSON:', e.message);
    }
  });
  stateObserver.observe(document.documentElement, { childList: true, subtree: true });

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 3b. SHOWROOM FIX โ€” Force transition to tab view when arriving via direct link
  // When arriving directly via a link, the showroom is active (data-has-showroom="yes")
  // and the player isn't initialized. We need to force the transition to tab-only view.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const showroomFixObserver = new MutationObserver(() => {
    const apptab = document.getElementById('apptab');
    if (!apptab) return;
    
    // Check if we're in showroom mode (arrived via direct link)
    if (apptab.getAttribute('data-has-showroom') === 'yes') {
      const showroom = document.getElementById('showroom');
      const tablature = document.getElementById('tablature');
      
      if (showroom && tablature) {
        // Move tablature out of showroom to preserve it
        if (tablature.parentElement === showroom) {
          apptab.insertBefore(tablature, showroom);
          sgdLog('log', 'Showroom Fix', 'Moved tablature out of showroom for proper rendering');
        }
        
        // Hide the showroom
        showroom.style.display = 'none';
        showroom.style.visibility = 'hidden';
        
        // Update the attribute to prevent re-processing
        apptab.setAttribute('data-has-showroom', 'fixed');
        
        sgdLog('log', 'Showroom Fix', 'Forced transition to tab-only view');
      }
    }
  });
  showroomFixObserver.observe(document.documentElement, { childList: true, subtree: true });

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 4. CSS โ€” UI CLEANUP + BUTTON STYLES
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  GM_addStyle(`
    /* โ”€โ”€ Hide unwanted elements โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
    section[data-consent="summary"],
    div[class*="Consent"],
    #onetrust-banner-sdk,
    [id*="ad-"],
    [class*="ad-"],
    div[id^="div-gpt-ad"],
    div[class*="Error"]
    { display: none !important; visibility: hidden !important; }

    /* โ”€โ”€ IMPORTANT: Do NOT override body/html overflow - handled by Autoscroll Fix โ”€โ”€ */
    /* NOTE: Commentรฉ pour ne pas interfรฉrer avec le showroom */
    /* #apptab    { opacity: 1 !important; visibility: visible !important; } */

    /* โ”€โ”€ Our button wrapper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
    /* Inherits B3a4pa / B3agq5 classes from the replaced #c-export div,
       so vertical alignment inside the flex toolbar is automatic.      */
    #sgd-wrapper {
      display      : inline-flex;
      align-items  : center;
      gap          : 12px;
    }

    /* โ”€โ”€ GP7 & MIDI buttons โ€” styled to match Songsterr native dark UI โ”€โ”€โ”€โ”€โ”€ */
    .sgd-btn {
      display         : inline-flex;
      align-items     : center;
      justify-content : center;
      gap             : 6px;
      padding         : 0 12px;
      height          : 36px;
      border          : 1px solid #3a3a3a;
      border-radius   : 6px;
      font-size       : 13px;
      font-weight     : 500;
      cursor          : pointer;
      white-space     : nowrap;
      transition      : all 0.15s ease;
      font-family     : -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      line-height     : 1;
      letter-spacing  : -0.01em;
      background      : #2a2a2a;
      color           : #e5e5e5;
    }
    .sgd-btn:hover {
      background : #3a3a3a;
      border-color: #4a4a4a;
    }
    .sgd-btn:active {
      background : #1a1a1a;
    }
    .sgd-btn:disabled {
      opacity    : 0.4;
      cursor     : not-allowed;
    }
    /* Primary action buttons (GP7) - blue accent matching Songsterr */
    .sgd-btn-gp {
      background : #2563eb;
      color      : #fff;
      border-color: #3b82f6;
    }
    .sgd-btn-gp:hover {
      background : #1d4ed8;
      border-color: #2563eb;
    }
    /* Secondary action buttons (MIDI) - neutral dark */
    .sgd-btn-midi {
      background : #404040;
      color      : #e5e5e5;
      border-color: #525252;
    }
    .sgd-btn-midi:hover {
      background : #525252;
      border-color: #626262;
    }

    /* โ”€โ”€ YouTube toggle button โ€” styled to match Songsterr dark UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
    #yt-toggle-btn {
      display         : inline-flex;
      align-items     : center;
      justify-content : center;
      width           : 36px;
      height          : 36px;
      border-radius   : 6px;
      border          : 1px solid #3a3a3a;
      background      : #2a2a2a;
      color           : #a5a5a5;
      cursor          : pointer;
      font-size       : 16px;
      transition      : all 0.15s ease;
    }
    #yt-toggle-btn:hover {
      background : #3a3a3a;
      color      : #e5e5e5;
    }
    /* Audio-only mode active - subtle green accent */
    #yt-toggle-btn.audio-only {
      background : #16a34a;
      color      : #fff;
      border-color: #22c55e;
    }
    #yt-toggle-btn.audio-only:hover {
      background : #15803d;
      border-color: #16a34a;
    }

    /* โ”€โ”€ Logging toggle button โ€” styled to match Songsterr dark UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
    #sgd-log-toggle {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      padding: 8px 16px;
      border: 1px solid #3a3a3a;
      border-radius: 6px;
      background: #2a2a2a;
      color: #e5e5e5;
      font-size: 14px;
      font-weight: 500;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      cursor: pointer;
      transition: all 0.15s ease;
    }
    #sgd-log-toggle:hover {
      background: #3a3a3a;
      border-color: #4a4a4a;
    }
    #sgd-log-toggle.active {
      background: #166534;
      border-color: #16a34a;
      color: #fff;
    }
    #sgd-log-toggle:not(.active) {
      background: #7f1d1d;
      border-color: #dc2626;
      color: #fff;
    }

    /* โ”€โ”€ Status toast โ€” centered at the bottom of the viewport โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
    #sgd-status {
      bottom       : 20px;
      left         : 50%;
      transform    : translateX(-50%);
      background   : rgba(15,23,42,.90);
      color        : #e2e8f0;
      font-size    : 12px;
      font-weight  : 500;
      padding      : 6px 16px;
      border-radius: 20px;
      z-index      : 99999;
      pointer-events: none;
      opacity      : 0;
      transition   : opacity .25s;
      font-family  : -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      white-space  : nowrap;
    }
    #sgd-status.visible { opacity: 1; }
    #sgd-status.ok  { color: #86efac; }
    #sgd-status.err { color: #fca5a5; }
  `);

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 5. TARGETED PLUS FEATURE UNLOCK
  //
  // โš ๏ธ CRITICAL PITFALL: We must NOT remove `disabled` from ALL buttons
  // on the page. Songsterr's tab player legitimately uses `disabled`
  // during its initialization phase (audio loading, tab parsing, etc.).
  // Force-enabling those buttons desynchronizes React's internal state
  // from the DOM โ†’ the tab freezes on first load.
  //
  // Strategy: target ONLY buttons locked by the Plus paywall, which are
  // identifiable by one of these three signals:
  //   1. They contain a lock SVG icon  (use[href*="lock"])
  //   2. Their data-id matches a known Plus feature name
  //   3. They carry the Songsterr lock CSS class (Cny223)
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

  // NOTE: PLUS_DATA_IDS already declared above (Autoscroll removed - handled separately)

  setInterval(() => {
    // โ”€โ”€ 1. Force print mode to "Plus" โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    const printEl = document.querySelector('[data-id^="Print--"]');
    if (printEl) printEl.setAttribute('data-id', 'Print--plus');

    // โ”€โ”€ 2. Remove lock SVG icons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // React adds <use href*="lock"> inside Plus-gated buttons.
    // We remove the icon and re-enable only its direct button parent.
    document.querySelectorAll('svg use[href*="lock"]').forEach(use => {
      const svg    = use.closest('svg');
      const parent = svg?.closest('button');
      if (svg)    svg.remove();
      if (parent) {
        parent.removeAttribute('disabled');
        parent.classList.remove('Cny223');
        parent.style.pointerEvents = 'auto';
      }
    });

    // โ”€โ”€ 3. Unlock Plus buttons by data-id โ”€โ”€
    PLUS_DATA_IDS.forEach(id => {
      const el = document.querySelector(`[data-id*="${id}"]`);
      if (el && el.hasAttribute('disabled')) {
        el.removeAttribute('disabled');
        el.classList.remove('Cny223');
        el.style.pointerEvents = 'auto';
      }
    });

    // โ”€โ”€ 4. Handle Autoscroll button (protected by ใƒ‘ใƒ—ใƒชใ‚ซ, data-id renamed to Auto-Scroll) โ”€โ”€
    const autoscrollEl = document.querySelector('[data-id*="Auto-Scroll"]');
    if (autoscrollEl && autoscrollEl.hasAttribute('disabled')) {
      autoscrollEl.removeAttribute('disabled');
      autoscrollEl.classList.remove('Cny223');
      autoscrollEl.style.pointerEvents = 'auto';
    }

    // โ”€โ”€ 5. Unlock any remaining buttons with Songsterr's lock class โ”€โ”€
    document.querySelectorAll('button.Cny223').forEach(btn => {
      btn.removeAttribute('disabled');
      btn.classList.remove('Cny223');
      btn.style.pointerEvents = 'auto';
    });
  }, 1000);

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 6. CONSOLE FILTER
  // Suppress noisy, irrelevant errors that would pollute the console.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  const consoleErrorOrig = console.error;
  const CONSOLE_FILTERS  = ['AudioContext', 'source-map', 'unreachable', 'buffer', 'Secure-YEC', 'Aborted', '401'];
  console.error = function (...args) {
    const message = String(args[0] || '');
    // More selective filtering - only filter known benign errors
    if (CONSOLE_FILTERS.some(f => message.includes(f)) && !message.includes('Songsterr') && !message.includes('SGD')) {
      return;
    }
    consoleErrorOrig.apply(console, args);
  };

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // โ–ผโ–ผโ–ผ  GP7/MIDI DOWNLOAD SYSTEM  โ–ผโ–ผโ–ผ
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  //
  // This system is inspired by the brilliant project from Metaphysics0:
  // https://github.com/Metaphysics0/songsterr-downloader
  //
  // His genius idea: Songsterr stores tab data on protected CloudFront CDNs.
  // But the web app needs to fetch it somehow! We intercept those legitimate
  // calls to get raw data, then use alphaTab to convert to standard formats
  // (GP7/MIDI).
  //
  // The CloudFront servers: dqsljvtekg760.cloudfront.net and d3d3l6a6rcgkaf.cloudfront.net
  // are Songsterr's real servers, we just pose as Chrome.

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CDN HEADERS - Posing as legitimate Chrome
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // Songsterr stores tab data on CloudFront CDN which validates Origin
  // and Referer headers. We spoof a Chrome signature so the CDN accepts
  // our requests. GM_xmlhttpRequest is required because browser fetch()
  // would block these cross-origin requests.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const CDN_BASE = 'https://dqsljvtekg760.cloudfront.net';
  const CDN_BASE_2 = 'https://d3d3l6a6rcgkaf.cloudfront.net'; // Fallback CDN

  const CDN_HEADERS = {
    'User-Agent'        : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
    'Accept'            : 'application/json, text/plain, */*',
    'Accept-Language'   : 'en-US,en;q=0.9',
    'Accept-Encoding'   : 'gzip, deflate, br',
    'sec-ch-ua'         : '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
    'sec-ch-ua-mobile'  : '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-site'    : 'same-site',
    'sec-fetch-mode'    : 'cors',
    'sec-fetch-dest'    : 'empty',
    'Referer'           : 'https://www.songsterr.com/',
    'Origin'            : 'https://www.songsterr.com',
    'Connection'        : 'keep-alive',
    'Cache-Control'     : 'no-cache',
    'Pragma'            : 'no-cache'
  };

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // API METADATA FETCH โ€” Direct API call to get fresh song metadata
  // When the DOM state is stale, we fetch directly from Songsterr's API
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  async function fetchMetaFromAPI(songId) {
    const url = `https://www.songsterr.com/api/meta/${songId}?allowOwnUnpublished=true`;
    sgdLog('log', 'SGD Debug', `Fetching fresh metadata from API: ${url}`);
    
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url: url,
        headers: {
          'Accept': 'application/json',
          'Referer': 'https://www.songsterr.com/',
          'Origin': 'https://www.songsterr.com'
        },
        responseType: 'json',
        onload: (res) => {
          if (res.status >= 200 && res.status < 300) {
            sgdLog('log', 'SGD Debug', 'โœ… API metadata fetched successfully');
            resolve(res.response);
          } else {
            reject(new Error(`API returned ${res.status}`));
          }
        },
        onerror: (err) => reject(new Error(`Network error: ${err}`))
      });
    });
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // STEP 1 โ€” READ PAGE METADATA (with API fallback)
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // Songsterr embeds all song metadata in <script id="state">
  // But after SPA navigation, React takes time to update it.
  // So we first check the DOM, and if stale, fetch fresh data from API.
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  async function getStateFromPage() {
    sgdLog('log', 'SGD Debug', 'Reading page state...');
    sgdLog('log', 'SGD Debug', 'Current URL:', location.href);
    sgdLog('log', 'SGD Debug', 'Current path:', location.pathname);
    
    // Extract songId from URL
    const urlMatch = location.pathname.match(/-s(\d+)$/);
    const urlSongId = urlMatch ? urlMatch[1] : null;
    sgdLog('log', 'SGD Debug', 'SongId from URL:', urlSongId);
    
    if (!urlSongId) {
      throw new Error('Could not extract songId from URL');
    }
    
    // First try: check DOM state quickly
    const el = document.getElementById('state');
    if (el) {
      try {
        const parsed = JSON.parse(el.textContent || el.innerText);
        const cur = parsed?.meta?.current;
        
        if (cur?.songId && String(cur.songId) === String(urlSongId) && cur?.revisionId && cur?.image) {
          sgdLog('log', 'SGD Debug', 'โœ… Using fresh DOM state');
          return extractResult(cur);
        }
      } catch (e) {
        sgdLog('warn', 'SGD', 'Failed to parse DOM state:', e.message);
      }
    }
    
    // Second try: fetch fresh metadata from API
    sgdLog('log', 'SGD Debug', 'DOM state stale, fetching from API...');
    showStatus('โณ Fetching fresh song data...', '', 0);
    
    try {
      const apiData = await fetchMetaFromAPI(urlSongId);
      
      // API returns data directly at root level, not under 'current' property
      if (!apiData?.revisionId || !apiData?.songId) {
        throw new Error('API response missing required fields');
      }
      
      const cur = apiData;
      sgdLog('log', 'SGD Debug', 'โœ… API data received:', {
        songId: urlSongId,
        title: cur.title,
        artist: cur.artist,
        revisionId: cur.revisionId
      });
      
      // Build result from API data - API returns flat structure
      const result = {
        songId    : parseInt(urlSongId),
        revisionId: cur.revisionId,
        image     : cur.image || cur.imageId || '', // image might be named differently
        title     : cur.title  || 'Song',
        artist    : cur.artist || 'Unknown Artist',
        tracks    : Array.isArray(cur.tracks) ? cur.tracks : []
      };
      
      // Also cache the revision data for later use
      if (window.__SGD_REVISION_CACHE && cur.revisions) {
        manageCacheSize(); // Prevent memory leaks
        window.__SGD_REVISION_CACHE.set(String(urlSongId), cur);
        sgdLog('log', 'SGD Debug', 'Cached revision data for song:', urlSongId);
      }
      
      return result;
      
    } catch (apiErr) {
      sgdLog('error', 'SGD Debug', 'API fetch failed:', apiErr.message);
      throw new Error(`Failed to get song data: ${apiErr.message}. Please refresh the page.`);
    }
  }
  
  function extractResult(cur) {
    const result = {
      songId    : cur.songId,
      revisionId: cur.revisionId,
      image     : cur.image,
      title     : cur.title  || 'Song',
      artist    : cur.artist || 'Unknown Artist',
      tracks    : Array.isArray(cur.tracks) ? cur.tracks : []
    };
    
    sgdLog('log', 'SGD Debug', 'โœ… State validated and extracted:', {
      songId: result.songId,
      revisionId: result.revisionId,
      title: result.title,
      artist: result.artist,
      trackCount: result.tracks.length,
      image: result.image
    });
    sgdLog('log', 'SGD Debug', 'Tracks:', result.tracks.map(t => ({ partId: t.partId, title: t.title, instrumentId: t.instrumentId })));
    
    return result;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // ร‰TAPE 2 โ€” Rร‰CUPร‰RATION DES JSON DE Rร‰VISIONS DEPUIS LE CDN (avec fallback)
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // Les donnรฉes de chaque piste sont stockรฉes dans des fichiers JSON sรฉparรฉs sur le CDN :
  //   URL pattern: {CDN_BASE}/{songId}/{revisionId}/{image}/{partId}.json
  // Toutes les pistes sont rรฉcupรฉrรฉes en parallรจle via Promise.all.
  // GM_xmlhttpRequest est utilisรฉ pour contourner les restrictions CORS du navigateur.
  // 
  // STRATร‰GIE DE FALLBACK (inspirรฉe de songsterr-downloader):
  // 1. Essayer le CDN primaire (dqsljvtekg760.cloudfront.net)
  // 2. Si รฉchec, essayer le CDN alternatif (d3d3l6a6rcgkaf.cloudfront.net)
  // 3. Si les deux รฉchouent, essayer les donnรฉes API interceptรฉes des appels Songsterr
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function fetchRevisionJson(url) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method      : 'GET',
        url,
        headers     : CDN_HEADERS,
        responseType: 'json',
        onload : res => {
          if (res.status >= 200 && res.status < 300) resolve(res.response);
          else reject(new Error(`HTTP ${res.status} โ€” ${url}`));
        },
        onerror: err => reject(new Error(`Network error: ${JSON.stringify(err)}`))
      });
    });
  }

  // Build revision URL with given CDN base
  function buildRevisionUrl(songId, revisionId, image, partId, cdnBase = CDN_BASE) {
    return `${cdnBase}/${songId}/${revisionId}/${image}/${partId}.json`;
  }

  // Fetch with fallback to alternate CDN
  async function fetchRevisionWithFallback(songId, revisionId, image, partId) {
    const primaryUrl = buildRevisionUrl(songId, revisionId, image, partId, CDN_BASE);
    const fallbackUrl = buildRevisionUrl(songId, revisionId, image, partId, CDN_BASE_2);
    
    // Try primary CDN first
    try {
      const response = await fetchRevisionJson(primaryUrl);
      return response;
    } catch (primaryError) {
      sgdLog('warn', 'SGD', `Primary CDN failed for part ${partId}, trying fallback...`);
    }
    
    // Try fallback CDN
    try {
      const response = await fetchRevisionJson(fallbackUrl);
      sgdLog('log', 'SGD', `Fallback CDN succeeded for part ${partId}`);
      return response;
    } catch (fallbackError) {
      throw new Error(`Both CDNs failed for part ${partId}`);
    }
  }

  // Extract revision data from intercepted API response (with debug)
  function getRevisionFromCache(songId, revisionId, partId) {
    const cache = window.__SGD_REVISION_CACHE;
    sgdLog('log', 'SGD Debug', `Cache lookup for songId=${songId}, revisionId=${revisionId}, partId=${partId}`);
    sgdLog('log', 'SGD Debug', `Cache exists: ${!!cache}, Cache size: ${cache ? cache.size : 0}`);
    
    if (!cache || cache.size === 0) {
      sgdLog('log', 'SGD Debug', 'Cache empty, skipping');
      return null;
    }
    
    const cachedData = cache.get(String(songId));
    if (!cachedData) {
      sgdLog('log', 'SGD Debug', `No cached data for songId=${songId}. Available keys:`, Array.from(cache.keys()));
      return null;
    }
    
    sgdLog('log', 'SGD Debug', `Found cached data for songId=${songId}, keys:`, Object.keys(cachedData));
    
    // Try to find the revision data in various formats
    if (cachedData.revisions && Array.isArray(cachedData.revisions)) {
      sgdLog('log', 'SGD Debug', `Looking in cachedData.revisions (${cachedData.revisions.length} items)`);
      const revision = cachedData.revisions.find(r => 
        String(r.revisionId || r.id) === String(revisionId)
      );
      if (revision) {
        sgdLog('log', 'SGD Debug', 'Found revision in cachedData.revisions');
        return revision;
      }
    }
    
    if (cachedData.revision) {
      sgdLog('log', 'SGD Debug', 'Found revision in cachedData.revision');
      return cachedData.revision;
    }
    
    // Try to find by partId in tracks
    if (cachedData.tracks && Array.isArray(cachedData.tracks)) {
      sgdLog('log', 'SGD Debug', `Looking in cachedData.tracks (${cachedData.tracks.length} items)`);
      const track = cachedData.tracks.find(t => t.partId === partId);
      if (track && track.revision) {
        sgdLog('log', 'SGD Debug', `Found revision in track.partId=${partId}`);
        return track.revision;
      }
    }
    
    sgdLog('log', 'SGD Debug', `No revision found in cache for partId=${partId}`);
    return null;
  }

  async function fetchAllRevisions(meta) {
    const { songId, revisionId, image, tracks } = meta;
    sgdLog('log', 'SGD Debug', `fetchAllRevisions called: songId=${songId}, revisionId=${revisionId}, image=${image}`);
    sgdLog('log', 'SGD Debug', `Total tracks in meta: ${tracks.length}`);
    
    // Debug: show first track structure
    if (tracks.length > 0) {
      sgdLog('log', 'SGD Debug', 'First track keys:', Object.keys(tracks[0]));
      sgdLog('log', 'SGD Debug', 'First track:', JSON.stringify(tracks[0], null, 2).substring(0, 300));
    }
    
    // API tracks don't have partId, use array index instead
    const validTracks = tracks
      .map((t, idx) => ({ ...t, partId: t.partId ?? idx })) // Add partId from index if missing
      .filter(t => typeof t.partId === 'number')
      .sort((a, b) => a.partId - b.partId);

    sgdLog('log', 'SGD Debug', `Valid tracks (with partId): ${validTracks.length}`);
    validTracks.forEach(t => sgdLog('log', 'SGD Debug', `Track: partId=${t.partId}, title=${t.title}`));

    if (validTracks.length === 0) throw new Error('No valid tracks found in page metadata.');

    // Check if we have intercepted API data
    const cacheAvailable = window.__SGD_REVISION_CACHE && window.__SGD_REVISION_CACHE.size > 0;
    sgdLog('log', 'SGD Debug', `API cache available: ${cacheAvailable}`);
    
    let cacheHits = 0;
    let cdnHits = 0;
    let failures = 0;
    
    const results = await Promise.all(
      validTracks.map(async track => {
        sgdLog('log', 'SGD Debug', `Processing track partId=${track.partId}`);
        
        // First: try intercepted API cache (most reliable)
        if (cacheAvailable) {
          const cachedRevision = getRevisionFromCache(songId, revisionId, track.partId);
          if (cachedRevision) {
            cacheHits++;
            sgdLog('log', 'SGD Debug', `โœ… CACHE HIT for partId=${track.partId}`);
            return { trackMeta: track, revision: cachedRevision };
          }
        }
        
        // Second: try CDN with fallback
        sgdLog('log', 'SGD Debug', `Cache miss, trying CDN for partId=${track.partId}`);
        try {
          const revision = await fetchRevisionWithFallback(songId, revisionId, image, track.partId);
          cdnHits++;
          sgdLog('log', 'SGD Debug', `โœ… CDN SUCCESS for partId=${track.partId}`);
          return { trackMeta: track, revision };
        } catch (err) {
          failures++;
          sgdLog('warn', 'SGD Debug', `โŒ FAILED for partId=${track.partId}:`, err.message);
          return null;
        }
      })
    );

    const revisions = results.filter(Boolean);
    sgdLog('log', 'SGD Debug', `Fetch complete: ${revisions.length}/${validTracks.length} tracks`);
    sgdLog('log', 'SGD Debug', `Stats: cacheHits=${cacheHits}, cdnHits=${cdnHits}, failures=${failures}`);
    
    if (revisions.length === 0) {
      throw new Error('Could not fetch any track data. ' +
        'Both CDNs returned errors and no API data was intercepted. ' +
        'Try refreshing the page and playing the tab to populate the cache.');
    }
    return revisions;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Songsterr duration [num, den] โ†’ alphaTab Duration + dots
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // Songsterr encode les durรฉes en fraction [numรฉrateur, dรฉnominateur].
  // alphaTab utilise une enum (Whole=1, Half=2, Quarter=4โ€ฆ) plus un compteur de points.
  // On trouve la meilleure correspondance en minimisant le delta sur toutes les
  // durรฉes de base combinรฉes avec 0, 1, ou 2 points d'augmentation.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function mapDuration(dur) {
    const D     = alphaTab.model.Duration;
    const bases = [D.Whole, D.Half, D.Quarter, D.Eighth, D.Sixteenth, D.ThirtySecond, D.SixtyFourth];

    if (!dur?.[0] || !dur?.[1]) return { duration: D.Quarter, dots: 0 };

    const target = dur[0] / dur[1];
    let best = { duration: D.Quarter, dots: 0 };
    let bestDelta = Infinity;

    for (const base of bases) {
      const bv = 1 / Number(base);
      for (const dots of [0, 1, 2]) {
        const dv    = bv + (dots >= 1 ? bv / 2 : 0) + (dots >= 2 ? bv / 4 : 0);
        const delta = Math.abs(dv - target);
        if (delta < bestDelta) { bestDelta = delta; best = { duration: base, dots }; }
      }
    }
    return best;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Tuplet integer โ†’ [numerator, denominator]
  // Examples: triplet 3 โ†’ [3,2], quintuplet 5 โ†’ [5,4], septuplet 7 โ†’ [7,4]
  // For unlisted values, the denominator is the nearest lower power of 2.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function getTupletRatio(t) {
    const map = { 3:[3,2], 5:[5,4], 6:[6,4], 7:[7,4], 9:[9,8], 10:[10,8], 12:[12,8] };
    if (map[t]) return map[t];
    if (t > 1) { const d = Math.pow(2, Math.floor(Math.log2(t))); return [t, d]; }
    return [1, 1];
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Songsterr instrument ID โ†’ MIDI program + flags
  // Instrument ID 1024 is Songsterr's code for drums/percussion.
  // Percussion must be routed to MIDI channel 9 (General MIDI standard).
  // All other IDs map directly to GM program numbers (clamped 0โ€“127).
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function mapInstrument(id) {
    if (id === 1024) return { program: 0, isPercussion: true };
    const prog = typeof id === 'number' ? Math.min(Math.max(id, 0), 127) : 24;
    return { program: prog, isPercussion: false };
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Percussion articulation index
  // alphaTab assigns its own internal index to each percussion
  // articulation. To get a stable mapping that survives version changes,
  // we perform a GP7 round-trip: export a minimal percussion score then
  // re-import it and read back the articulation array order.
  // The resulting Map (MIDI note โ†’ index) is built once and cached.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  let _percMap = null;

  function buildPercMap() {
    // Build a minimal score with one empty percussion track
    const score = new alphaTab.model.Score();
    const mb    = new alphaTab.model.MasterBar();
    score.addMasterBar(mb);
    const track = new alphaTab.model.Track();
    track.playbackInfo.primaryChannel   = 9;
    track.playbackInfo.secondaryChannel = 9;
    const staff   = new alphaTab.model.Staff();
    staff.isPercussion = true;
    track.addStaff(staff);
    const bar   = new alphaTab.model.Bar();
    const voice = new alphaTab.model.Voice();
    const beat  = new alphaTab.model.Beat();
    beat.isEmpty = true;
    voice.addBeat(beat); bar.addVoice(voice); staff.addBar(bar);
    score.addTrack(track);

    // Export then re-import to read the articulation index order
    const settings  = new alphaTab.Settings();
    score.finish(settings);
    const data       = new alphaTab.exporter.Gp7Exporter().export(score, settings);
    const reimported = alphaTab.importer.ScoreLoader.loadScoreFromBytes(data, settings);

    const map = new Map();
    reimported.tracks[0].percussionArticulations.forEach((a, i) => {
      if (!map.has(a.id)) map.set(a.id, i);
    });
    return map;
  }

  function getPercIndex(midiNote) {
    if (!_percMap) _percMap = buildPercMap();
    return _percMap.get(midiNote) ?? midiNote;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // LOOKUP TABLES
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const VELOCITY_MAP = {
    ppp: alphaTab.model.DynamicValue.PPP,
    pp : alphaTab.model.DynamicValue.PP,
    p  : alphaTab.model.DynamicValue.P,
    mp : alphaTab.model.DynamicValue.MP,
    mf : alphaTab.model.DynamicValue.MF,
    f  : alphaTab.model.DynamicValue.F,
    ff : alphaTab.model.DynamicValue.FF,
    fff: alphaTab.model.DynamicValue.FFF
  };

  const HARMONIC_MAP = {
    natural   : alphaTab.model.HarmonicType.Natural,
    artificial: alphaTab.model.HarmonicType.Artificial,
    pinch     : alphaTab.model.HarmonicType.Pinch,
    tap       : alphaTab.model.HarmonicType.Tap,
    semi      : alphaTab.model.HarmonicType.Semi,
    feedback  : alphaTab.model.HarmonicType.Feedback
  };

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Build alphaTab MasterBars (global timeline)
  // MasterBars hold the data shared across all tracks: time signatures,
  // section markers, repeat brackets, and tempo automations (BPM).
  // The track with the most measures is used as the master reference.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function buildMasterBars(score, masterRev, count) {
    let sigNum = 4, sigDen = 4;

    for (let i = 0; i < count; i++) {
      const m  = masterRev?.measures?.[i];
      const s  = m?.signature;

      // Update time signature when a new one is present and valid
      if (Array.isArray(s) && s.length === 2 && s[0] && s[1]) [sigNum, sigDen] = s;

      const mb = new alphaTab.model.MasterBar();
      mb.timeSignatureNumerator   = sigNum;
      mb.timeSignatureDenominator = sigDen;

      // Section marker (e.g. "Verse", "Chorus", "Bridge")
      if (m?.marker) {
        const text = typeof m.marker === 'string' ? m.marker : (m.marker?.text || '');
        const sec  = new alphaTab.model.Section();
        sec.marker = sec.text = text;
        mb.section = sec;
      }

      if (m?.repeatStart)                                                 mb.isRepeatStart    = true;
      if (typeof m?.repeatCount    === 'number' && m.repeatCount > 0)    mb.repeatCount      = m.repeatCount;
      if (typeof m?.alternateEnding === 'number' && m.alternateEnding > 0) mb.alternateEndings = m.alternateEnding;

      score.addMasterBar(mb);
    }

    // Tempo automations โ€” always referenced against a quarter note (index 2)
    const tempo = masterRev?.automations?.tempo;
    if (Array.isArray(tempo)) {
      for (const pt of tempo) {
        const mb = score.masterBars[pt.measure];
        if (!mb) continue;
        const ratio = pt.position > 0 ? Math.max(0, Math.min(1, pt.position / (pt.type || 4))) : 0;
        mb.tempoAutomations.push(
          alphaTab.model.Automation.buildTempoAutomation(false, ratio, pt.bpm, 2, true)
        );
      }
    }
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // CONVERSION โ€” Songsterr Note โ†’ alphaTab Note
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // Two CRUCIAL coordinate differences:
  //   โ˜… STRING INDEX: Songsterr string 0 = highest-pitched string.
  //     alphaTab string 1 = lowest-pitched string.
  //     Formula: alphaTab.string = numStrings - songsterr.string
  //
  //   โ˜… BEND SCALE: Songsterr encodes bend points in hundredths of a
  //     semitone. alphaTab uses quarter-tones.
  //     Formula: alphaTab.tone = songsterr.tone ร— 2
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function mapNote(nd, isPerc, numStrings) {
    const note  = new alphaTab.model.Note();
    note.string = isPerc ? 0 : numStrings - (nd.string ?? 0);
    note.fret   = nd.fret ?? 0;

    // Percussion notes use an articulation index instead of string/fret
    if (isPerc) note.percussionArticulation = getPercIndex(nd.fret ?? 0);

    if (nd.tie)         note.isTieDestination   = true;
    if (nd.dead)        note.isDead             = true;
    if (nd.ghost)       note.isGhost            = true;
    if (nd.hp)          note.isHammerPullOrigin = true;
    if (nd.staccato)    note.isStaccato         = true;
    if (nd.accentuated) note.accentuated        = alphaTab.model.AccentuationType.Heavy;

    if (nd.wideVibrato)  note.vibrato = alphaTab.model.VibratoType.Wide;
    else if (nd.vibrato) note.vibrato = alphaTab.model.VibratoType.Slight;

    // Harmonic type
    if (nd.harmonic) {
      const ht = HARMONIC_MAP[nd.harmonic.toLowerCase()];
      if (typeof ht === 'number') {
        note.harmonicType = ht;
        if (typeof nd.harmonicFret === 'number') note.harmonicValue = nd.harmonicFret;
      }
    }

    // Slide type mapping
    if (nd.slide) {
      const s = nd.slide.toLowerCase();
      const Out = alphaTab.model.SlideOutType, In = alphaTab.model.SlideInType;
      if      (s === 'shift')                             note.slideOutType = Out.Shift;
      else if (s === 'legato')                            note.slideOutType = Out.Legato;
      else if (s === 'into_from_below' || s === 'below') note.slideInType  = In.IntoFromBelow;
      else if (s === 'into_from_above')                  note.slideInType  = In.IntoFromAbove;
      else if (s === 'out_up')                           note.slideOutType = Out.OutUp;
      else if (s === 'out_down' || s === 'downwards')    note.slideOutType = Out.OutDown;
    }

    // Bend โ€” โ˜… multiply by 2: Songsterr hundredths โ†’ alphaTab quarter-tones
    if (nd.bend?.points?.length > 0) {
      note.bendType = alphaTab.model.BendType.Custom;
      for (const pt of nd.bend.points) {
        note.addBendPoint(new alphaTab.model.BendPoint(
          Math.round(pt.position),
          Math.round(pt.tone * 2)  // โ˜… scale factor ร—2
        ));
      }
    }

    return note;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Songsterr Beat โ†’ alphaTab Beat
  // Handles: durations, dots, tuplets, dynamics, pick stroke,
  // beat-level vibrato, and palm mute.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function mapBeat(bd, masterBar, isPerc, numStrings) {
    const beat = new alphaTab.model.Beat();
    if (bd.rest) beat.isEmpty = true;

    const dur    = mapDuration(bd.duration);
    beat.duration = dur.duration;
    beat.dots     = bd.dots ?? dur.dots;

    if (bd.text) beat.text = bd.text;

    // Tuplet: recompute base duration from the `type` denominator field
    if (typeof bd.tuplet === 'number' && bd.tuplet > 1) {
      const [n, d]    = getTupletRatio(bd.tuplet);
      beat.tupletNumerator   = n;
      beat.tupletDenominator = d;
      if (typeof bd.type === 'number' && bd.type > 0) {
        beat.duration = mapDuration([1, bd.type]).duration;
        beat.dots     = bd.dots ?? 0;
      }
    }

    // Dynamic (velocity) level
    if (typeof bd.velocity === 'string') {
      const dyn = VELOCITY_MAP[bd.velocity.toLowerCase()];
      if (typeof dyn === 'number') beat.dynamics = dyn;
    }

    // Pick stroke direction
    if (typeof bd.pickStroke === 'string') {
      const ps = bd.pickStroke.toLowerCase();
      if (ps === 'down') beat.pickStroke = alphaTab.model.PickStroke.Down;
      else if (ps === 'up') beat.pickStroke = alphaTab.model.PickStroke.Up;
    }

    // Beat-level vibrato
    if (bd.wideVibrato || bd.vibratoWithTremoloBar) beat.vibrato = alphaTab.model.VibratoType.Wide;
    else if (bd.vibrato)                            beat.vibrato = alphaTab.model.VibratoType.Slight;

    if (bd.palmMute) beat.isPalmMute = true;

    // Add all notes to this beat
    for (const nd of (bd.notes || [])) {
      if (!nd.rest) beat.addNote(mapNote(nd, isPerc, numStrings));
    }

    return beat;
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Fill an empty voice with rest beats
  // Used when a measure has no beat data (full-measure rest).
  // One rest beat is added per beat of the time signature numerator.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function fillWithRests(voice, masterBar) {
    const num = masterBar.timeSignatureNumerator   || 4;
    const den = masterBar.timeSignatureDenominator || 4;
    const dur = mapDuration([1, den]);
    for (let i = 0; i < num; i++) {
      const rest = new alphaTab.model.Beat();
      rest.isEmpty  = true;
      rest.duration = dur.duration;
      rest.dots     = dur.dots;
      voice.addBeat(rest);
    }
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // CONVERSION โ€” Build a complete alphaTab Track
  // Handles:
  //   โ€ข Tuning: Songsterr stores strings highโ†’low, alphaTab expects the
  //     raw array as-is (the constructor handles the direction).
  //   โ€ข Percussion: forced to MIDI channel 9 (GM standard).
  //   โ€ข Measures: iterates all master bars; empty ones get rest voices.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function buildTrack(score, entry, masterBarCount, channel) {
    const { trackMeta, revision } = entry;
    const playback = mapInstrument(trackMeta.instrumentId ?? revision.instrumentId);
    const isPerc   = playback.isPercussion || !!trackMeta.isDrums;

    const track = new alphaTab.model.Track();
    track.name      = trackMeta.title || trackMeta.name || revision.name || 'Track';
    track.shortName = track.name.slice(0, 20);
    track.playbackInfo.program          = playback.program;
    track.playbackInfo.primaryChannel   = channel;
    track.playbackInfo.secondaryChannel = channel;

    const staff = new alphaTab.model.Staff();
    staff.isPercussion = isPerc;

    // โ˜… Tuning array passed as-is from Songsterr (highโ†’low order)
    const tuning = revision.tuning || trackMeta.tuning;
    if (Array.isArray(tuning) && tuning.length > 0 && !isPerc) {
      staff.stringTuning = new alphaTab.model.Tuning('Custom', tuning, false);
    }
    const numStrings = Array.isArray(tuning) ? tuning.length : 6;

    for (let mi = 0; mi < masterBarCount; mi++) {
      const bar    = new alphaTab.model.Bar();
      const m      = revision.measures?.[mi];
      const mb     = score.masterBars[mi];
      const voices = m?.voices || [];

      if (!voices || voices.length === 0) {
        // No voices - add a single voice with rests
        const v = new alphaTab.model.Voice();
        fillWithRests(v, mb);
        bar.addVoice(v);
      } else {
        let hasValidVoice = false;
        for (const sv of voices) {
          const v    = new alphaTab.model.Voice();
          const bts  = sv?.beats || [];
          if (!bts || bts.length === 0 || sv?.rest) {
            fillWithRests(v, mb);
          } else {
            for (const bd of bts) {
              if (bd) v.addBeat(mapBeat(bd, mb, isPerc, numStrings));
            }
            if (v.beats.length === 0) fillWithRests(v, mb);
          }
          if (v.beats.length > 0) hasValidVoice = true;
          bar.addVoice(v);
        }
        // Ensure at least one voice exists
        if (!hasValidVoice && bar.voices.length === 0) {
          const v = new alphaTab.model.Voice();
          fillWithRests(v, mb);
          bar.addVoice(v);
        }
      }
      staff.addBar(bar);
    }

    track.addStaff(staff);
    score.addTrack(track);
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // CONVERSION โ€” Assemble the complete alphaTab Score
  // Handles:
  //   โ€ข The track with the most measures is elected as the "master" track
  //     whose measure data drives MasterBar construction.
  //   โ€ข MIDI channels 0โ€“15 are assigned sequentially; channel 9 is always
  //     reserved for percussion (General MIDI specification).
  //   โ€ข score.finish() is mandatory before any export โ€” it finalises all
  //     internal cross-references within the score model.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function buildScore(meta, revisions) {
    const score   = new alphaTab.model.Score();
    score.title   = meta.title;
    score.artist  = meta.artist;
    score.tab     = 'Songsterr Ultimate v3';

    // Elect the track with the most measures as the master reference
    const masterRev = revisions.reduce((best, cur) =>
      (cur.revision?.measures?.length || 0) > (best.revision?.measures?.length || 0) ? cur : best
    ).revision;

    const masterBarCount = Math.max(1,
      revisions.reduce((m, e) => Math.max(m, e.revision?.measures?.length || 0), 0)
    );

    buildMasterBars(score, masterRev, masterBarCount);

    // Assign MIDI channels (0โ€“15), skipping channel 9 for non-percussion
    let nextChannel = 0;
    for (const entry of revisions) {
      const id    = entry.trackMeta.instrumentId ?? entry.revision.instrumentId;
      const isPerc = id === 1024 || !!entry.trackMeta.isDrums;
      let channel;
      if (isPerc) {
        channel = 9; // GM spec: channel 9 is always percussion
      } else {
        if (nextChannel === 9) nextChannel++; // Skip the reserved drum channel
        channel = nextChannel++;
      }
      buildTrack(score, entry, masterBarCount, channel);
    }

    const settings = new alphaTab.Settings();
    score.finish(settings); // โ˜… Mandatory โ€” finalises all internal linkage
    return { score, settings };
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // EXPORT GP7 โ€” Returns a Uint8Array in Guitar Pro 7 (.gp) format
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  function exportGP7(meta, revisions) {
    const { score, settings } = buildScore(meta, revisions);
    return new alphaTab.exporter.Gp7Exporter().export(score, settings);
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // EXPORT MIDI โ€” Returns a Uint8Array in standard MIDI (.mid) format
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  function exportMIDI(meta, revisions) {
    const { score, settings } = buildScore(meta, revisions);
    const midiFile  = new alphaTab.midi.MidiFile();
    const handler   = new alphaTab.midi.AlphaSynthMidiFileHandler(midiFile, true);
    new alphaTab.midi.MidiFileGenerator(score, settings, handler).generate();
    return midiFile.toBinary();
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // UTILITY โ€” Trigger a browser file download from a Uint8Array
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€--
  // Creates a temporary object URL, clicks it, then revokes it.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  function triggerDownload(bytes, fileName, mime) {
    const blob = new Blob([bytes], { type: mime });
    const url  = URL.createObjectURL(blob);
    const a    = document.createElement('a');
    a.href = url; a.download = fileName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  // Sanitize a title into a safe filename (no special characters)
  function safeName(str) {
    return str.replace(/[^a-zA-Z0-9 _\-]/g, '').trim().replace(/\s+/g, '_') || 'tab';
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // UI โ€” Status toast (centered bottom of viewport)
  // Lazily created on first use, auto-hides after a given duration.
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  let _toastTimer = null;
  let _toast = null;

  function getToast() {
    if (!_toast) {
      _toast = document.createElement('div');
      _toast.id = 'sgd-status';
      document.body.appendChild(_toast);
    }
    return _toast;
  }

  function showStatus(msg, type = '', duration = 4500) {
    const t = getToast();
    t.textContent = msg;
    t.className   = 'visible ' + type;
    clearTimeout(_toastTimer);
    if (duration > 0) {
      _toastTimer = setTimeout(() => { t.className = ''; }, duration);
    }
  }

  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // MAIN DOWNLOAD FLOW โ€” triggered on button click
  // Four sequential steps:
  //   1. Read song metadata from the #state element
  //   2. Fetch all revision JSONs from the CloudFront CDN
  //   3. Convert to GP7 or MIDI via alphaTab
  //   4. Trigger browser download
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  async function handleDownload(format, btnGP, btnMID) {
    sgdLog('log', 'SGD Debug', '========================================');
    sgdLog('log', 'SGD Debug', `DOWNLOAD STARTED: format=${format}`);
    sgdLog('log', 'SGD Debug', `Current URL: ${location.href}`);
    sgdLog('log', 'SGD Debug', `Current path: ${location.pathname}`);
    sgdLog('log', 'SGD Debug', '========================================');
    
    btnGP.disabled  = true;
    btnMID.disabled = true;
    showStatus('โณ Reading page stateโ€ฆ', '', 0);

    try {
      // Step 1 โ€” extract metadata from #state (with retry logic for SPA nav)
      sgdLog('log', 'SGD Debug', 'Step 1: Reading page state...');
      const meta = await getStateFromPage();
      sgdLog('log', 'SGD Debug', `Got metadata: ${meta.artist} - ${meta.title} (${meta.tracks.length} tracks)`);
      showStatus(`โณ Fetching ${meta.tracks.length} track(s) from CDNโ€ฆ`, '', 0);

      // Step 2 โ€” download all revision JSONs
      sgdLog('log', 'SGD Debug', 'Step 2: Fetching revisions...');
      const revisions = await fetchAllRevisions(meta);
      sgdLog('log', 'SGD Debug', `Got ${revisions.length} revisions`);
      revisions.forEach((r, i) => {
        sgdLog('log', 'SGD Debug', `Revision ${i}: track="${r.trackMeta.title}", measures=${r.revision?.measures?.length || 0}`);
      });
      showStatus(`โš™๏ธ Converting ${revisions.length} track(s) โ†’ ${format.toUpperCase()}โ€ฆ`, '', 0);

      // Step 3 โ€” build and export
      sgdLog('log', 'SGD Debug', 'Step 3: Building and exporting...');
      const name = safeName(`${meta.artist} - ${meta.title}`);
      sgdLog('log', 'SGD Debug', `Sanitized filename: ${name}`);
      let bytes, fileName, mime;

      if (format === 'gp') {
        sgdLog('log', 'SGD Debug', 'Exporting as GP7...');
        bytes    = exportGP7(meta, revisions);
        fileName = `${name}.gp`;
        mime     = 'application/gp';
      } else {
        sgdLog('log', 'SGD Debug', 'Exporting as MIDI...');
        bytes    = exportMIDI(meta, revisions);
        fileName = `${name}.mid`;
        mime     = 'audio/midi';
      }
      sgdLog('log', 'SGD Debug', `Export complete: ${bytes.length} bytes`);

      // Step 4 โ€” trigger browser download
      sgdLog('log', 'SGD Debug', 'Step 4: Triggering download...');
      triggerDownload(bytes, fileName, mime);
      sgdLog('log', 'SGD Debug', `โœ… Download triggered: ${fileName}`);
      showStatus(`โœ… "${fileName}" downloaded!`, 'ok');

    } catch (err) {
      sgdLog('error', 'SGD Debug', 'โŒ Download failed:', err);
      sgdLog('error', 'SGD Debug', 'Error stack:', err.stack);
      showStatus(`โŒ ${err.message}`, 'err', 7000);
    } finally {
      btnGP.disabled  = false;
      btnMID.disabled = false;
      sgdLog('log', 'SGD Debug', '========================================\n');
    }
  }

  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // 7. BUTTON INJECTION โ€” Replaces the native export button
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  //
  // DOM structure (reverse-engineering Songsterr's HTML):
  //   <div id="c-export" class="B3a4pa B3agq5">   โ† our injection target
  //     <button id="control-export" ...>Export</button>
  //   </div>
  //
  // The controls bar (.B3a1lv) is a flex container. Each item carries
  // the classes B3a4pa + B3agq5 which handle vertical alignment.
  // We replace the entire #c-export div and give our wrapper those same
  // classes so it sits at exactly the same position in the bar.
  //
  // SPA resilience:
  //   โ€ข Permanent MutationObserver: re-injects if #sgd-wrapper disappears
  //     after a React re-render (e.g. switching Tab โ†” Chords view)
  //   โ€ข history.pushState / replaceState / popstate hooks: detect SPA
  //     navigation and schedule re-injection after React re-renders
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

  // Global flag to track if we're in a transition
  let _isPageTransitioning = false;
  
  // Debounce injection attempts to prevent race conditions
  let _injectionTimeout = null;
  
  function debouncedInjection() {
    if (_injectionTimeout) clearTimeout(_injectionTimeout);
    _injectionTimeout = setTimeout(() => {
      tryInjectButtons();
      _injectionTimeout = null;
    }, 100);
  }

  // Only inject on tab/chords song pages, not on the homepage or artist pages
  function isTabPage() {
    return /\/a\/wsa\/.+/.test(location.pathname);
  }

  function createOurButtons() {
    // Reuse the native container's CSS classes for automatic flex alignment
    const wrapper     = document.createElement('div');
    wrapper.id        = 'sgd-wrapper';
    wrapper.className = 'B3a4pa B3agq5'; // same classes as the replaced #c-export div

    // YouTube Audio-Only Toggle Button
    let ytAudioOnlyMode = false;
    try { ytAudioOnlyMode = localStorage.getItem('songsterr_yt_audio_only') === 'true'; } catch (e) {}
    const btnYT = document.createElement('button');
    btnYT.id = 'yt-toggle-btn';
    btnYT.innerHTML = ytAudioOnlyMode ? '๐ŸŽต' : '๐ŸŽฌ';
    btnYT.title = ytAudioOnlyMode ? 'Mode audio uniquement (cliquer pour afficher la vidรฉo)' : 'Vidรฉo visible (cliquer pour audio uniquement)';
    if (ytAudioOnlyMode) btnYT.classList.add('audio-only');
    btnYT.addEventListener('click', () => { if (typeof toggleYtAudioOnly === 'function') toggleYtAudioOnly(); });

    const btnGP      = document.createElement('button');
    btnGP.className  = 'sgd-btn sgd-btn-gp';
    btnGP.innerHTML  = '๐ŸŽธ GP7';
    btnGP.title      = 'Download Guitar Pro 7 (.gp)';

    const btnMID     = document.createElement('button');
    btnMID.className = 'sgd-btn sgd-btn-midi';
    btnMID.innerHTML = '๐ŸŽน MIDI';
    btnMID.title     = 'Download MIDI (.mid)';

    // Check if page is ready before allowing download (just check the flag)
    const canDownload = () => {
      if (_isPageTransitioning) {
        showStatus('โณ Page loading, please wait...', '', 2000);
        return false;
      }
      return true; // Trust checkPageReady() which already validated via API
    };

    btnGP.addEventListener('click',  () => {
      if (!canDownload()) return;
      handleDownload('gp',   btnGP, btnMID);
    });
    btnMID.addEventListener('click', () => {
      if (!canDownload()) return;
      handleDownload('midi', btnGP, btnMID);
    });

    wrapper.appendChild(btnYT);
    wrapper.appendChild(btnGP);
    wrapper.appendChild(btnMID);
    return wrapper;
  }

  function tryInjectButtons() {
    if (!isTabPage()) return false;

    // Already injected and still connected to the DOM โ€” nothing to do
    if (document.getElementById('sgd-wrapper')?.isConnected) return true;

    // โ”€โ”€ Primary target: #c-export (stable React ID) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    const cExport = document.getElementById('c-export');
    if (cExport) {
      cExport.replaceWith(createOurButtons());
      sgdLog('log', 'SGD', 'โœ… Injected (#c-export)');
      return true;
    }

    // โ”€โ”€ Fallback 1: parent of #control-export button โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    const ctrlExport = document.getElementById('control-export');
    if (ctrlExport) {
      (ctrlExport.closest('div') || ctrlExport.parentElement).replaceWith(createOurButtons());
      sgdLog('log', 'SGD', 'โœ… Injected (#control-export parent)');
      return true;
    }

    // โ”€โ”€ Fallback 2: any element with a download-related title/data-id โ”€
    const nativeBtn = document.querySelector(
      '[data-id*="Download"], [data-id*="Export"], [title*="Download tab"]'
    );
    if (nativeBtn) {
      (nativeBtn.closest('div') || nativeBtn.parentElement).replaceWith(createOurButtons());
      sgdLog('log', 'SGD', 'โœ… Injected (fallback title/data-id)');
      return true;
    }

    return false; // Target not in DOM yet โ€” will retry via MutationObserver
  }

  // Permanent MutationObserver: re-injects whenever #sgd-wrapper is
  // removed from the DOM (React re-render after tab โ†” chords switch)
  const btnObserver = new MutationObserver(() => {
    if (!document.getElementById('sgd-wrapper')?.isConnected) {
      debouncedInjection(); // Use debounced injection
    }
  });

  // SPA navigation hook.
  // React Router uses history.pushState to navigate without a page reload.
  // We schedule three injection attempts with increasing delays to cover
  // slow initial renders and lazy-loaded components.
  // IMPORTANT: Track current song and clear cache when changing songs
  let _lastSongPath = location.pathname;
  function onSpaNavigate() {
    const currentPath = location.pathname;
    sgdLog('log', 'SGD Debug', 'SPA Navigation detected:', { from: _lastSongPath, to: currentPath });
    
    // Mark transition start to block downloads
    if (currentPath !== _lastSongPath) {
      _isPageTransitioning = true;
      sgdLog('log', 'SGD Debug', '๐Ÿšซ Page transition started, downloads blocked');
    }
    
    // Only clear cache if we actually changed songs (not just Tab/Chords toggle)
    if (currentPath !== _lastSongPath) {
      if (window.__SGD_REVISION_CACHE) {
        const oldSize = window.__SGD_REVISION_CACHE.size;
        window.__SGD_REVISION_CACHE.clear();
        sgdLog('log', 'SGD Debug', `โœ… Cache cleared (${oldSize} entries) - song changed`);
      } else {
        sgdLog('log', 'SGD Debug', 'No cache to clear');
      }
    } else {
      sgdLog('log', 'SGD Debug', 'Same path, cache preserved');
    }
    _lastSongPath = currentPath;
    
    // Use debounced injection to prevent race conditions
    debouncedInjection();
    setTimeout(() => debouncedInjection(), 500);
    setTimeout(() => debouncedInjection(), 1200);
    
    // Check when page is ready
    checkPageReady();
  }
  
  // Check if state matches URL and allow downloads (uses API when DOM is stale)
  async function checkPageReady() {
    const urlMatch = location.pathname.match(/-s(\d+)$/);
    const urlSongId = urlMatch ? urlMatch[1] : null;
    if (!urlSongId) {
      _isPageTransitioning = false;
      return;
    }
    
    // Quick DOM check first
    const el = document.getElementById('state');
    if (el) {
      try {
        const parsed = JSON.parse(el.textContent || el.innerText);
        const cur = parsed?.meta?.current;
        if (String(cur?.songId) === String(urlSongId)) {
          _isPageTransitioning = false;
          sgdLog('log', 'SGD Debug', 'โœ… Page ready (DOM), downloads enabled');
          return;
        }
      } catch (e) {
        sgdLog('warn', 'SGD', 'Failed to parse DOM state during ready check:', e.message);
      }
    }
    
    // DOM is stale - use API to verify page is ready
    sgdLog('log', 'SGD Debug', 'DOM stale, checking via API...');
    try {
      const apiData = await fetchMetaFromAPI(urlSongId);
      sgdLog('log', 'SGD Debug', 'API response received, unblocking downloads');
      // API returns flat structure, check for revisionId at root level
      if (apiData?.revisionId) {
        _isPageTransitioning = false;
        sgdLog('log', 'SGD Debug', 'โœ… Page ready (API), downloads enabled');
        showStatus('โœ… Ready to download!', 'ok', 1500);
      } else {
        sgdLog('log', 'SGD Debug', 'โŒ API response missing revisionId');
      }
    } catch (err) {
      sgdLog('log', 'SGD Debug', 'API check failed:', err.message);
      // Keep blocked, will retry on next navigation
    }
  }

  const _pushState    = history.pushState.bind(history);
  history.pushState   = function (...a) { _pushState(...a);     onSpaNavigate(); };
  const _replaceState = history.replaceState.bind(history);
  history.replaceState = function (...a) { _replaceState(...a); onSpaNavigate(); };
  window.addEventListener('popstate', onSpaNavigate); // Back/Forward browser buttons

  // Bootstrap
  function startObserving() {
    const go = () => {
      btnObserver.observe(document.body, { childList: true, subtree: true });
      debouncedInjection(); // Use debounced injection
    };
    if (document.body) go();
    else document.addEventListener('DOMContentLoaded', go);
  }

  startObserving();

})();