YouTube Music Beautifier

Elevate your YouTube Music Experience with time-synced lyrics, beautiful animated backgrounds, and enhanced controls!

// ==UserScript==
// @name         YouTube Music Beautifier
// @namespace    http://tampermonkey.net/
// @version      2.2
// @match        https://music.youtube.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @description Elevate your YouTube Music Experience with time-synced lyrics, beautiful animated backgrounds, and enhanced controls!
// ==/UserScript==

(function () {
  'use strict';

  // --- Suppress noisy network errors (lightweight) ---
  (function () {
    const noisy = (msg) =>
      typeof msg === 'string' &&
      [
        'XMLHttpRequest cannot load',
        'Fetch API cannot load',
        'Resource blocked by content blocker',
        'due to access control checks',
      ].some((p) => msg.includes(p));
    const ytTelemetry = (msg) =>
      typeof msg === 'string' &&
      [
        'music.youtube.com/api/stats/atr',
        'music.youtube.com/api/stats/qoe',
        'music.youtube.com/youtubei/v1/log_event',
      ].some((p) => msg.includes(p));
    window._ytmBeautifierSuppressed = window._ytmBeautifierSuppressed || [];
    try {
      ['error', 'warn'].forEach((m) => {
        const orig = console[m].bind(console);
        console[m] = (...args) => {
          try {
            const text = args
              .map((a) =>
                typeof a === 'string'
                  ? a
                  : a && a.message
                  ? a.message
                  : JSON.stringify(a)
              )
              .join(' ');
            if (noisy(text) || ytTelemetry(text)) {
              window._ytmBeautifierSuppressed.push({ t: Date.now(), args });
              if (window._ytmBeautifierSuppressed.length > 200)
                window._ytmBeautifierSuppressed.shift();
              return;
            }
          } catch (e) {}
          return orig(...args);
        };
      });
    } catch (e) {}
    const origOnError = window.onerror;
    window.onerror = function (message, ...rest) {
      if (noisy(String(message))) return true;
      return typeof origOnError === 'function'
        ? origOnError.apply(this, [message, ...rest])
        : false;
    };
    window.addEventListener('unhandledrejection', (ev) => {
      try {
        const r = ev?.reason;
        const msg = typeof r === 'string' ? r : r && r.message ? r.message : '';
        if (noisy(String(msg))) {
          ev.preventDefault();
          return;
        }
      } catch (e) {}
    });
  })();

  // --- GM API fallbacks ---
  const gmGet = (k, d) =>
    typeof GM_getValue !== 'undefined'
      ? GM_getValue(k, d)
      : (function () {
          try {
            const raw = localStorage.getItem('ytm_beautifier_' + k);
            return raw != null ? JSON.parse(raw) : d;
          } catch (e) {
            return d;
          }
        })();
  const gmSet = (k, v) =>
    typeof GM_setValue !== 'undefined'
      ? GM_setValue(k, v)
      : localStorage.setItem('ytm_beautifier_' + k, JSON.stringify(v));
  const gmStyle = (css) =>
    typeof GM_addStyle !== 'undefined'
      ? GM_addStyle(css)
      : document.head.appendChild(
          Object.assign(document.createElement('style'), { textContent: css })
        );
  const gmXhr = (details) => {
    if (typeof GM_xmlhttpRequest !== 'undefined')
      return GM_xmlhttpRequest(details);
    fetch(details.url, {
      method: details.method || 'GET',
      headers: details.headers || {},
    })
      .then((r) =>
        r
          .text()
          .then((t) => {
            if (details.onload) details.onload({ responseText: t, status: r.status, ok: r.ok, headers: r.headers });
          })
      )
      .catch((e) => {
        if (details.onerror) details.onerror(e);
      });
  };

  // --- Config & state ---
  const REST_URL = 'https://ytm.nwvbug.com';
  let currentSong = null,
    lyrics = [],
    times = [],
    offsetSec = 0,
    previousOffset = null,
    containerEl = null,
    currentIndex = 0,
    romanizeEnabled = gmGet('romanize_enabled', false) || false,
    fontSize = gmGet('font_size', 14) || 14,
    attachedMedia = null;

  // --- Utilities ---
  // debug flag and helper
  try {
    window._ytm_debug = window._ytm_debug === undefined ? true : window._ytm_debug;
  } catch (e) {}
  const logDebug = (...args) => {
    try {
      if (window._ytm_debug) console.log('[ytm-beautifier-debug]', ...args);
    } catch (e) {}
  };

  const toSec = (s) => {
    if (!s) return 0;
    const [m, sec] = s.split(':').map((x) => parseInt(x, 10));
    return (m || 0) * 60 + (sec || 0);
  };
  const pad = (s) =>
    `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
  const txt = (s) =>
    (s || '').replaceAll('&', '&').replaceAll(' ', ' ');
  const nowPlaying = () => {
    const bar = document.querySelector('ytmusic-player-bar');
    if (!bar) return null;
    const title = txt(
      bar.querySelector('yt-formatted-string.title.ytmusic-player-bar')
        ?.innerHTML || ''
    );
    const thumbnail = bar.querySelector('img.ytmusic-player-bar')?.src || null;
    const byline = Array.from(
      document.querySelectorAll(
        '.byline.style-scope.ytmusic-player-bar.complex-string > *'
      )
    )
      .map((n) => n.innerText)
      .join('');
    const [artist = '', album = '', date = ''] = byline
      .split('•')
      .map((s) => txt(s?.trim()));
    const left = bar.querySelector('.left-controls');
    const timeStr = left
      ?.querySelector('span.time-info.ytmusic-player-bar')
      ?.innerHTML?.trim();
    if (!timeStr) return null;
    const [elapsed, total] = timeStr.split(' / ');
    const playBtn = left?.querySelector('#play-pause-button');
    const isPlaying = playBtn?.getAttribute('aria-label') === 'Pause';
    let largeImage = null;
    try {
      largeImage = document.querySelector('#thumbnail')?.children?.[0]?.src;
    } catch (e) {}
    return {
      title,
      artist,
      album,
      date,
      thumbnail,
      largeImage,
      isPlaying,
      elapsed: toSec(elapsed),
      total: toSec(total),
    };
  };

  // helper to get the first visible media element (video or audio)
  function getVisibleMediaElement() {
    try {
      const all = Array.from(document.querySelectorAll('video,audio'));
      for (let i = 0; i < all.length; i++) {
        const m = all[i];
        if (!m) continue;
        try {
          if (m.duration > 0 && m.offsetParent !== null) return m;
        } catch (e) {}
      }
    } catch (e) {}
    return null;
  }

  // --- Romanization helper ---
  const romanizationLanguages = new Set([
    'ja', 'ru', 'ko', 'zh-CN', 'zh-TW', 'bn', 'th', 'ar', 'ta', 'te', 'ml', 'kn', 'gu', 'pa', 'mr', 'ur', 'si', 'my', 'ka', 'km', 'lo', 'fa',
  ]);

  function detectLangFromText(text) {
    if (!text) return 'und';
    // quick script checks via Unicode ranges
    if (/[\u3040-\u30FF]/.test(text)) return 'ja'; // Hiragana/Katakana
    if (/[-]/.test(text) === false && /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]/.test(text)) return 'zh-CN'; // Han
    if (/[\uAC00-\uD7AF]/.test(text)) return 'ko'; // Hangul
    if (/[\u0400-\u04FF]/.test(text)) return 'ru'; // Cyrillic
    if (/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/.test(text)) return 'ar'; // Arabic block (covers Persian/Urdu script)
    if (/[\u0E00-\u0E7F]/.test(text)) return 'th'; // Thai
    if (/[\u0900-\u097F]/.test(text)) return 'hi'; // Devanagari
    if (/[\u0980-\u09FF]/.test(text)) return 'bn'; // Bengali
    if (/[\u0A80-\u0AFF]/.test(text)) return 'gu'; // Gujarati
    if (/[\u0A00-\u0A7F]/.test(text)) return 'pa'; // Gurmukhi
    if (/[\u0D80-\u0DFF]/.test(text)) return 'si'; // Sinhala
    if (/[\u1000-\u109F]/.test(text)) return 'my'; // Myanmar
    if (/[\u0C80-\u0CFF]/.test(text)) return 'kn'; // Kannada
    if (/[\u0D00-\u0D7F]/.test(text)) return 'ml'; // Malayalam
    if (/[\u0B80-\u0BFF]/.test(text)) return 'ta'; // Tamil
    if (/[\u0C00-\u0C7F]/.test(text)) return 'te'; // Telugu
    if (/[\u1780-\u17FF]/.test(text)) return 'km'; // Khmer
    if (/[\u0E80-\u0EFF]/.test(text)) return 'lo'; // Lao
    if (/[\u10A0-\u10FF]/.test(text)) return 'ka'; // Georgian
    // fallback: contains any non-ASCII? then und
    if (/[^-]/.test(text)) return 'und';
    return 'en';
  }

  function simpleExtractLatin(str) {
    // extract contiguous Latin (including diacritics) sequences
    const re = /[A-Za-z\u00C0-\u024F\u1E00-\u1EFF'’ː\-]+(?:[\s\u00C0-\u024F\u1E00-\u1EFF'’ː\-]+[A-Za-z\u00C0-\u024F\u1E00-\u1EFF'’ː\-]+)*/g;
    const matches = Array.from(String(str).matchAll(re)).map((m) => m[0].trim());
    return matches.join(' ');
  }

  function romanize(text, source_language = 'auto', options = { skip_if_identical: true }) {
    return new Promise((resolve) => {
      const original = String(text || '');
      const notes = [];
      if (!original || original.trim() === '' || /^[\s♪♫]+$/.test(original)) {
        return resolve({ original_text: original, source_language: source_language || 'auto', romanized_text: null, notes: 'empty or musical symbols' });
      }

      let detected = source_language || 'auto';
      if (!detected || detected === 'auto') {
        detected = detectLangFromText(original) || 'und';
        notes.push(`detected: ${detected}`);
      }

      // decide whether to attempt romanization
      const hasNonLatin = /[^\u0000-\u007f]/.test(original);
      const wantRomanize = romanizationLanguages.has(detected) || hasNonLatin || detected === 'und';
      if (!wantRomanize) {
        return resolve({ original_text: original, source_language: detected, romanized_text: null, notes: 'no non-Latin script detected' });
      }

      // Try Google Translate undocumented endpoint for transliteration
      try {
        const sl = (source_language && source_language !== 'auto') ? source_language : 'auto';
        const tl = (detected && detected !== 'und') ? `${detected}-Latn` : 'auto-Latn';
        const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${encodeURIComponent(sl)}&tl=${encodeURIComponent(tl)}&dt=rm&q=${encodeURIComponent(original)}`;
        gmXhr({ method: 'GET', url, onload: (resp) => {
          try {
            const body = resp?.responseText || '';
            let romanized = null;
            try {
              const j = JSON.parse(body);
              // recursively walk parsed JSON and collect Latin-like strings
              const pieces = [];
              const walker = (v) => {
                if (!v && v !== 0) return;
                if (typeof v === 'string') {
                  const ex = simpleExtractLatin(v);
                  if (ex && ex.length > 1 && ex.toLowerCase() !== 'null') pieces.push(ex);
                  return;
                }
                if (Array.isArray(v)) return v.forEach(walker);
                if (typeof v === 'object') return Object.values(v).forEach(walker);
              };
              walker(j);
              if (pieces.length) {
                // join with space and collapse duplicates/short artifacts
                const uniq = Array.from(new Set(pieces.map((s) => s.trim()))).filter(Boolean);
                // filter out tokens that look like language tags or very short codes (e.g., 'fa', 'en', 'pa-Arab')
                const cleaned = uniq
                  .map((s) => s.replace(/^\s+|\s+$/g, ''))
                  .filter((s) => !/^[a-z]{1,3}(-[A-Za-z0-9]+)?$/i.test(s));
                const joined = cleaned.join(' ');
                const extracted = simpleExtractLatin(joined);
                if (extracted && extracted.length > 0) romanized = extracted;
              }
            } catch (e) {
              // fallback: extract from raw
              const extracted = simpleExtractLatin(body);
              if (extracted && extracted.length > 0) romanized = extracted;
            }

            // normalize spaces; try to map back to lines roughly
            if (romanized) {
              // try to respect original line breaks if possible
              const origLines = original.split(/\r?\n/);
              const romanLines = romanized.split(/\s*\n\s*/).join(' ');
              // simple attempt: if original had multiple lines, split romanized into same count by spaces
              let final = romanized;
              if (origLines.length > 1) {
                // split tokens and distribute
                const tokens = romanized.split(/\s+/).filter(Boolean);
                const per = Math.ceil(tokens.length / origLines.length) || 1;
                const groups = [];
                for (let i = 0; i < origLines.length; i++) groups.push(tokens.slice(i * per, (i + 1) * per).join(' '));
                final = groups.join('\n').trim();
              }

              // optional ascii-only post-processing: strip diacritics / combining marks
              if (options && options.ascii) {
                try {
                  // normalize and remove combining diacritics
                  final = final.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
                  // remove any remaining non-ASCII characters except whitespace and basic punctuation
                  final = final.replace(/[^\x00-\x7F\s'’\-\n]/g, '');
                } catch (e) {
                  // fallback: remove common combining range if normalize isn't available
                  final = final.replace(/[\u0300-\u036f]/g, '');
                }
              }

              // skip_if_identical check (case-insensitive)
              if (options && options.skip_if_identical && final.toLowerCase().trim() === original.toLowerCase().trim()) {
                return resolve({ original_text: original, source_language: detected, romanized_text: null, notes: 'identical to input' });
              }

              // notes: add language-specific hints
              let note = '';
              if (detected.startsWith('zh')) note = 'pinyin with tone marks possible';
              else if (detected === 'ja') note = 'Japanese → romaji';
              else if (detected === 'ru') note = 'Cyrillic → Latin transliteration';
              else if (detected === 'th') note = 'Thai → Latin transliteration';
              else if (detected === 'ar' || detected === 'fa' || detected === 'ur') note = 'Arabic script → Latin transliteration';
              else if (detected === 'ko') note = 'Korean → Latin transliteration (romanization)';

              return resolve({ original_text: original, source_language: detected, romanized_text: final, notes: note });
            }
          } catch (err) {
            // fall through to local fallback
          }
          // fallback: attempt simple extraction from original
          const fallback = simpleExtractLatin(original);
          if (fallback && fallback.length > 0 && !(options && options.skip_if_identical && fallback.toLowerCase() === original.toLowerCase())) {
            return resolve({ original_text: original, source_language: detected, romanized_text: fallback, notes: 'extracted Latin substrings (fallback)' });
          }
          return resolve({ original_text: original, source_language: detected, romanized_text: null, notes: 'romanization not available' });
        }, onerror: () => {
          const fallback = simpleExtractLatin(original);
          if (fallback && fallback.length > 0) return resolve({ original_text: original, source_language: detected, romanized_text: fallback, notes: 'extracted Latin substrings (fallback)' });
          return resolve({ original_text: original, source_language: detected, romanized_text: null, notes: 'request failed' });
        } });
      } catch (e) {
        const fallback = simpleExtractLatin(original);
        if (fallback && fallback.length > 0) return resolve({ original_text: original, source_language: detected, romanized_text: fallback, notes: 'extracted Latin substrings (fallback)' });
        return resolve({ original_text: original, source_language: detected, romanized_text: null, notes: 'error' });
      }
    });
  }

  // --- Translation helper (returns English translation) ---
  function translateToEnglish(text) {
    return new Promise((resolve) => {
      if (!text || !text.trim()) return resolve(null);
      try {
        const q = encodeURIComponent(String(text));
        const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t&q=${q}`;
        gmXhr({ method: 'GET', url, onload: (resp) => {
          try {
            const body = resp?.responseText || '';
            const j = JSON.parse(body);
            if (Array.isArray(j) && Array.isArray(j[0])) {
              const txt = j[0].map((seg) => seg[0]).filter(Boolean).join(' ').trim();
              return resolve(txt || null);
            }
          } catch (e) {}
          return resolve(null);
        }, onerror: () => resolve(null) });
      } catch (e) { return resolve(null); }
    });
  }

  // Shared translation cache and generic translate helper (top-level so buildLyrics can access)
  let __ytm_translation_cache = {};
  async function translate(text, target) {
    if (!text || !text.trim()) return null;
    if (!target || target === 'en') return translateToEnglish(text);
    return new Promise((resolve) => {
      try {
        const q = encodeURIComponent(String(text));
        const tl = encodeURIComponent(String(target));
        const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${tl}&dt=t&q=${q}`;
        gmXhr({ method: 'GET', url, onload: (resp) => {
          try {
            const body = resp?.responseText || '';
            const j = JSON.parse(body);
            if (Array.isArray(j) && Array.isArray(j[0])) {
              const txt = j[0].map((seg) => seg[0]).filter(Boolean).join(' ').trim();
              return resolve(txt || null);
            }
          } catch (e) {}
          return resolve(null);
        }, onerror: () => resolve(null) });
      } catch (e) { resolve(null); }
    });
  }

  // --- Lyrics fetching/parsing ---
  function fetchLyrics(title, artist, album, year, reroll = false) {
    const base = (
      title +
      ' ' +
      artist +
      (reroll ? ' ' + album : '') +
      ' ' +
      year
    )
      .replaceAll('/', '-')
      .replaceAll('%', '%25');
    gmXhr({
      method: 'GET',
      url: `${REST_URL}/request-lyrics/${base}`,
      onload: (r) => parseLyricsResponse(r?.responseText || '' , year),
      onerror: () => {
        lyrics = ['Failed to fetch lyrics'];
        times = [0];
        buildLyrics();
      },
    });
  }
  function parseLyricsResponse(text, songDuration) {
    // coerce songDuration to a number when possible; sometimes callers pass non-numeric (e.g. likes string)
    try {
      const s = Number(songDuration);
      if (!isNaN(s) && s > 0) songDuration = s;
      else {
        const np = nowPlaying();
        if (np && np.total) songDuration = np.total;
      }
    } catch (e) {}
    logDebug('parseLyricsResponse: got response length', text?.length || 0, 'songDuration', songDuration);
    // store last raw response for diagnostics
    try {
      window._ytm_last_lyrics_response = text;
    } catch (e) {}

    if (!text || text.trim() === '' || text === 'no_lyrics_found') {
      lyrics = ['No lyrics available'];
      times = [0];
      return buildLyrics();
    }

    // If the response looks like an HTML error page, report nicely
    if (/<html|<head|<title|doctype html/i.test(text) && /500|error|not found|service unavailable/i.test(text)) {
      lyrics = ['No lyrics available (server error)'];
      times = [0];
      return buildLyrics();
    }

    // If the response looks like an LRC file (timestamps [mm:ss] or [mm:ss.xx]) - handle directly
    if (/^\s*\[\d{1,2}:\d{2}(?:[\.:]\d{1,3})?\]/m.test(text)) {
      return parseLRC(text, songDuration);
    }

    // Try JSON parse and be tolerant to shapes
    try {
      const res = JSON.parse(text);
      logDebug('parseLyricsResponse: parsed JSON root keys', Object.keys(res || {}));
      // Common fields: res.lrc can be string (LRC), object, or array
  if (res.lrc && typeof res.lrc === 'string') return parseLRC(res.lrc, songDuration);
  if (res.lrc && Array.isArray(res.lrc)) return parseYtm(res.lrc, songDuration);
      // sometimes the payload is directly an array
  if (Array.isArray(res)) return parseYtm(res, songDuration);
      // sometimes it's nested: {data:{lines: [...]}}
      const maybe = res.lines || res.data?.lines || res.data?.lrc || res.lyrics || res.result;
  if (Array.isArray(maybe)) return parseYtm(maybe, songDuration);
      // fallback: if res has text properties, try to extract joined text
      if (typeof res === 'object') {
        const collected = [];
        const walker = (v) => {
          if (!v) return;
          if (typeof v === 'string') collected.push(v);
          else if (Array.isArray(v)) v.forEach(walker);
          else if (typeof v === 'object') Object.values(v).forEach(walker);
        };
        walker(res);
  const joined = collected.join('\n');
  if (/^\s*\[\d{1,2}:\d{2}/m.test(joined)) return parseLRC(joined, songDuration);
      }
    } catch (e) {}

    // after parsing we'll normalize times if we have a duration

    // last resort: try to extract Latin or text lines
    const extracted = simpleExtractLatin(text);
    if (extracted && extracted.length > 0) {
      lyrics = extracted.split(/\n+/).map((s) => s.trim()).filter(Boolean);
      if (lyrics.length) {
        times = lyrics.map(() => 0);
        // normalize (no duration available here)
        times = normalizeTimes(times, songDuration);
        return buildLyrics();
      }
    }

    lyrics = ['Lyrics parsing failed'];
    times = [0];
    buildLyrics();
  }
  function parseLRC(txtLrc, songDuration) {
    const lines = String(txtLrc).split(/\r?\n/);
    lyrics = [];
    times = [];
    try { window._ytm_raw_lines = lines.slice(0,200); } catch (e) {}
    // support metadata tags [ti:..] [ar:..] [al:..] etc. ignore them
    const tsRe = /\[(\d{1,2}):(\d{2})(?:[\.:](\d{1,3}))?\]/g;
    lines.forEach((l) => {
      if (!l || !l.trim()) return;
      // ignore metadata
      if (/^\s*\[(ti|ar|al|by|offset)[:]/i.test(l)) return;
      // find all timestamps in the line
      const timesFound = [];
      let m;
      while ((m = tsRe.exec(l)) !== null) {
        const mm = parseInt(m[1] || '0', 10);
        const ss = parseInt(m[2] || '0', 10);
        const ms = parseInt((m[3] || '0').padEnd(3, '0'), 10);
        const total = mm * 60 + ss + ms / 1000;
        timesFound.push(total);
      }
      // strip timestamps from text
      const text = l.replace(tsRe, '').trim();
      if (timesFound.length) {
        timesFound.forEach((t) => {
          times.push(Math.floor(t));
          lyrics.push(text || '♪♪');
        });
      } else if (/^[^\[]+$/.test(l.trim())) {
        // no timestamp but plain text line: append as unlabeled
        times.push(0);
        lyrics.push(l.trim() || '♪♪');
      }
    });
    // normalize times based on song duration if provided
    logDebug('parseLRC: parsed', times.length, 'lines, sample times', times.slice(0,6));
    times = normalizeTimes(times, songDuration);
    logDebug('parseLRC: normalized sample times', times.slice(0,6));
    sanitizeAndBuild();
  }
  // normalize times array using song duration heuristics
  function normalizeTimes(arrTimes, songDuration) {
    try {
      if (!Array.isArray(arrTimes) || arrTimes.length === 0) return arrTimes;
      const timesCopy = arrTimes.slice();
      const maxT = Math.max(...timesCopy);
      const median = (() => {
        const s = timesCopy.slice().sort((a,b) => a-b);
        const mid = Math.floor(s.length/2);
        return s.length % 2 === 1 ? s[mid] : (s[mid-1]+s[mid])/2;
      })();
      // if we have a sensible song duration, use it to decide
      if (songDuration && songDuration > 1) {
        logDebug('normalizeTimes: median', median, 'max', maxT, 'songDuration', songDuration);
        if (median > songDuration * 1.2 || maxT > songDuration * 1.2) {
          logDebug('normalizeTimes: applying ms->s scaling');
          // likely milliseconds -> divide by 1000
          const scaled = timesCopy.map((v) => Math.floor(Number(v)/1000));
          try { window._ytm_parsed_times = scaled; } catch (e) {}
          return scaled;
        }
      }
      // fallback: if times are huge (>100000) treat as ms
      if (maxT > 100000) {
        const scaled = timesCopy.map((v) => Math.floor(Number(v)/1000));
        try { window._ytm_parsed_times = scaled; } catch (e) {}
        return scaled;
      }
      try { window._ytm_parsed_times = timesCopy; } catch (e) {}
      return timesCopy;
    } catch (e) { return arrTimes; }
  }
  function parseYtm(arr, songDuration) {
    try {
      // array of {text,time} or nested shapes
      if (!Array.isArray(arr)) arr = [arr];
      const outLyrics = [];
      const outTimes = [];
      arr.forEach((a) => {
        if (!a) return;
        if (typeof a === 'string') {
          outLyrics.push(a.trim() || '♪♪');
          outTimes.push(0);
          return;
        }
        // common structures: {text:'', time:123} or {lines:[{text:'',time:...}]}
        if (a.text || a.lyric) {
          outLyrics.push((a.text || a.lyric).trim() || '♪♪');
          // time may be in seconds or milliseconds; coerce safely
          let raw = a.time || a.t || 0;
          let num = Number(raw) || 0;
          if (num > 100000) num = Math.round(num / 1000); // likely milliseconds
          outTimes.push(Math.floor(num || 0));
          return;
        }
        if (Array.isArray(a.lines)) {
          a.lines.forEach((ln) => {
            outLyrics.push((ln.text || ln.lyric || '').trim() || '♪♪');
            let raw = ln.time || ln.t || 0;
            let num = Number(raw) || 0;
            if (num > 100000) num = Math.round(num / 1000);
            outTimes.push(Math.floor(num || 0));
          });
          return;
        }
        // nested objects: try to extract string values
        const walkerTexts = [];
        const walker = (v) => {
          if (!v) return;
          if (typeof v === 'string') walkerTexts.push(v);
          else if (Array.isArray(v)) v.forEach(walker);
          else if (typeof v === 'object') Object.values(v).forEach(walker);
        };
        walker(a);
        if (walkerTexts.length) {
          outLyrics.push(walkerTexts.join(' ').trim() || '♪♪');
          // try to extract a numeric time from the object (time, t)
          let rawT = a.time || a.t || 0;
          let nT = Number(rawT) || 0;
          if (nT > 100000) nT = Math.round(nT / 1000);
          outTimes.push(Math.floor(nT || 0));
        }
      });
  try { window._ytm_raw_parsed = arr; } catch (e) {}
  logDebug('parseYtm: extracted', outLyrics.length, 'lines, sample times', outTimes.slice(0,6));
  if (!outLyrics.length) throw new Error('no lyrics parsed');
      lyrics = outLyrics;
      times = outTimes;
      // normalize times using song duration heuristic
      times = normalizeTimes(times, songDuration);
  logDebug('parseYtm: normalized sample times', times.slice(0,6));
      sanitizeAndBuild();
    } catch (e) {
      // fallback: if arr is a string containing lrc
      if (typeof arr === 'string') return parseLRC(arr);
      lyrics = ['Lyrics parsing failed'];
      times = [0];
      buildLyrics();
    }
  }
  function sanitizeAndBuild() {
    if (lyrics.length === 0) {
      lyrics = ['Lyrics parsing failed'];
      times = [0];
    }
    buildLyrics();
  }

  // --- UI styles (minified-ish) ---
  gmStyle(`@import url('https://fonts.googleapis.com/css2?family=Host+Grotesk:ital,wght@0,300..800;1,300..800&display=swap');
#ytm-lyrics-card{position:fixed;top:20px;right:20px;width:350px;max-height:90vh;background:rgba(0,0,0,.85);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.08);border-radius:16px;font-family:Host Grotesk,serif;color:#fff;z-index:10000;display:none;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,.3);transition:all .3s;resize:both;overflow:auto;min-width:260px;min-height:120px;max-width:calc(100% - 16px);}
#ytm-lyrics-card.active{display:flex}
/* header: allow wrapping and flexible layout so title/artist/stats fit on small screens */
#ytm-lyrics-header{padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.08);display:flex;flex-direction:column;align-items:stretch;cursor:grab;gap:6px;position:relative}
#ytm-lyrics-header > div{min-width:0}
#ytm-song-info{flex:0 0 auto;min-width:0;display:flex;flex-direction:column;width:100%}
#ytm-lyrics-controls{flex:0 0 auto;min-width:0;width:100%;display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end}
#ytm-song-title{font-size:14px;font-weight:600;white-space:normal;overflow:visible;max-width:100%;word-break:normal;overflow-wrap:break-word;hyphens:auto}
#ytm-song-artist{font-size:12px;opacity:.7;max-width:100%;white-space:normal;overflow:visible}
#ytm-song-stats{font-size:12px;opacity:.9;margin-left:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ytm-mini-control{width:auto;height:30px;min-width:28px;border-radius:8px;background:rgba(255,255,255,.08);border:none;color:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:13px;padding:0 6px}
#ytm-romanize-btn[aria-pressed="true"]{background:linear-gradient(135deg,#ffd46f,#ffb86b);color:#000}
#ytm-romanize-btn{transition:all .18s}
/* header controls: allow wrapping when space is insufficient */
#ytm-lyrics-controls{display:flex;gap:8px;align-items:center;flex:0 0 auto;flex-wrap:wrap;justify-content:flex-end;width:100%}
#ytm-lyrics-controls > *{flex:0 1 auto;min-width:0}
/* resize row: compact single horizontal row under header */
#ytm-resize-row{display:flex;gap:8px;padding:6px 16px 0 16px;justify-content:flex-end;align-items:center}
#ytm-resize-row .ytm-mini-control{width:30px;height:28px;font-size:12px}
#ytm-lyrics-controls .ytm-mini-control.active,#ytm-romanize-btn.active{background:linear-gradient(135deg,#ffd46f,#ffb86b);color:#000}
#ytm-lyrics-card{--ytm-font-size:14px}
#ytm-header-actions{position:absolute;right:12px;top:12px;display:flex;gap:8px;align-items:center;z-index:10002}
#ytm-header-actions .ytm-mini-control{background:rgba(255,255,255,0.06)}
#ytm-lyrics-card *{box-sizing:border-box}
#ytm-lyrics-content{flex:1;overflow:auto;padding:20px;text-align:center}
.ytm-lyric-line{font-size:var(--ytm-font-size);opacity:.4;margin:12px 0;transition:all .25s;cursor:pointer;padding:8px 12px;border-radius:8px}
.ytm-lyric-line.active{opacity:1;font-weight:600;color:#ff6b6b;transform:scale(1.03);background:rgba(255,107,107,.06)}
.ytm-romanized{display:block;font-size:calc(var(--ytm-font-size) * 0.85);opacity:.75;margin-top:6px;font-style:italic}
.ytm-translated{display:block;font-size:calc(var(--ytm-font-size) * 0.9);opacity:.85;margin-top:6px;color:#9fe0ff}
#ytm-sync-btn{width:28px;height:28px;font-size:12px;margin-left:8px}
.ytm-sync-feedback{display:inline-block;margin-left:8px;color:#8ef0a1;opacity:0;transition:opacity .3s}
#ytm-translate-btn{width:36px;height:28px;font-size:11px;margin-left:6px}
#ytm-launcher{position:fixed;top:100px;right:20px;background:linear-gradient(135deg,#ff6b6b,#ff5252);color:#fff;border:none;border-radius:999px;padding:10px 14px;font-weight:700;cursor:pointer;z-index:99999;display:inline-flex;align-items:center;gap:10px;backdrop-filter:blur(6px);box-shadow:0 8px 30px rgba(0,0,0,.35)}
#ytm-launcher.active{transform:translateY(-1px);box-shadow:0 10px 36px rgba(0,0,0,.45)}
/* Small-screen responsive tweaks */
@media(max-width:768px){
  #ytm-lyrics-card{width:320px;right:10px;top:10px;max-height:70vh}
  #ytm-launcher{top:10px;right:10px;padding:8px 12px}
}
@media(max-width:480px){
  /* make card nearly full-width and move to top-left margin */
  #ytm-lyrics-card{width:calc(100% - 16px);right:8px;left:8px;top:8px;border-radius:12px}
  /* stack header items vertically to avoid truncation */
  #ytm-lyrics-header{flex-direction:column;align-items:flex-start;padding:10px 12px;gap:6px}
  #ytm-lyrics-controls{width:100%;display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end}
  /* allow title/artist to wrap and show multiple lines */
  #ytm-song-title{white-space:normal;overflow:visible;max-width:100%;font-size:15px}
  #ytm-song-artist{white-space:normal;overflow:visible;max-width:100%;font-size:13px}
  #ytm-song-stats{font-size:12px;margin-top:6px}
  /* reduce control sizes to fit */
  .ytm-mini-control{width:28px;height:28px;font-size:12px;padding:0 5px}
  /* hide some controls when the controls container has the compact class (moved to overflow) */
  #ytm-lyrics-controls.compact #ytm-translate-lang,#ytm-lyrics-controls.compact #ytm-translate-lang-input{display:none}
  #ytm-resize-row{display:none}
  #ytm-romanize-btn{order:0}
}
/* At moderate narrow widths, ensure controls drop below the title instead of squeezing it */
@media(max-width:680px){
  #ytm-lyrics-controls{flex-basis:100%;justify-content:flex-end;flex-wrap:wrap}
}
@media(max-width:360px){
  /* hide less important controls on very small screens */
  #ytm-translate-all, [id^="ytm-tr-"], [id^="ytm-sync-"]{display:none}
  .ytm-mini-control{width:26px;height:26px;font-size:11px}
}
  /* overflow menu styling */
  #ytm-overflow-menu{font-size:13px}
  #ytm-overflow-list > div > *{display:inline-flex;align-items:center;gap:8px}
  #ytm-overflow-list .ytm-mini-control{margin:0;padding:6px 8px;background:rgba(255,255,255,0.04);border-radius:6px}
`);

  // small extra styles for song stats (views / likes)
  gmStyle(`#ytm-song-stats{font-size:12px;opacity:.9;margin-top:6px;color:#dfe7ee;display:flex;align-items:center;gap:8px;flex-wrap:wrap;white-space:normal}
#ytm-song-stats .ytm-stat{margin-right:0;opacity:.9}
#ytm-song-stats a{color:inherit;text-decoration:underline;opacity:.85}
/* allow a larger refresh button that isn't constrained by the default 30px width */
.ytm-mini-control.ytm-refresh{width:auto;min-width:0;padding:4px 8px;font-size:11px}
@media(max-width:480px){
  /* ensure stats wrap nicely on small screens */
  #ytm-song-stats{width:100%;margin-top:6px}
  .ytm-mini-control.ytm-refresh{padding:4px 6px}
}`);

  // --- Build UI ---
  function buildLyricsCard() {
    const old = document.getElementById('ytm-lyrics-card');
    if (old) old.remove();
    const card = document.createElement('div');
    card.id = 'ytm-lyrics-card';
    const header = document.createElement('div');
    header.id = 'ytm-lyrics-header';
    const info = document.createElement('div');
    info.id = 'ytm-song-info';
    info.style.minWidth = 0;
    const t = document.createElement('div');
    t.id = 'ytm-song-title';
    t.textContent = 'No song playing';
    const a = document.createElement('div');
    a.id = 'ytm-song-artist';
    a.textContent = 'YouTube Music';
  // place for views / likes; populated by updateSongStats
  const stats = document.createElement('div');
    stats.id = 'ytm-song-stats';
    stats.textContent = '';
  info.appendChild(t);
  info.appendChild(a);
  info.appendChild(stats);
    
    const ctr = document.createElement('div');
    ctr.id = 'ytm-lyrics-controls';
    // reset offset button (header)
    const resetOffsetBtn = Object.assign(document.createElement('button'), {
      className: 'ytm-mini-control',
      id: 'ytm-reset-offset',
      title: 'Reset subtitle offset for this song',
      textContent: '⤶',
    });
    // current offset label
    const offsetLabel = Object.assign(document.createElement('span'), {
      id: 'ytm-offset-label',
      textContent: '',
      style: 'margin-left:10px;font-size:12px;opacity:0.9',
    });
    // undo offset button
    const undoOffsetBtn = Object.assign(document.createElement('button'), {
      className: 'ytm-mini-control',
      id: 'ytm-undo-offset',
      title: 'Undo last offset change',
      textContent: '↶',
    });
    // Romanize toggle
    const romanBtn = Object.assign(document.createElement('button'), {
      className: 'ytm-mini-control',
      id: 'ytm-romanize-btn',
      title: romanizeEnabled ? 'Romanization: ON' : 'Romanization: OFF',
      textContent: 'Aa',
      'aria-pressed': romanizeEnabled ? 'true' : 'false',
    });
  if (romanizeEnabled) romanBtn.classList.add('active');
  ctr.appendChild(romanBtn);
    // Translate ALL lines button (beside romanize) and target language selector
    const langSelect = Object.assign(document.createElement('select'), {
      className: 'ytm-mini-control',
      id: 'ytm-translate-lang',
      title: 'Translation target language',
    });
    // small compact options: English default + some common languages
    const langs = [
      ['en', 'EN'],
      ['auto', 'Auto'],
      ['hi', 'HI'],
      ['ur', 'UR'],
      ['es', 'ES'],
      ['fr', 'FR'],
      ['de', 'DE'],
      ['ja', 'JA'],
      ['ko', 'KO'],
      ['zh-CN', 'ZH-CN'],
    ];
    langs.forEach(([code, label]) => {
      const op = document.createElement('option');
      op.value = code;
      op.textContent = label;
      langSelect.appendChild(op);
    });
    // restore persisted translate language
    try {
      const savedLang = gmGet('translate_lang', null) || null;
      langSelect.value = savedLang || 'en';
    } catch (e) { langSelect.value = 'en'; }
    langSelect.style.minWidth = '56px';
    langSelect.style.padding = '0 6px';
    langSelect.style.height = '30px';
    langSelect.style.borderRadius = '8px';
    langSelect.style.background = 'rgba(255,255,255,.04)';
    langSelect.style.color = '#fff';
    ctr.appendChild(langSelect);
    // custom language input (freeform) - small textbox
    const langInput = Object.assign(document.createElement('input'), {
      className: 'ytm-mini-control',
      id: 'ytm-translate-lang-input',
      title: 'Type a language code (e.g. pt, hi, ur) and press Enter',
      placeholder: 'code',
      type: 'text',
    });
    langInput.style.width = '56px';
    langInput.style.padding = '4px 6px';
    langInput.style.background = 'rgba(255,255,255,.04)';
    langInput.style.color = '#fff';
    langInput.style.border = 'none';
    langInput.style.borderRadius = '8px';
    langInput.style.fontSize = '12px';
    ctr.appendChild(langInput);
    const translateAllBtn = Object.assign(document.createElement('button'), {
      className: 'ytm-mini-control',
      id: 'ytm-translate-all',
      title: 'Translate all lines to the selected language',
      textContent: 'T→all'
    });
    translateAllBtn.style.marginLeft = '6px';
    ctr.appendChild(translateAllBtn);
    // update per-line translate button labels when language changes
    langSelect.onchange = () => {
      try {
        const target = langSelect.value || 'en';
        try { gmSet('translate_lang', target); } catch (e) {}
        const label = target === 'auto' ? 'Auto' : (target.length > 2 ? target : target.toUpperCase());
        document.querySelectorAll('[id^="ytm-tr-"]').forEach((b) => {
          try { b.textContent = `T→${label}`; b.title = `Translate to ${target}`; } catch (e) {}
        });
        translateAllBtn.title = `Translate all lines to ${target}`;
      } catch (e) {}
    };
    // handle freeform input: add/select on Enter or blur
    const applyLangInput = (val) => {
      try {
        if (!val) return;
        const code = String(val).trim();
        if (!code) return;
        // if option exists, select it; otherwise add it
        let opt = Array.from(langSelect.options).find((o) => o.value === code);
        if (!opt) {
          opt = document.createElement('option');
          opt.value = code;
          opt.textContent = code.length > 2 ? code : code.toUpperCase();
          langSelect.appendChild(opt);
        }
        langSelect.value = code;
        try { gmSet('translate_lang', code); } catch (e) {}
        // trigger onchange to update labels
        langSelect.onchange && langSelect.onchange();
      } catch (e) {}
    };
    langInput.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') {
        e.preventDefault();
        applyLangInput(langInput.value);
        langInput.value = '';
      }
    });
    langInput.addEventListener('blur', () => {
      if (langInput.value && langInput.value.trim()) {
        applyLangInput(langInput.value);
        langInput.value = '';
      }
    });
    // font size controls (order: romanize, increase, decrease)
    const decBtn = Object.assign(document.createElement('button'), {
      className: 'ytm-mini-control',
      id: 'ytm-font-dec',
      title: 'Decrease font size',
      'aria-label': 'Decrease font size',
      textContent: 'A˅',
    });
    const incBtn = Object.assign(document.createElement('button'), {
      className: 'ytm-mini-control',
      id: 'ytm-font-inc',
      title: 'Increase font size',
      'aria-label': 'Increase font size',
      textContent: 'A˄',
    });
    // append in desired visual order: romanize (already appended), increase, decrease
    ctr.appendChild(incBtn);
    ctr.appendChild(decBtn);
    const minB = Object.assign(document.createElement('button'), {
      className: 'ytm-mini-control',
      id: 'ytm-minimize-btn',
      title: 'Minimize',
      textContent: '−',
    });
    const closeB = Object.assign(document.createElement('button'), {
      className: 'ytm-mini-control',
      id: 'ytm-close-btn',
      title: 'Close',
      textContent: '×',
    });
    // create a header-top actions container and place minimize/close there (top-right)
    const headerActions = document.createElement('div');
    headerActions.id = 'ytm-header-actions';
    headerActions.style.cssText = 'position:absolute;right:12px;top:12px;display:flex;gap:8px;align-items:center;z-index:10002';
    header.appendChild(headerActions);
    headerActions.appendChild(minB);
    headerActions.appendChild(closeB);
  // append reset offset to controls (right aligned)
    ctr.appendChild(resetOffsetBtn);
    ctr.appendChild(offsetLabel);
    ctr.appendChild(undoOffsetBtn);
    // overflow / hamburger menu for small screens
    const overflowBtn = Object.assign(document.createElement('button'), {
      className: 'ytm-mini-control',
      id: 'ytm-overflow-btn',
      title: 'More',
      textContent: '☰',
      'aria-expanded': 'false',
    });
    overflowBtn.style.marginLeft = '6px';
    // popup container (hidden by default)
    const overflowMenu = Object.assign(document.createElement('div'), {
      id: 'ytm-overflow-menu',
      style: 'position:absolute;right:12px;top:48px;background:rgba(10,10,10,0.95);border:1px solid rgba(255,255,255,0.06);border-radius:8px;padding:8px;display:none;z-index:10001;min-width:160px;box-shadow:0 8px 24px rgba(0,0,0,0.6)'
    });
    // build a placeholder list inside the menu where we will move optional controls
    const overflowList = document.createElement('div');
    overflowList.id = 'ytm-overflow-list';
    overflowMenu.appendChild(overflowList);
    // append overflow button to header controls (it will be shown only when needed)
    ctr.appendChild(overflowBtn);
    header.appendChild(overflowMenu);
  // resize buttons (inline with other header controls)
  const rwDec = Object.assign(document.createElement('button'), { className: 'ytm-mini-control', title: 'Decrease width (inverted)', textContent: '←' });
  const rwInc = Object.assign(document.createElement('button'), { className: 'ytm-mini-control', title: 'Increase width (inverted)', textContent: '→' });
  const rhDec = Object.assign(document.createElement('button'), { className: 'ytm-mini-control', title: 'Decrease height', textContent: '↑' });
  const rhInc = Object.assign(document.createElement('button'), { className: 'ytm-mini-control', title: 'Increase height', textContent: '↓' });
  const rReset = Object.assign(document.createElement('button'), { className: 'ytm-mini-control', title: 'Reset size', textContent: '⤢' });
    // romanize toggle handler
    romanBtn.onclick = () => {
      romanizeEnabled = !romanizeEnabled;
      romanBtn.setAttribute('aria-pressed', romanizeEnabled ? 'true' : 'false');
      if (romanizeEnabled) romanBtn.classList.add('active'); else romanBtn.classList.remove('active');
      romanBtn.title = romanizeEnabled ? 'Romanization: ON' : 'Romanization: OFF';
      gmSet('romanize_enabled', romanizeEnabled);
      // rebuild lines to show/hide romanized text
      buildLyrics();
    };
    // font size handlers
    const applyFontSize = (size) => {
      // allow larger font sizes (up to 48px)
      fontSize = Math.max(10, Math.min(48, Math.round(size)));
      gmSet('font_size', fontSize);
      try {
        const card = document.getElementById('ytm-lyrics-card');
        if (card) card.style.setProperty('--ytm-font-size', fontSize + 'px');
        // small reposition to ensure card fits
        if (card && card.classList.contains('active')) {
          const rect = document.getElementById('ytm-launcher')?.getBoundingClientRect();
          if (rect) {
            const computedRight = Math.max(8, Math.round(window.innerWidth - rect.right));
            const computedTop = Math.round(rect.bottom + 8);
            card.style.right = computedRight + 'px';
            card.style.top = Math.min(Math.max(8, computedTop), Math.max(8, window.innerHeight - (card.offsetHeight || 300) - 10)) + 'px';
          }
        }
      } catch (e) {}
    };
    decBtn.onclick = () => applyFontSize(fontSize - 1);
    incBtn.onclick = () => applyFontSize(fontSize + 1);
    // manual resize helper
    const applyManualResize = (dw = 0, dh = 0) => {
      try {
        const card = document.getElementById('ytm-lyrics-card');
        if (!card) return;
        const rect = card.getBoundingClientRect();
        const curW = rect.width;
        const curH = rect.height;
        const minW = 260;
        const minH = 120;
        const maxW = Math.max(300, window.innerWidth - 16);
        const maxH = Math.max(200, window.innerHeight - 16);
        const nw = Math.max(minW, Math.min(maxW, Math.round(curW + dw)));
        const nh = Math.max(minH, Math.min(maxH, Math.round(curH + dh)));
        card.style.width = nw + 'px';
        card.style.height = nh + 'px';
        try { gmSet('card_size', { w: nw, h: nh }); } catch (e) {}
      } catch (e) {}
    };
  // resize button handlers (adjust by pixels)
  // invert width button roles: left arrow increases, right arrow decreases
  rwDec.onclick = () => applyManualResize(40, 0);
  rwInc.onclick = () => applyManualResize(-40, 0);
    rhDec.onclick = () => applyManualResize(0, -30);
    rhInc.onclick = () => applyManualResize(0, 30);
    rReset.onclick = () => {
      try {
        const card = document.getElementById('ytm-lyrics-card');
        if (!card) return;
        card.style.width = '';
        card.style.height = '';
        gmSet('card_size', { w: null, h: null });
      } catch (e) {}
    };
    // reset offset handler
    resetOffsetBtn.onclick = () => {
      try {
        if (!currentSong) return;
        const id = (currentSong.title || '') + (currentSong.artist || '') + (currentSong.album || '');
  // store previous in-memory before clearing
  previousOffset = previousOffset ?? offsetSec;
  offsetSec = 0;
        updateOffsetLabel();
        // quick visual toast in header
        const fb = document.createElement('span');
        fb.className = 'ytm-sync-feedback';
        fb.textContent = 'offset reset';
        ctr.appendChild(fb);
        requestAnimationFrame(() => (fb.style.opacity = '1'));
        setTimeout(() => (fb.style.opacity = '0'), 1200);
        setTimeout(() => fb.remove(), 1600);
      } catch (e) {}
    };

    // undo handler
    undoOffsetBtn.onclick = () => {
      try {
        if (!currentSong) return;
        const id = (currentSong.title || '') + (currentSong.artist || '') + (currentSong.album || '');
        if (previousOffset === null) return;
  const curPrev = previousOffset;
  // consume previousOffset and clear it (in-memory only)
  previousOffset = null;
  offsetSec = curPrev;
        updateOffsetLabel();
        // feedback
        const fb = document.createElement('span');
        fb.className = 'ytm-sync-feedback';
        fb.textContent = `undo ${offsetSec >= 0 ? '+' : ''}${offsetSec}s`;
        ctr.appendChild(fb);
        requestAnimationFrame(() => (fb.style.opacity = '1'));
        setTimeout(() => (fb.style.opacity = '0'), 1200);
        setTimeout(() => fb.remove(), 1600);
      } catch (e) {}
    };
    // use top-level __ytm_translation_cache and translate()

    // translate-all handler (throttled, uses selected language)
    translateAllBtn.onclick = async () => {
      try {
        const content = document.getElementById('ytm-lyrics-content');
        if (!content) return;
        // gather lines
        const lines = Array.from(document.querySelectorAll('.ytm-lyric-line'));
        if (!lines.length) return;
        // disable button briefly
        translateAllBtn.disabled = true;
        const delay = (ms) => new Promise((r) => setTimeout(r, ms));
        const target = (document.getElementById('ytm-translate-lang')?.value) || 'en';
        for (let i = 0; i < lines.length; i++) {
          try {
            const l = lines[i];
            const idx = Number((l.id || '').replace('ytm-lyric-', ''));
            const text = (l.querySelector('span')?.textContent || '').trim();
            const outEl = document.getElementById(`ytm-trans-${idx}`);
            if (!outEl) continue;
            // show pending
            outEl.textContent = '…';
            // check cache
            const cacheKey = target + '|' + text;
            if (cacheKey in __ytm_translation_cache) {
              outEl.textContent = __ytm_translation_cache[cacheKey] || '';
              continue;
            }
            // call translator and throttle a bit to avoid hammering
            // eslint-disable-next-line no-await-in-loop
            const res = await translate(text, target);
            __ytm_translation_cache[cacheKey] = res || '';
            outEl.textContent = res || '';
            // throttle 180ms between calls
            // eslint-disable-next-line no-await-in-loop
            await delay(180);
          } catch (e) {
            try { const outEl = document.getElementById(`ytm-trans-${i}`); if (outEl) outEl.textContent = ''; } catch (e) {}
          }
        }
        translateAllBtn.disabled = false;
      } catch (e) { try { translateAllBtn.disabled = false; } catch (e) {} }
    };
    // overflow menu toggle
    overflowBtn.onclick = (e) => {
      try {
        e.stopPropagation();
        const open = overflowBtn.getAttribute('aria-expanded') === 'true';
        if (open) {
          overflowMenu.style.display = 'none';
          overflowBtn.setAttribute('aria-expanded', 'false');
        } else {
          // ensure overflow menu is visible and positioned within card bounds
          overflowMenu.style.display = 'block';
          overflowBtn.setAttribute('aria-expanded', 'true');
        }
      } catch (e) {}
    };
    // close overflow on outside click
    document.addEventListener('click', (ev) => {
      try {
        if (!overflowMenu || !overflowBtn) return;
        if (overflowBtn.contains(ev.target) || overflowMenu.contains(ev.target)) return;
        overflowMenu.style.display = 'none';
        overflowBtn.setAttribute('aria-expanded', 'false');
      } catch (e) {}
    });

    // helper to move controls into overflow list
    const moveToOverflow = (el) => {
      try {
        if (!el) return;
        // create a wrapper for the control inside the overflow list
        const wrapper = document.createElement('div');
        wrapper.style.margin = '6px 0';
        // clone node for safe placement (keep original hidden)
        const clone = el.cloneNode(true);
        clone.id = (clone.id || '') + '-overflow';
        wrapper.appendChild(clone);
        overflowList.appendChild(wrapper);
        // hide original in header controls
        el.style.display = 'none';
      } catch (e) {}
    };
    const restoreFromOverflow = () => {
      try {
        // show original controls
        Array.from(overflowList.children).forEach((w) => {
          try {
            const c = w.firstChild;
            if (!c) return;
            const origId = (c.id || '').replace('-overflow', '');
            const orig = document.getElementById(origId);
            if (orig) orig.style.display = '';
          } catch (e) {}
        });
        // clear overflowList
        overflowList.textContent = '';
      } catch (e) {}
    };

    // responsive toggle: when small, move less-critical controls into overflow
    const applyResponsiveOverflow = () => {
      try {
        const w = window.innerWidth;
        // threshold where we want a compact header
        const THRESH = 420;
        const controls = document.getElementById('ytm-lyrics-controls');
        if (!controls) return;
        if (w <= THRESH) {
          // mark compact class and move optional controls
          controls.classList.add('compact');
          // move translate select/input and translate-all into overflow
          moveToOverflow(document.getElementById('ytm-translate-lang'));
          moveToOverflow(document.getElementById('ytm-translate-lang-input'));
          moveToOverflow(document.getElementById('ytm-translate-all'));
          // also move reset/undo offset if very narrow
          moveToOverflow(document.getElementById('ytm-reset-offset'));
          moveToOverflow(document.getElementById('ytm-undo-offset'));
          // show the overflow button
          overflowBtn.style.display = '';
        } else {
          controls.classList.remove('compact');
          restoreFromOverflow();
          overflowBtn.style.display = 'none';
          overflowMenu.style.display = 'none';
          overflowBtn.setAttribute('aria-expanded', 'false');
        }
      } catch (e) {}
    };
    // run once to apply initial layout and on resize
    try { applyResponsiveOverflow(); } catch (e) {}
    window.addEventListener('resize', () => {
      try { applyResponsiveOverflow(); } catch (e) {}
    });
    // apply initial font size on the card so the variable cascades to romanized spans
  try { card.style.setProperty('--ytm-font-size', fontSize + 'px'); } catch (e) {}
  header.appendChild(info);
  header.appendChild(ctr);
  // build a single compact resize row placed under the header
  const resizeRow = Object.assign(document.createElement('div'), { id: 'ytm-resize-row' });
  // order: width-decrease (← increases due to inversion), width-increase (→ decreases), height-decrease (↑), height-increase (↓), reset
  resizeRow.appendChild(rwDec);
  resizeRow.appendChild(rwInc);
  resizeRow.appendChild(rhDec);
  resizeRow.appendChild(rhInc);
  resizeRow.appendChild(rReset);

    const content = document.createElement('div');
    content.id = 'ytm-lyrics-content';
    content.appendChild(
      Object.assign(document.createElement('div'), {
        className: 'ytm-lyric-line',
        textContent: '🎵 Loading lyrics...',
      })
    );
  card.appendChild(header);
  card.appendChild(resizeRow);
  card.appendChild(content);
    document.body.appendChild(card);
    containerEl = card;
    // restore persisted card size if available
    try {
      const sz = gmGet('card_size', null) || null;
      if (sz && sz.w) {
        // clamp persisted width to viewport so mobile isn't forced wider than screen
        const w = Math.max(260, Math.min(window.innerWidth - 16, Number(sz.w) || 260));
        card.style.width = w + 'px';
      }
      if (sz && sz.h) {
        const h = Math.max(120, Math.min(window.innerHeight - 16, Number(sz.h) || 300));
        card.style.height = h + 'px';
      }
    } catch (e) {}

    // dragging
    let dragging = false,
      ix = 0,
      iy = 0,
      xo = 0,
      yo = 0;
    header.addEventListener('mousedown', (e) => {
      ix = e.clientX - xo;
      iy = e.clientY - yo;
      if (e.target === header || e.target.closest('#ytm-song-title'))
        dragging = true;
    });
    document.addEventListener('mousemove', (e) => {
      if (!dragging) return;
      e.preventDefault();
      const cx = e.clientX - ix,
        cy = e.clientY - iy;
      xo = cx;
      yo = cy;
      card.style.transform = `translate3d(${cx}px, ${cy}px,0)`;
    });
    document.addEventListener('mouseup', () => (dragging = false));

    closeB.onclick = hide;
    minB.onclick = () => {
      const c = document.getElementById('ytm-lyrics-content');
      if (!c) return;
      if (c.style.display === 'none') {
        c.style.display = 'block';
        minB.textContent = '−';
        minB.title = 'Minimize';
      } else {
        c.style.display = 'none';
        minB.textContent = '+';
        minB.title = 'Expand';
      }
    };
    return card;
  }

  // --- Video ID extraction and stats fetching ---
  const __ytm_stats_cache = {};

  function formatNumber(n) {
    try { return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } catch (e) { return n; }
  }

  function getVideoIdFromPage() {
    try {
      // try canonical link
      const can = document.querySelector('link[rel="canonical"]')?.href || '';
      let m = can && can.match(/[?&]v=([^&]+)/);
      if (m && m[1]) return m[1];
      // try location
      m = window.location.href.match(/[?&]v=([^&]+)/);
      if (m && m[1]) return m[1];
      // find any anchor with a watch link
      const a = Array.from(document.querySelectorAll('a[href*="/watch?v="]')).find(Boolean);
      if (a) {
        m = a.href.match(/[?&]v=([^&]+)/);
        if (m && m[1]) return m[1];
      }
      // sometimes music.youtube contains data-watch-id attributes on thumbnails
      const thumb = document.querySelector('[data-watch-id]') || document.querySelector('[data-video-id]');
      if (thumb) return thumb.getAttribute('data-watch-id') || thumb.getAttribute('data-video-id') || null;
    } catch (e) {}
    return null;
  }

  function fetchYouTubeCounts(videoId) {
    return new Promise((resolve) => {
      if (!videoId) return resolve(null);
      if (__ytm_stats_cache[videoId]) return resolve(__ytm_stats_cache[videoId]);
      try {
        const url = 'https://www.youtube.com/youtubei/v1/player';
        const body = JSON.stringify({
          videoId: videoId,
          context: { client: { clientName: 'WEB', clientVersion: '2.20231101.00.00' } }
        });
        // helper: parse the player response for views/likes
        const parseCounts = (txt) => {
          try {
            const j = typeof txt === 'string' && txt.length ? JSON.parse(txt) : (txt || {});
            const views = j?.videoDetails?.viewCount || j?.videoDetails?.view_count || null;
            const likes = j?.microformat?.playerMicroformatRenderer?.likeCount || j?.videoDetails?.likes || null;
            if (views == null && likes == null) return null;
            return { views: views ? Number(views) : null, likes: likes ? Number(likes) : null };
          } catch (e) { return null; }
        };

        // Try gmXhr first (if available). If it fails or returns unusable JSON, try the pageFetch once.
        try {
          if (typeof gmXhr === 'function') {
            gmXhr({
              method: 'POST',
              url,
              headers: { 'Content-Type': 'application/json' },
              data: body,
              onload: (resp) => {
                const txt = resp?.responseText || '';
                const parsed = parseCounts(txt);
                if (parsed) {
                  __ytm_stats_cache[videoId] = parsed;
                  return resolve(parsed);
                }
                // fall back to pageFetch
                pageFetch(url, body).then((txt2) => {
                  const p2 = parseCounts(txt2);
                  if (p2) {
                    __ytm_stats_cache[videoId] = p2;
                    return resolve(p2);
                  }
                  return resolve(null);
                }).catch(() => resolve(null));
              },
              onerror: () => {
                pageFetch(url, body).then((txt2) => {
                  const p2 = parseCounts(txt2);
                  if (p2) {
                    __ytm_stats_cache[videoId] = p2;
                    return resolve(p2);
                  }
                  return resolve(null);
                }).catch(() => resolve(null));
              },
            });
            return;
          }
        } catch (e) {}

        // last attempt: try pageFetch directly
        pageFetch(url, body).then((txt) => {
          const parsed = parseCounts(txt);
          if (parsed) {
            __ytm_stats_cache[videoId] = parsed;
            return resolve(parsed);
          }
          return resolve(null);
        }).catch(() => resolve(null));
      } catch (e) { return resolve(null); }
    });
  }

  // Inject a small helper into the page that performs same-origin fetches and returns results via postMessage
  function ensurePageFetcherInjected() {
    try {
      if (window.__ytm_beautifier_page_fetcher_installed) return;
      const src = `(() => {
        if (window.__ytm_beautifier_page_fetcher_installed) return;
        window.__ytm_beautifier_page_fetcher_installed = true;
        window.addEventListener('message', async (ev) => {
          try {
            const d = ev.data || {};
            if (!d || d.source !== 'ytm-beautifier-page-fetch') return;
            const { id, url, body } = d;
            try {
              const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body });
              const txt = await res.text();
              window.postMessage({ source: 'ytm-beautifier-page-fetch-response', id, ok: true, text: txt }, '*');
            } catch (err) {
              window.postMessage({ source: 'ytm-beautifier-page-fetch-response', id, ok: false, error: String(err) }, '*');
            }
          } catch (e) {}
        }, false);
      })();`;
      const s = document.createElement('script');
      s.textContent = src;
      (document.head || document.documentElement).appendChild(s);
      s.parentNode && s.parentNode.removeChild(s);
      window.__ytm_beautifier_page_fetcher_installed = true;
    } catch (e) {}
  }

  function pageFetch(url, body) {
    return new Promise((resolve, reject) => {
      try {
        ensurePageFetcherInjected();
        const id = Math.random().toString(36).slice(2);
        const onmsg = (ev) => {
          try {
            const d = ev.data || {};
            if (!d || d.source !== 'ytm-beautifier-page-fetch-response' || d.id !== id) return;
            window.removeEventListener('message', onmsg);
            if (d.ok) return resolve(d.text || '');
            return reject(d.error || 'fetch-failed');
          } catch (e) { try { window.removeEventListener('message', onmsg); } catch (e) {} reject(e); }
        };
        window.addEventListener('message', onmsg, false);
        // send request to page
        window.postMessage({ source: 'ytm-beautifier-page-fetch', id, url, body }, '*');
        // timeout
        setTimeout(() => {
          try { window.removeEventListener('message', onmsg); } catch (e) {}
          reject('timeout');
        }, 6000);
      } catch (e) { reject(e); }
    });
  }

  async function updateSongStats(song) {
    try {
      const statsEl = document.getElementById('ytm-song-stats');
      if (!statsEl) return;
      statsEl.textContent = '';
      // discover video id from a few known places (canonical URL, location, anchors, data attributes)
      let vid = getVideoIdFromPage();
      if (!vid) {
        const anchors = Array.from(document.querySelectorAll('a[href]')).map((a) => a.href).filter(Boolean);
        for (const h of anchors) {
          try {
            const m = h.match(/[?&]v=([^&]+)/);
            if (m && m[1]) { vid = m[1]; break; }
          } catch (e) {}
        }
      }
      if (!vid) {
        // nothing we can do
        statsEl.textContent = '';
        return;
      }
  // fetch counts (best-effort)
  const res = await fetchYouTubeCounts(vid);
      // build safe DOM: counts area, link, id, refresh button, optional hint
      statsEl.textContent = '';
      // counts container (may be empty)
      const countsContainer = document.createElement('span');
      countsContainer.id = 'ytm-stats-counts';
      countsContainer.style.marginRight = '8px';
      if (res) {
        if (res.views != null) {
          const vspan = document.createElement('span');
          vspan.className = 'ytm-stat';
          vspan.textContent = `👁 ${formatNumber(res.views)}`;
          countsContainer.appendChild(vspan);
        }
        if (res.likes != null) {
          const lspan = document.createElement('span');
          lspan.className = 'ytm-stat';
          lspan.style.marginLeft = '8px';
          lspan.textContent = `👍 ${formatNumber(res.likes)}`;
          countsContainer.appendChild(lspan);
        }
      }
      statsEl.appendChild(countsContainer);

      // always include Open on YouTube link
      const a = document.createElement('a');
      a.href = `https://www.youtube.com/watch?v=${encodeURIComponent(vid)}`;
      a.target = '_blank';
      a.rel = 'noreferrer';
      a.textContent = 'Open on YouTube';
      statsEl.appendChild(a);

  // NOTE: intentionally not showing video ID to keep header compact

      // refresh button (always present)
      const refreshBtn = document.createElement('button');
  refreshBtn.id = 'ytm-refresh-stats';
  refreshBtn.className = 'ytm-mini-control ytm-refresh';
      refreshBtn.style.marginLeft = '8px';
      refreshBtn.style.padding = '4px 8px';
      refreshBtn.style.fontSize = '11px';
      refreshBtn.textContent = 'Refresh';
      refreshBtn.onclick = () => updateSongStats(song);
      statsEl.appendChild(refreshBtn);

      // optional hint when gmXhr unavailable and counts missing
      if (!res) {
        const hint = (typeof GM_xmlhttpRequest === 'undefined')
          ? ' (enable GM_xmlhttpRequest / cross-domain permission in your userscript manager to fetch counts)'
          : '';
        const hintDiv = document.createElement('div');
        hintDiv.style.fontSize = '11px';
        hintDiv.style.opacity = '.75';
        hintDiv.style.marginTop = '4px';
        hintDiv.textContent = hint;
        statsEl.appendChild(hintDiv);
      }
    } catch (e) {}
  }

  function createLauncher() {
    if (document.getElementById('ytm-launcher')) return;
    const btn = document.createElement('button');
    btn.id = 'ytm-launcher';
    btn.title = 'Show lyrics';
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('width', '18');
    svg.setAttribute('height', '18');
    svg.setAttribute('viewBox', '0 0 24 24');
    const p1 = document.createElementNS(svg.namespaceURI, 'path');
    p1.setAttribute('d', 'M4 6H20V18H8L4 22V6Z');
    p1.setAttribute('fill', 'white');
    p1.setAttribute('opacity', '0.95');
    const p2 = document.createElementNS(svg.namespaceURI, 'path');
    p2.setAttribute('d', 'M7 9H17');
    p2.setAttribute('stroke', 'rgba(0,0,0,0.12)');
    p2.setAttribute('stroke-width', '1.5');
    p2.setAttribute('stroke-linecap', 'round');
    svg.appendChild(p1);
    svg.appendChild(p2);
    const span = document.createElement('span');
    span.textContent = 'Lyrics';
    btn.appendChild(svg);
    btn.appendChild(span);
    btn.onclick = () => {
      const card = document.getElementById('ytm-lyrics-card');
      if (card && card.classList.contains('active')) hide();
      else show();
    };
    document.body.appendChild(btn);
    return btn;
  }

  function buildLyrics() {
    const content = document.getElementById('ytm-lyrics-content');
    if (!content) return;
    content.textContent = '';
    for (let i = 0; i < 3; i++)
      content.appendChild(document.createElement('div'));
    lyrics.forEach((l, i) => {
      const d = document.createElement('div');
      d.className = 'ytm-lyric-line';
      d.id = `ytm-lyric-${i}`;
      // line container: text + small sync button
      const span = document.createElement('span');
      span.textContent = l;
      d.appendChild(span);
      const syncBtn = Object.assign(document.createElement('button'), { className: 'ytm-mini-control', id: `ytm-sync-${i}`, title: 'Set this line as current', textContent: '⤴' });
      syncBtn.style.marginLeft = '8px';
      syncBtn.style.minWidth = '30px';
      syncBtn.onclick = (ev) => {
        ev.stopPropagation();
        try {
          const song = currentSong || nowPlaying();
          if (!song) return;
          const id = (song.title || '') + (song.artist || '') + (song.album || '');
          const targetSec = times[i] || 0;
          // we want the clicked subtitle to be displayed now, so offset = currentMediaTime - targetSec
          const { cur: curRaw, curSource } = getPlaybackTime();
          const cur = Math.floor(curRaw || 0);
          const newOffset = cur - targetSec;
          logDebug('syncBtn clicked', { cur, curSource, targetSec, newOffset, previousOffsetBefore: previousOffset, songTitle: song.title });
          // save previous offset in-memory and apply new offset (do not persist)
          try { previousOffset = previousOffset ?? offsetSec; } catch (e) { previousOffset = offsetSec; }
          offsetSec = newOffset;
          updateOffsetLabel();
          // visual feedback next to the button
          const fb = document.createElement('span');
          fb.className = 'ytm-sync-feedback';
          fb.textContent = `offset ${newOffset >= 0 ? '+' : ''}${newOffset}s`;
          syncBtn.parentNode && syncBtn.parentNode.appendChild(fb);
          requestAnimationFrame(() => (fb.style.opacity = '1'));
          setTimeout(() => (fb.style.opacity = '0'), 1200);
          setTimeout(() => fb.remove(), 1600);
          // also update UI highlight immediately
          updateLyricsDisplay((song && song.elapsed ? Math.floor(song.elapsed) : cur) - offsetSec);
        } catch (e) {}
      };
      d.appendChild(syncBtn);
      // translate button (per-line)
  const langLabel = (document.getElementById('ytm-translate-lang')?.value || 'en');
  const trLabel = langLabel === 'auto' ? 'T→Auto' : `T→${(langLabel.length > 2 ? langLabel : langLabel.toUpperCase())}`;
  const trBtn = Object.assign(document.createElement('button'), { className: 'ytm-mini-control', id: `ytm-tr-${i}`, title: `Translate to ${langLabel}`, textContent: trLabel });
      trBtn.style.marginLeft = '6px';
      trBtn.onclick = (ev) => {
        ev.stopPropagation();
        try {
          const el = document.getElementById(`ytm-trans-${i}`);
          if (!el) return;
          el.textContent = '…';
          const target = (document.getElementById('ytm-translate-lang')?.value) || 'en';
          const cacheKey = target + '|' + l;
          if (cacheKey in __ytm_translation_cache) {
            el.textContent = __ytm_translation_cache[cacheKey] || '';
          } else {
            translate(l, target).then((res) => {
              __ytm_translation_cache[cacheKey] = res || '';
              if (!res) el.textContent = '';
              else el.textContent = res;
            }).catch(() => (el.textContent = ''));
          }
        } catch (e) {}
      };
      d.appendChild(trBtn);
      d.title = `Click to seek to: ${pad(times[i] || 0)}`;
      d.onclick = () => seekTo(i);
      // if romanization enabled, append a small placeholder span for romanized text
      if (romanizeEnabled) {
        const r = document.createElement('span');
        r.className = 'ytm-romanized';
        r.id = `ytm-roman-${i}`;
        r.textContent = '…';
        d.appendChild(r);
        // request romanization (debounced per-line via setTimeout minimal)
        (function(idx, text) {
          setTimeout(() => {
            romanize(text, 'auto', { skip_if_identical: true }).then((res) => {
              const el = document.getElementById(`ytm-roman-${idx}`);
              if (!el) return;
              el.textContent = res.romanized_text || '';
              if (!res.romanized_text) el.style.display = 'none';
            }).catch(() => {
              const el = document.getElementById(`ytm-roman-${idx}`);
              if (el) el.style.display = 'none';
            });
          }, 60);
        })(i, l);
      }
  // translated text placeholder
  const tspan = document.createElement('span');
  tspan.className = 'ytm-translated';
  tspan.id = `ytm-trans-${i}`;
  tspan.textContent = '';
  d.appendChild(tspan);
      content.appendChild(d);
    });
    for (let i = 0; i < 3; i++)
      content.appendChild(document.createElement('div'));
    currentIndex = 0;
  }

  // --- Seeking helpers (attempts in order) ---
  function seekTo(i) {
    if (times[i] === undefined) return;
    const el = document.getElementById(`ytm-lyric-${i}`);
    if (el) {
      el.classList.add('seeking');
      setTimeout(() => el.classList.remove('seeking'), 500);
    }
    // prefer seeking method based on where we read the current playback time from
    try {
      const { curSource } = getPlaybackTime();
      simulateSeek(times[i], curSource);
    } catch (e) {
      simulateSeek(times[i]);
    }
    currentIndex = i;
    document
      .querySelectorAll('.ytm-lyric-line')
      .forEach((n) => n.classList.remove('active'));
    if (el) {
      el.classList.add('active');
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
  }

  // determine current playback time and its source (media element, player-bar DOM, or song.elapsed)
  function getPlaybackTime() {
    let cur = 0;
    let curSource = 'none';
    try {
      // 1) try visible media element
  const media = getVisibleMediaElement();
      if (media && !isNaN(media.currentTime)) {
        cur = Math.floor(media.currentTime || 0);
        curSource = 'media';
        return { cur, curSource };
      }
    } catch (e) {}
    try {
      // 2) try parsing the player-bar time DOM directly (more immediate)
      const left = document.querySelector('ytmusic-player-bar .left-controls');
      const timeStr = left?.querySelector('span.time-info.ytmusic-player-bar')?.innerHTML?.trim();
      if (timeStr) {
        const [elapsedStr] = (timeStr.split(' / ') || []);
        const parsed = toSec(elapsedStr || '0:00');
        if (!isNaN(parsed)) {
          cur = Math.floor(parsed || 0);
          curSource = 'player-bar-dom';
          return { cur, curSource };
        }
      }
    } catch (e) {}
    try {
      // 3) fallback to song.elapsed from nowPlaying
      const song = nowPlaying();
      if (song && song.elapsed) {
        cur = Math.floor(song.elapsed || 0);
        curSource = 'song.elapsed';
        return { cur, curSource };
      }
    } catch (e) {}
    return { cur: 0, curSource };
  }

  function simulateSeek(target, preferredSource) {
    const song = nowPlaying();
    if (!song) return;
    const curElapsed = song.elapsed || 0;
    const diff = target - curElapsed;
    if (Math.abs(diff) < 0.8) return;
    // decide attempt order based on preferredSource
    logDebug('simulateSeek: target', target, 'preferredSource', preferredSource, 'diff', diff);
    // Prefer native/media/video/app seeks first (they are generally more precise).
    // Use the progress/slider approach only as a last resort because it can be
    // imprecise due to UI rounding or different coordinate mapping.
    const attempts = ['media', 'video', 'app', 'progress'];
    for (const at of attempts) {
      try {
        logDebug('simulateSeek: trying', at);
        if (at === 'media' && tryMediaSeek(target)) return;
        if (at === 'app' && tryAppAPI(target)) return;
        if (at === 'progress' && tryProgressSeek(target, song.total)) return;
        if (at === 'video' && tryVideoSeek(target)) return;
      } catch (e) {}
    }
    // final fallback: keyboard
    keyboardFallback(diff);
  }

  function tryMediaSeek(target) {
    try {
      const media = getVisibleMediaElement();
      if (!media) return false;
      try {
        // Try a direct set on the media element. Some players/containers may
        // snap the requested time to a previous keyframe/segment which can
        // make the effective seek land a little earlier (commonly ~1-3s).
        // Set it once, then verify shortly after and reapply if necessary.
        media.currentTime = target;
      } catch (e) {}
      ['seeking', 'timeupdate', 'seeked'].forEach((evt) =>
        media.dispatchEvent(new Event(evt))
      );
      // brief verification: if the actual currentTime is still noticeably
      // earlier than requested, try setting it again once (handles keyframe
      // snapping or player-side adjustments)
      try {
        setTimeout(() => {
          try {
            const actual = Math.floor(media.currentTime || 0);
            if (actual < Math.floor(target) - 1) {
              try {
                media.currentTime = target;
              } catch (e) {}
            }
          } catch (e) {}
        }, 180);
      } catch (e) {}
      return true;
    } catch (e) {
      return false;
    }
  }

  function tryAppAPI(target) {
    try {
      const app = document.querySelector('ytmusic-app');
      const cands = [
        app?.playerApi_,
        app?.player_,
        app?.playerApi,
        app?.appContext_?.playerApi,
        window.ytplayer,
        window.yt?.player,
      ];
      for (const api of cands) {
        if (!api) continue;
        if (typeof api.seekTo === 'function') {
          try {
            api.seekTo(target);
            return true;
          } catch (e) {}
        }
        if (typeof api.setCurrentTime === 'function') {
          try {
            api.setCurrentTime(target);
            return true;
          } catch (e) {}
        }
        if (api.player && typeof api.player.seekTo === 'function') {
          try {
            api.player.seekTo(target);
            return true;
          } catch (e) {}
        }
      }
    } catch (e) {}
    return false;
  }

  function tryProgressSeek(target, total) {
    if (!total || total <= 0) return false;
    const pct = Math.max(0, Math.min(1, target / total));
    const tried = [];
    // helper to attempt manipulating a slider-like element
    const attemptOnEl = (el) => {
      if (!el) return false;
      tried.push(el);
      try {
        logDebug('tryProgressSeek: attempting on element', el);
        const v = pct * 100;
        // set common properties
        if (el.value !== undefined) {
          el.value = v;
          el.setAttribute && el.setAttribute('value', String(v));
          el.dispatchEvent && el.dispatchEvent(new Event('input', { bubbles: true }));
          el.dispatchEvent && el.dispatchEvent(new Event('change', { bubbles: true }));
          if (el.value == v) return true;
        }
        if (typeof el._setValue === 'function') {
          el._setValue(v);
          return true;
        }
        if (el.immediateValue !== undefined) {
          el.immediateValue = v;
          el.value = v;
          el.dispatchEvent && el.dispatchEvent(new Event('input', { bubbles: true }));
          return true;
        }
        // aria updates
        try { el.setAttribute && el.setAttribute('aria-valuenow', String(v)); } catch (e) {}
        // fallback: try click at computed position using elementFromPoint
        try {
          const r = el.getBoundingClientRect();
          const x = Math.round(r.left + r.width * pct);
          const y = Math.round(r.top + Math.max(2, r.height / 2));
          const elAt = document.elementFromPoint(x, y) || el;
          const dispatchPointer = (type) => {
            const ev = new PointerEvent(type, {
              bubbles: true,
              cancelable: true,
              clientX: x,
              clientY: y,
              isPrimary: true,
              pointerId: 1,
            });
            (elAt || el).dispatchEvent(ev);
          };
          ['pointerdown', 'pointermove', 'pointerup', 'click'].forEach((t) => dispatchPointer(t));
          return true;
        } catch (e) {}
      } catch (e) {}
      return false;
    };

    // search DOM and simple shadow roots for slider controls
    const candidates = [];
    // common selectors
    ['#progress-bar', '.progress-bar', 'tp-yt-paper-slider#progress-bar', '.ytmusic-player-bar #progress-bar', '[role="slider"]'].forEach((s) => {
      try { document.querySelectorAll(s).forEach((n) => candidates.push(n)); } catch (e) {}
    });
    // search inside ytmusic-player-bar shadow root if present
    try {
      const bar = document.querySelector('ytmusic-player-bar');
      if (bar && bar.shadowRoot) {
        bar.shadowRoot.querySelectorAll('[role="slider"]').forEach((n) => candidates.push(n));
        bar.shadowRoot.querySelectorAll('tp-yt-paper-slider').forEach((n) => candidates.push(n));
      }
    } catch (e) {}
    // search inside ytmusic-app's shadow root
    try {
      const app = document.querySelector('ytmusic-app');
      if (app && app.shadowRoot) {
        app.shadowRoot.querySelectorAll('[role="slider"]').forEach((n) => candidates.push(n));
        app.shadowRoot.querySelectorAll('tp-yt-paper-slider').forEach((n) => candidates.push(n));
      }
    } catch (e) {}

    // unique candidates
    const uniq = Array.from(new Set(candidates.filter(Boolean)));
    for (const el of uniq) if (attemptOnEl(el)) return true;

    // finally, probe element at expected progress x coordinate on the player bar area
    try {
      const playerBar = document.querySelector('ytmusic-player-bar');
      const rect = playerBar ? playerBar.getBoundingClientRect() : null;
      if (rect) {
        const x = Math.round(rect.left + rect.width * pct);
        const y = Math.round(rect.top + rect.height / 2);
        const elAt = document.elementFromPoint(x, y);
        if (elAt && attemptOnEl(elAt)) return true;
      }
    } catch (e) {}

    // if nothing worked
    logDebug('tryProgressSeek: failed after trying', tried.length, 'elements');
    return false;
  }

  function tryVideoSeek(target) {
    try {
      const videos = document.querySelectorAll('video');
      for (const v of videos) {
        if (v && !isNaN(v.duration) && v.duration > 0) {
          try {
            v.currentTime = target;
            v.dispatchEvent(new Event('timeupdate'));
            v.dispatchEvent(new Event('seeked'));
            return true;
          } catch (e) {}
        }
      }
      // try youtube API fallbacks
      const players = document.querySelectorAll('[data-player-name]');
      for (const p of players)
        if (typeof p.seekTo === 'function') {
          try {
            p.seekTo(target);
            return true;
          } catch (e) {}
        }
      if (window.ytplayer?.seekTo) {
        try {
          window.ytplayer.seekTo(target);
          return true;
        } catch (e) {}
      }
    } catch (e) {}
    return false;
  }

  function keyboardFallback(diff) {
    const interval = 10,
      count = Math.min(10, Math.floor(Math.abs(diff) / interval));
    if (count === 0) return;
    const key = diff > 0 ? 'ArrowRight' : 'ArrowLeft';
    const target =
      document.querySelector('ytmusic-player-bar') || document.body;
    for (let i = 0; i < count; i++)
      setTimeout(
        () =>
          target.dispatchEvent(
            new KeyboardEvent('keydown', {
              key,
              code: key,
              bubbles: true,
              cancelable: true,
            })
          ),
        i * 100
      );
  }

  // --- Lyrics update & media listener attachment ---
  function updateLyricsDisplay(sec) {
    if (!times.length) return;
    const lines = document.querySelectorAll('.ytm-lyric-line');
    let idx = 0;
    for (let i = 0; i < times.length; i++) {
      if (sec >= times[i]) idx = i;
      else break;
    }
    if (idx !== currentIndex) {
      lines.forEach((l) => l.classList.remove('active'));
      if (lines[idx]) {
        lines[idx].classList.add('active');
        lines[idx].scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
    currentIndex = idx;
  }

  function attachMediaListeners() {
    try {
      const media = getVisibleMediaElement();
      if (media === attachedMedia) return;
      if (attachedMedia)
        try {
          attachedMedia.removeEventListener('timeupdate', onMediaTime);
          attachedMedia.removeEventListener('seeked', onMediaSeek);
        } catch (e) {}
      attachedMedia = media;
      if (!attachedMedia) return;
      attachedMedia.addEventListener('timeupdate', onMediaTime);
      attachedMedia.addEventListener('seeked', onMediaSeek);
    } catch (e) {}
  }
  function onMediaTime(e) {
    try {
      const t = Math.floor(e.target.currentTime || 0);
      updateLyricsDisplay(t - offsetSec);
      if (currentSong) currentSong.elapsed = t;
      updateOffsetLabel();
    } catch (e) {}
  }
  function onMediaSeek(e) {
    try {
      const t = Math.floor(e.target.currentTime || 0);
      currentIndex = 0;
      updateLyricsDisplay(t - offsetSec);
      if (currentSong) currentSong.elapsed = t;
    } catch (e) {}
  }

  function updateOffsetLabel() {
    try {
      const lbl = document.getElementById('ytm-offset-label');
      if (!lbl) return;
      if (!currentSong) { lbl.textContent = ''; return; }
      const id = (currentSong.title || '') + (currentSong.artist || '') + (currentSong.album || '');
  const val = offsetSec || 0;
      lbl.textContent = `offset: ${val >= 0 ? '+' : ''}${val}s`;
    } catch (e) {}
  }

  // --- UI show/hide/update ---
  function show() {
    if (!containerEl) buildLyricsCard();
    const launcher = document.getElementById('ytm-launcher');
    if (launcher) {
      launcher.classList.add('active');
      launcher.setAttribute('aria-pressed', 'true');
    }
    if (launcher && containerEl) {
      const rect = launcher.getBoundingClientRect(),
        margin = 8;
      containerEl.classList.add('active');
      containerEl.style.transform = 'none';
      const computedRight = Math.max(
        8,
        Math.round(window.innerWidth - rect.right)
      );
      let computedTop = Math.round(rect.bottom + margin);
      const cardH = containerEl.offsetHeight || 300,
        maxTop = Math.max(8, window.innerHeight - cardH - 10);
      if (computedTop > maxTop) computedTop = maxTop;
      containerEl.style.top = computedTop + 'px';
      containerEl.style.right = computedRight + 'px';
    } else containerEl && containerEl.classList.add('active');
    const song = nowPlaying();
    if (song) updateUI(song);
  }
  function hide() {
    containerEl && containerEl.classList.remove('active');
    const launcher = document.getElementById('ytm-launcher');
    if (launcher) {
      launcher.classList.remove('active');
      launcher.setAttribute('aria-pressed', 'false');
    }
  }

  function updateUI(song) {
    if (!song) return;
    const titleEl = document.getElementById('ytm-song-title'),
      artistEl = document.getElementById('ytm-song-artist');
    if (titleEl) titleEl.textContent = song.title || 'Unknown Title';
    if (artistEl) artistEl.textContent = song.artist || 'Unknown Artist';
    const adjusted = (song.elapsed || 0) - offsetSec;
    updateLyricsDisplay(adjusted);
    const id = (song.title || '') + (song.artist || '') + (song.album || '');
    if (
      (currentSong?.title || '') +
        (currentSong?.artist || '') +
        (currentSong?.album || '') !==
      id
    ) {
      currentSong = song;
      lyrics = [];
      times = [];
      fetchLyrics(song.title, song.artist, song.album, song.date);
      // update views/likes for the new song (best-effort)
      try { updateSongStats(song); } catch (e) {}
      // clear offsets on new song (do not persist per-song offsets anymore)
      previousOffset = null;
      offsetSec = 0;
      try {
        // clear translation cache if present
        if (typeof __ytm_translation_cache !== 'undefined') {
          for (const k in __ytm_translation_cache) delete __ytm_translation_cache[k];
        }
      } catch (e) {}
      // update label for this song
      updateOffsetLabel();
    }
  }

  // --- Monitor & init ---
  function monitor() {
    const s = nowPlaying();
    if (s && containerEl && containerEl.classList.contains('active'))
      updateUI(s);
    attachMediaListeners();
  }

  function init() {
    createLauncher();
    setInterval(monitor, 2000);
    const bar = document.querySelector('ytmusic-player-bar');
    if (bar) {
      let to;
      new MutationObserver(() => {
        clearTimeout(to);
        to = setTimeout(() => {
          monitor();
        }, 500);
      }).observe(bar, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['aria-label', 'src'],
      });
    }
    attachMediaListeners();
  }

  if (document.readyState === 'loading')
    document.addEventListener('DOMContentLoaded', init);
  else setTimeout(init, 1000);

  // keyboard
  document.addEventListener('keydown', (e) => {
    if (!containerEl || !containerEl.classList.contains('active')) return;
    if (e.key === 'Escape') hide();
    if ((e.key === 'l' || e.key === 'L') && (e.ctrlKey || e.metaKey)) {
      e.preventDefault();
      show();
    }
  });

  // exposed API for debugging/testing
  window.ytmBeautifier = {
    show,
    hide,
    getNowPlaying: nowPlaying,
    getSongLyrics: fetchLyrics,
    romanize: (text, source_language = 'auto', options = { skip_if_identical: true }) => romanize(text, source_language, options),
    reattachMedia: () => {
      attachMediaListeners();
      console.log('reattachMedia called');
    },
    setDebug: (v) => { try { window._ytm_debug = !!v; console.log('ytm debug set to', !!v); } catch (e) {} },
    debugSeek: (target) => {
      console.log('Target', target);
      [
        '#progress-bar',
        '.progress-bar',
        'tp-yt-paper-slider#progress-bar',
        '.ytmusic-player-bar #progress-bar',
        '[role="slider"]',
      ].forEach((s) => console.log(s, document.querySelector(s)));
      document
        .querySelectorAll('video')
        .forEach((v, i) => console.log(i, v.currentTime, v.duration, v.paused));
      console.log(
        'ytplayer',
        window.ytplayer,
        'ytmusic-app',
        document.querySelector('ytmusic-app')
      );
      console.log('song', nowPlaying());
    },
    testSeek: (t = 60) => {
      console.log('Testing seek to', t);
      const s = nowPlaying();
      if (s) {
        tryProgressSeek(t, s.total);
        setTimeout(() => tryVideoSeek(t), 800);
        setTimeout(() => {
          try {
            document
              .querySelector('tp-yt-paper-slider')
              ?.setAttribute('value', ((t / s.total) * 100).toString());
          } catch (e) {}
        }, 1600);
      }
    },
  };
})();