YouTube Music Beautifier

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
      }
    },
  };
})();