@uimaxbai/am-lyrics 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/react.js CHANGED
@@ -322,7 +322,7 @@ class GoogleService {
322
322
  }
323
323
  }
324
324
 
325
- const VERSION = '1.3.0';
325
+ const VERSION = '1.4.1';
326
326
  const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
327
327
  const FETCH_TIMEOUT_MS = 8000; // Timeout for all lyrics fetch requests
328
328
  const SEEK_THRESHOLD_MS = 500;
@@ -345,32 +345,17 @@ function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) {
345
345
  }
346
346
  const KPOE_SERVERS = [
347
347
  'https://lyricsplus.binimum.org',
348
- 'https://lyricsplus.atomix.one',
349
348
  'https://lyricsplus-seven.vercel.app',
350
349
  'https://lyricsplus.prjktla.workers.dev',
351
350
  'https://lyrics-plus-backend.vercel.app',
352
351
  ];
353
352
  const DEFAULT_KPOE_SOURCE_ORDER = 'apple,lyricsplus,musixmatch,spotify,qq,deezer,musixmatch-word';
354
- const TIDAL_SERVERS = [
355
- 'https://arran.monochrome.tf',
356
- 'https://api.monochrome.tf/',
357
- 'https://triton.squid.wtf',
358
- 'https://wolf.qqdl.site',
359
- 'https://maus.qqdl.site',
360
- 'https://vogel.qqdl.site',
361
- 'https://katze.qqdl.site',
362
- 'https://hund.qqdl.site',
363
- 'https://tidal.kinoplus.online',
364
- 'https://hifi-one.spotisaver.net',
365
- 'https://hifi-two.spotisaver.net',
366
- ];
367
353
  const GENIUS_WORKER_URL = 'https://fetch-genius.samidy.workers.dev/';
368
354
  let AmLyrics$1 = class AmLyrics extends i {
369
355
  constructor() {
370
356
  super(...arguments);
371
357
  this.downloadFormat = 'auto';
372
358
  this.highlightColor = '#ffffff';
373
- this.hoverBackgroundColor = 'rgba(255, 255, 255, 0.13)';
374
359
  this.autoScroll = true;
375
360
  this.interpolate = true;
376
361
  this.showRomanization = false;
@@ -415,6 +400,10 @@ let AmLyrics$1 = class AmLyrics extends i {
415
400
  // Syllable animation tracking
416
401
  this.lastActiveIndex = 0;
417
402
  this.visibleLineIds = new Set();
403
+ // Cached element tracking to avoid repeated querySelectorAll calls
404
+ this.preActiveLineElements = [];
405
+ this.positionedLineElements = [];
406
+ this.activeGapLineElements = [];
418
407
  // Bound handler references for proper event listener removal
419
408
  this._boundHandleUserScroll = this.handleUserScroll.bind(this);
420
409
  this._boundAnimateProgress = this.animateProgress.bind(this);
@@ -487,6 +476,31 @@ let AmLyrics$1 = class AmLyrics extends i {
487
476
  }
488
477
  set currentTime(value) {
489
478
  const oldValue = this._currentTime;
479
+ // If the new time is significantly smaller than the old time (e.g. song looped)
480
+ if (value < oldValue && oldValue - value > 1000 && this.lyrics) {
481
+ this.activeLineIndices = [];
482
+ this.activeMainWordIndices.clear();
483
+ this.activeBackgroundWordIndices.clear();
484
+ this.mainWordProgress.clear();
485
+ this.backgroundWordProgress.clear();
486
+ this.mainWordAnimations.clear();
487
+ this.backgroundWordAnimations.clear();
488
+ this.preActiveLineElements = [];
489
+ this.positionedLineElements = [];
490
+ this.activeGapLineElements = [];
491
+ // Stop all running animations and clear highlights immediately
492
+ if (this.lyricsContainer) {
493
+ const activeLines = this.lyricsContainer.querySelectorAll('.lyrics-line.active, .lyrics-line.pre-active');
494
+ activeLines.forEach(line => {
495
+ line.classList.remove('active', 'pre-active');
496
+ AmLyrics.resetSyllables(line);
497
+ });
498
+ const activeGaps = this.lyricsContainer.querySelectorAll('.lyrics-gap.active, .lyrics-gap.gap-exiting');
499
+ activeGaps.forEach(gap => gap.classList.remove('active', 'gap-exiting'));
500
+ // Reset gap cache since we manually messed with the elements
501
+ this.gapElementCache.clear();
502
+ }
503
+ }
490
504
  this._currentTime = value;
491
505
  if (oldValue !== value && this.lyrics) {
492
506
  this._onTimeChanged(oldValue, value);
@@ -548,6 +562,9 @@ let AmLyrics$1 = class AmLyrics extends i {
548
562
  this.lyricsContainer.removeEventListener('wheel', this._boundHandleUserScroll);
549
563
  this.lyricsContainer.removeEventListener('touchmove', this._boundHandleUserScroll);
550
564
  }
565
+ this.preActiveLineElements = [];
566
+ this.positionedLineElements = [];
567
+ this.activeGapLineElements = [];
551
568
  }
552
569
  async fetchLyrics() {
553
570
  // Cancel any in-flight fetch to prevent stale results from racing
@@ -582,16 +599,7 @@ let AmLyrics$1 = class AmLyrics extends i {
582
599
  }
583
600
  }
584
601
  if (collectedSources.length === 0 && resolvedMetadata?.metadata) {
585
- const tidalResult = await AmLyrics.fetchLyricsFromTidal(resolvedMetadata.metadata, resolvedMetadata.catalogIsrc);
586
- if (tidalResult && tidalResult.lines.length > 0) {
587
- collectedSources.push({
588
- lines: tidalResult.lines,
589
- source: 'Tidal',
590
- });
591
- }
592
- }
593
- // Fallback: LRCLIB
594
- if (collectedSources.length === 0 && resolvedMetadata?.metadata) {
602
+ // Fallback: LRCLIB
595
603
  const lrclibResult = await AmLyrics.fetchLyricsFromLrclib(resolvedMetadata.metadata);
596
604
  if (lrclibResult && lrclibResult.lines.length > 0) {
597
605
  collectedSources.push({
@@ -611,15 +619,17 @@ let AmLyrics$1 = class AmLyrics extends i {
611
619
  }
612
620
  this.hasFetchedAllProviders =
613
621
  collectedSources.length === 0 ||
614
- collectedSources.some(s => s.source === 'LRCLIB' ||
615
- s.source === 'Tidal' ||
616
- s.source === 'Genius');
622
+ collectedSources.some(s => s.source === 'LRCLIB' || s.source === 'Genius');
617
623
  this._updateFooter();
618
624
  if (collectedSources.length > 0) {
619
625
  this.availableSources = AmLyrics.mergeAndSortSources(collectedSources);
620
626
  this.currentSourceIndex = 0;
621
- this.lyrics = this.availableSources[0].lines;
622
- this.lyricsSource = this.availableSources[0].source;
627
+ const sourceResult = this.availableSources[0];
628
+ this.lyrics = sourceResult.lines;
629
+ this.lyricsSource = sourceResult.source;
630
+ if (sourceResult.songwriters) {
631
+ this.songwriters = sourceResult.songwriters;
632
+ }
623
633
  await this.onLyricsLoaded();
624
634
  return;
625
635
  }
@@ -641,6 +651,9 @@ let AmLyrics$1 = class AmLyrics extends i {
641
651
  this.backgroundWordProgress.clear();
642
652
  this.mainWordAnimations.clear();
643
653
  this.backgroundWordAnimations.clear();
654
+ this.preActiveLineElements = [];
655
+ this.positionedLineElements = [];
656
+ this.activeGapLineElements = [];
644
657
  if (this.lyricsContainer) {
645
658
  this.isProgrammaticScroll = true;
646
659
  this.lyricsContainer.scrollTop = 0;
@@ -670,36 +683,30 @@ let AmLyrics$1 = class AmLyrics extends i {
670
683
  return 2;
671
684
  if (lower.includes('musixmatch') && hasWordSync)
672
685
  return 3;
673
- if (lower.includes('tidal') && hasWordSync)
674
- return 4;
675
686
  if (lower.includes('lrclib') && hasWordSync)
676
- return 5;
687
+ return 4;
677
688
  if (hasWordSync)
678
- return 6;
689
+ return 5;
679
690
  if (lower.includes('apple') && !hasWordSync && !isUnsynced)
680
- return 7;
691
+ return 6;
681
692
  if (isQQ && !hasWordSync && !isUnsynced)
682
- return 8;
693
+ return 7;
683
694
  if (lower.includes('musixmatch') && !hasWordSync && !isUnsynced)
684
- return 9;
685
- if (lower.includes('tidal') && !hasWordSync && !isUnsynced)
686
- return 10;
695
+ return 8;
687
696
  if (lower.includes('lrclib') && !hasWordSync && !isUnsynced)
688
- return 11;
697
+ return 9;
689
698
  if (!hasWordSync && !isUnsynced)
690
- return 12;
699
+ return 10;
691
700
  if (lower.includes('apple') && isUnsynced)
692
- return 13;
701
+ return 11;
693
702
  if (isQQ && isUnsynced)
694
- return 14;
703
+ return 12;
695
704
  if (lower.includes('musixmatch') && isUnsynced)
696
- return 15;
697
- if (lower.includes('tidal') && isUnsynced)
698
- return 16;
705
+ return 13;
699
706
  if (lower.includes('lrclib') && isUnsynced)
700
- return 17;
707
+ return 14;
701
708
  if (lower.includes('genius'))
702
- return 18;
709
+ return 15;
703
710
  return 20;
704
711
  }
705
712
  static mergeAndSortSources(collectedSources) {
@@ -730,13 +737,6 @@ let AmLyrics$1 = class AmLyrics extends i {
730
737
  const resolvedMetadata = await this.resolveSongMetadata();
731
738
  if (resolvedMetadata?.metadata) {
732
739
  const newSources = [];
733
- // Try Tidal if not fetched
734
- if (!this.availableSources.some(s => s.source.toLowerCase().includes('tidal'))) {
735
- const tidalResult = await AmLyrics.fetchLyricsFromTidal(resolvedMetadata.metadata, resolvedMetadata.catalogIsrc);
736
- if (tidalResult && tidalResult.lines.length > 0) {
737
- newSources.push({ lines: tidalResult.lines, source: 'Tidal' });
738
- }
739
- }
740
740
  // Try LRCLIB if not fetched
741
741
  if (!this.availableSources.some(s => s.source.toLowerCase().includes('lrclib'))) {
742
742
  const lrclibResult = await AmLyrics.fetchLyricsFromLrclib(resolvedMetadata.metadata);
@@ -771,8 +771,12 @@ let AmLyrics$1 = class AmLyrics extends i {
771
771
  if (this.availableSources.length > 1) {
772
772
  this.currentSourceIndex =
773
773
  (this.currentSourceIndex + 1) % this.availableSources.length;
774
- this.lyrics = this.availableSources[this.currentSourceIndex].lines;
775
- this.lyricsSource = this.availableSources[this.currentSourceIndex].source;
774
+ const sourceResult = this.availableSources[this.currentSourceIndex];
775
+ this.lyrics = sourceResult.lines;
776
+ this.lyricsSource = sourceResult.source;
777
+ if (sourceResult.songwriters) {
778
+ this.songwriters = sourceResult.songwriters;
779
+ }
776
780
  await this.onLyricsLoaded();
777
781
  }
778
782
  }
@@ -781,6 +785,7 @@ let AmLyrics$1 = class AmLyrics extends i {
781
785
  title: this.songTitle?.trim() ?? '',
782
786
  artist: this.songArtist?.trim() ?? '',
783
787
  album: this.songAlbum?.trim() || undefined,
788
+ songwriters: this.songwriters?.trim() || undefined,
784
789
  durationMs: undefined,
785
790
  };
786
791
  if (typeof this.songDurationMs === 'number' && this.songDurationMs > 0) {
@@ -820,6 +825,9 @@ let AmLyrics$1 = class AmLyrics extends i {
820
825
  if (!metadata.album && catalogResult.album) {
821
826
  metadata.album = catalogResult.album;
822
827
  }
828
+ if (!metadata.songwriters && catalogResult.songwriters) {
829
+ metadata.songwriters = catalogResult.songwriters;
830
+ }
823
831
  if (metadata.durationMs == null &&
824
832
  typeof catalogResult.durationMs === 'number' &&
825
833
  catalogResult.durationMs > 0) {
@@ -1010,9 +1018,13 @@ let AmLyrics$1 = class AmLyrics extends i {
1010
1018
  const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
1011
1019
  if (ttmlRes.ok) {
1012
1020
  const ttmlText = await ttmlRes.text();
1013
- const lines = AmLyrics.parseTTML(ttmlText);
1014
- if (lines && lines.length > 0) {
1015
- allResults.push({ lines, source: 'BiniLyrics' });
1021
+ const parseResult = AmLyrics.parseTTML(ttmlText);
1022
+ if (parseResult && parseResult.lines.length > 0) {
1023
+ allResults.push({
1024
+ lines: parseResult.lines,
1025
+ source: 'BiniLyrics',
1026
+ songwriters: parseResult.songwriters,
1027
+ });
1016
1028
  return allResults;
1017
1029
  }
1018
1030
  }
@@ -1044,11 +1056,12 @@ let AmLyrics$1 = class AmLyrics extends i {
1044
1056
  const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
1045
1057
  if (ttmlRes.ok) {
1046
1058
  const ttmlText = await ttmlRes.text();
1047
- const lines = AmLyrics.parseTTML(ttmlText);
1048
- if (lines && lines.length > 0) {
1059
+ const parseResult = AmLyrics.parseTTML(ttmlText);
1060
+ if (parseResult && parseResult.lines.length > 0) {
1049
1061
  allResults.push({
1050
- lines,
1062
+ lines: parseResult.lines,
1051
1063
  source: 'BiniLyrics',
1064
+ songwriters: parseResult.songwriters,
1052
1065
  });
1053
1066
  return allResults;
1054
1067
  }
@@ -1181,77 +1194,6 @@ let AmLyrics$1 = class AmLyrics extends i {
1181
1194
  }
1182
1195
  return lines;
1183
1196
  }
1184
- /**
1185
- * Fetch lyrics from Tidal API.
1186
- * Picks 2 random servers, tries search + lyrics on each.
1187
- */
1188
- static async fetchLyricsFromTidal(metadata, isrc) {
1189
- const title = metadata.title?.trim();
1190
- const artist = metadata.artist?.trim();
1191
- if (!title || !artist)
1192
- return null;
1193
- // Pick 3 random unique servers for better reliability
1194
- const shuffled = [...TIDAL_SERVERS].sort(() => Math.random() - 0.5);
1195
- const serversToTry = shuffled.slice(0, 3);
1196
- for (const base of serversToTry) {
1197
- try {
1198
- const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
1199
- // Step 1: Search for the track
1200
- const searchQuery = `${title} ${artist}`;
1201
- const searchParams = new URLSearchParams({ s: searchQuery });
1202
- // eslint-disable-next-line no-await-in-loop
1203
- const searchResponse = await fetchWithTimeout(`${normalizedBase}/search/?${searchParams.toString()}`);
1204
- if (!searchResponse.ok) {
1205
- // eslint-disable-next-line no-continue
1206
- continue;
1207
- }
1208
- // eslint-disable-next-line no-await-in-loop
1209
- const searchData = await searchResponse.json();
1210
- const items = searchData?.data?.items;
1211
- if (!Array.isArray(items) || items.length === 0) {
1212
- // eslint-disable-next-line no-continue
1213
- continue;
1214
- }
1215
- // Find best match: prefer ISRC match, then first result
1216
- let bestTrack = items[0];
1217
- if (isrc) {
1218
- const isrcMatch = items.find((item) => item.isrc && item.isrc.toLowerCase() === isrc.toLowerCase());
1219
- if (isrcMatch) {
1220
- bestTrack = isrcMatch;
1221
- }
1222
- }
1223
- const trackId = bestTrack?.id;
1224
- if (!trackId) {
1225
- // eslint-disable-next-line no-continue
1226
- continue;
1227
- }
1228
- // Step 2: Fetch lyrics
1229
- // eslint-disable-next-line no-await-in-loop
1230
- const lyricsResponse = await fetchWithTimeout(`${normalizedBase}/lyrics/?id=${trackId}`);
1231
- if (!lyricsResponse.ok) {
1232
- // eslint-disable-next-line no-continue
1233
- continue;
1234
- }
1235
- // eslint-disable-next-line no-await-in-loop
1236
- const lyricsData = await lyricsResponse.json();
1237
- const subtitles = lyricsData?.lyrics?.subtitles;
1238
- if (subtitles && typeof subtitles === 'string') {
1239
- const lines = AmLyrics.parseLrcSubtitles(subtitles);
1240
- if (lines.length > 0) {
1241
- const provider = lyricsData?.lyrics?.lyricsProvider || 'Tidal';
1242
- return {
1243
- lines,
1244
- source: `Tidal (${provider})`,
1245
- };
1246
- }
1247
- }
1248
- }
1249
- catch {
1250
- // Try next server
1251
- }
1252
- }
1253
- return null;
1254
- }
1255
1197
  /**
1256
1198
  * Fetch lyrics from LRCLIB.
1257
1199
  * Uses search endpoint, prefers synced lyrics.
@@ -1434,6 +1376,19 @@ let AmLyrics$1 = class AmLyrics extends i {
1434
1376
  agentMap[id] = type;
1435
1377
  }
1436
1378
  }
1379
+ let songwriters;
1380
+ const songwritersNodes = doc.getElementsByTagName('songwriter');
1381
+ if (songwritersNodes.length > 0) {
1382
+ const names = [];
1383
+ for (let i = 0; i < songwritersNodes.length; i += 1) {
1384
+ if (songwritersNodes[i].textContent) {
1385
+ names.push(songwritersNodes[i].textContent);
1386
+ }
1387
+ }
1388
+ if (names.length > 0) {
1389
+ songwriters = names.join(', ');
1390
+ }
1391
+ }
1437
1392
  const translationNodes = doc.getElementsByTagName('translation');
1438
1393
  for (let i = 0; i < translationNodes.length; i += 1) {
1439
1394
  const texts = translationNodes[i].getElementsByTagName('text');
@@ -1550,7 +1505,7 @@ let AmLyrics$1 = class AmLyrics extends i {
1550
1505
  text: bgText,
1551
1506
  timestamp: timeToMs(bgSpan.getAttribute('begin')),
1552
1507
  endtime: timeToMs(bgSpan.getAttribute('end')),
1553
- part: false,
1508
+ part: !/\s$/.test(bgText),
1554
1509
  });
1555
1510
  }
1556
1511
  // eslint-disable-next-line no-continue
@@ -1573,7 +1528,7 @@ let AmLyrics$1 = class AmLyrics extends i {
1573
1528
  text,
1574
1529
  timestamp: timeToMs(span.getAttribute('begin')),
1575
1530
  endtime: timeToMs(span.getAttribute('end')),
1576
- part: false,
1531
+ part: !/\s$/.test(text),
1577
1532
  });
1578
1533
  }
1579
1534
  }
@@ -1659,7 +1614,7 @@ let AmLyrics$1 = class AmLyrics extends i {
1659
1614
  oppositeTurn: alignment === 'end',
1660
1615
  });
1661
1616
  }
1662
- return lines;
1617
+ return { lines, songwriters };
1663
1618
  }
1664
1619
  catch (e) {
1665
1620
  // eslint-disable-next-line no-console
@@ -1824,7 +1779,10 @@ let AmLyrics$1 = class AmLyrics extends i {
1824
1779
  if (!newActiveLines.includes(lineIndex)) {
1825
1780
  const lineElement = this._getLineElement(lineIndex);
1826
1781
  if (lineElement) {
1827
- lineElement.classList.remove('active');
1782
+ lineElement.classList.remove('active', 'pre-active');
1783
+ const preIdx = this.preActiveLineElements.indexOf(lineElement);
1784
+ if (preIdx !== -1)
1785
+ this.preActiveLineElements.splice(preIdx, 1);
1828
1786
  AmLyrics.resetSyllables(lineElement);
1829
1787
  }
1830
1788
  }
@@ -1836,6 +1794,9 @@ let AmLyrics$1 = class AmLyrics extends i {
1836
1794
  if (lineElement) {
1837
1795
  lineElement.classList.add('active');
1838
1796
  lineElement.classList.remove('pre-active');
1797
+ const preIdx = this.preActiveLineElements.indexOf(lineElement);
1798
+ if (preIdx !== -1)
1799
+ this.preActiveLineElements.splice(preIdx, 1);
1839
1800
  }
1840
1801
  }
1841
1802
  }
@@ -1855,10 +1816,9 @@ let AmLyrics$1 = class AmLyrics extends i {
1855
1816
  }
1856
1817
  }
1857
1818
  // Also update syllables in active gap lines (breathing dots)
1858
- const activeGaps = this.lyricsContainer.querySelectorAll('.lyrics-gap.active');
1859
- activeGaps.forEach(gapLine => {
1819
+ for (const gapLine of this.activeGapLineElements) {
1860
1820
  AmLyrics.updateSyllablesForLine(gapLine, newTime);
1861
- });
1821
+ }
1862
1822
  // Imperatively manage gap active state
1863
1823
  if (this.gapElementCache.size > 0) {
1864
1824
  for (const [, gap] of this.gapElementCache) {
@@ -1869,9 +1829,21 @@ let AmLyrics$1 = class AmLyrics extends i {
1869
1829
  const isExiting = gap.classList.contains('gap-exiting');
1870
1830
  const exitLeadMs = GAP_EXIT_LEAD_MS;
1871
1831
  const shouldStartExiting = isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
1872
- if (shouldBeActive && !isActive && !isExiting) {
1832
+ if (shouldBeActive && (!isActive || isSeek) && !isExiting) {
1873
1833
  gap.classList.remove('gap-exiting');
1834
+ if (isSeek && isActive) {
1835
+ gap.classList.remove('active');
1836
+ // eslint-disable-next-line no-void
1837
+ void gap.offsetWidth; // Force reflow
1838
+ }
1839
+ const gapDuration = gapEndTime - gapStartTime;
1840
+ const baseLoopDelay = AmLyrics.getGapLoopDelay(gapDuration);
1841
+ const totalDelay = baseLoopDelay + (newTime - gapStartTime);
1842
+ gap.style.setProperty('--gap-loop-delay', `-${totalDelay}ms`);
1874
1843
  gap.classList.add('active');
1844
+ if (!this.activeGapLineElements.includes(gap)) {
1845
+ this.activeGapLineElements.push(gap);
1846
+ }
1875
1847
  const dotSyllables = gap.querySelectorAll('.lyrics-syllable');
1876
1848
  dotSyllables.forEach(dot => {
1877
1849
  const dotStart = parseFloat(dot.getAttribute('data-start-time') || '0');
@@ -1879,24 +1851,34 @@ let AmLyrics$1 = class AmLyrics extends i {
1879
1851
  if (newTime > dotEnd) {
1880
1852
  dot.classList.add('finished');
1881
1853
  if (!dot.classList.contains('highlight')) {
1882
- AmLyrics.updateSyllableAnimation(dot);
1854
+ AmLyrics.updateSyllableAnimation(dot, newTime - dotStart);
1883
1855
  }
1884
1856
  }
1885
1857
  else if (newTime >= dotStart && newTime <= dotEnd) {
1886
- AmLyrics.updateSyllableAnimation(dot);
1858
+ AmLyrics.updateSyllableAnimation(dot, newTime - dotStart);
1887
1859
  }
1888
1860
  });
1889
1861
  }
1890
1862
  else if (shouldStartExiting) {
1891
- gap.classList.add('gap-exiting');
1863
+ // Cancel gap-loop first, force reflow, then start gap-ended
1864
+ // so the browser sees a clean animation swap
1892
1865
  gap.classList.remove('active');
1866
+ // eslint-disable-next-line no-void
1867
+ void gap.offsetWidth;
1868
+ gap.classList.add('gap-exiting');
1869
+ const gapIdx = this.activeGapLineElements.indexOf(gap);
1870
+ if (gapIdx !== -1)
1871
+ this.activeGapLineElements.splice(gapIdx, 1);
1893
1872
  setTimeout(() => {
1894
1873
  gap.classList.remove('gap-exiting');
1895
1874
  }, GAP_EXIT_LEAD_MS);
1896
1875
  }
1897
- else if (isActive && !shouldBeActive) {
1876
+ else if (!shouldBeActive && (isActive || isExiting)) {
1898
1877
  gap.classList.remove('active');
1899
1878
  gap.classList.remove('gap-exiting');
1879
+ const gapIdx = this.activeGapLineElements.indexOf(gap);
1880
+ if (gapIdx !== -1)
1881
+ this.activeGapLineElements.splice(gapIdx, 1);
1900
1882
  }
1901
1883
  else if (isExiting && newTime < gapEndTime - exitLeadMs) {
1902
1884
  gap.classList.remove('gap-exiting');
@@ -1914,20 +1896,41 @@ let AmLyrics$1 = class AmLyrics extends i {
1914
1896
  const isExiting = gap.classList.contains('gap-exiting');
1915
1897
  const exitLeadMs = GAP_EXIT_LEAD_MS;
1916
1898
  const shouldStartExiting = isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
1917
- if (shouldBeActive && !isActive && !isExiting) {
1899
+ if (shouldBeActive && (!isActive || isSeek) && !isExiting) {
1918
1900
  gap.classList.remove('gap-exiting');
1901
+ if (isSeek && isActive) {
1902
+ gap.classList.remove('active');
1903
+ // eslint-disable-next-line no-void
1904
+ void gap.offsetWidth; // Force reflow
1905
+ }
1906
+ const gapDuration = gapEndTime - gapStartTime;
1907
+ const baseLoopDelay = AmLyrics.getGapLoopDelay(gapDuration);
1908
+ const totalDelay = baseLoopDelay + (newTime - gapStartTime);
1909
+ gap.style.setProperty('--gap-loop-delay', `-${totalDelay}ms`);
1919
1910
  gap.classList.add('active');
1911
+ if (!this.activeGapLineElements.includes(gap)) {
1912
+ this.activeGapLineElements.push(gap);
1913
+ }
1920
1914
  }
1921
1915
  else if (shouldStartExiting) {
1922
- gap.classList.add('gap-exiting');
1916
+ // Cancel gap-loop first, force reflow, then start gap-ended
1923
1917
  gap.classList.remove('active');
1918
+ // eslint-disable-next-line no-void
1919
+ void gap.offsetWidth;
1920
+ gap.classList.add('gap-exiting');
1921
+ const gapIdx = this.activeGapLineElements.indexOf(gap);
1922
+ if (gapIdx !== -1)
1923
+ this.activeGapLineElements.splice(gapIdx, 1);
1924
1924
  setTimeout(() => {
1925
1925
  gap.classList.remove('gap-exiting');
1926
1926
  }, GAP_EXIT_LEAD_MS);
1927
1927
  }
1928
- else if (isActive && !shouldBeActive) {
1928
+ else if (!shouldBeActive && (isActive || isExiting)) {
1929
1929
  gap.classList.remove('active');
1930
1930
  gap.classList.remove('gap-exiting');
1931
+ const gapIdx = this.activeGapLineElements.indexOf(gap);
1932
+ if (gapIdx !== -1)
1933
+ this.activeGapLineElements.splice(gapIdx, 1);
1931
1934
  }
1932
1935
  else if (isExiting && newTime < gapEndTime - exitLeadMs) {
1933
1936
  gap.classList.remove('gap-exiting');
@@ -1942,6 +1945,25 @@ let AmLyrics$1 = class AmLyrics extends i {
1942
1945
  else if (this.lastInstrumentalIndex !== null) {
1943
1946
  this.lastInstrumentalIndex = null;
1944
1947
  }
1948
+ // Check footer active state
1949
+ const lastLyric = this.lyrics && this.lyrics.length > 0
1950
+ ? this.lyrics[this.lyrics.length - 1]
1951
+ : null;
1952
+ const footer = this.lyricsContainer.querySelector('.lyrics-footer');
1953
+ if (footer && lastLyric && lastLyric.endtime > 0) {
1954
+ const isFooterActive = newTime > lastLyric.endtime + 200; // Snappier 200ms buffer
1955
+ if (isFooterActive && !footer.classList.contains('active')) {
1956
+ footer.classList.add('active');
1957
+ if (this.autoScroll &&
1958
+ !this.isUserScrolling &&
1959
+ !this.isClickSeeking) {
1960
+ this.focusLine(footer);
1961
+ }
1962
+ }
1963
+ else if (!isFooterActive && footer.classList.contains('active')) {
1964
+ footer.classList.remove('active');
1965
+ }
1966
+ }
1945
1967
  // Pre-scroll: scroll to upcoming line ~0.5s before it starts
1946
1968
  if (this.autoScroll &&
1947
1969
  !this.isUserScrolling &&
@@ -1964,6 +1986,9 @@ let AmLyrics$1 = class AmLyrics extends i {
1964
1986
  preActiveLineIndex = i;
1965
1987
  if (!isBackToBack) {
1966
1988
  nextLineEl.classList.add('pre-active');
1989
+ if (!this.preActiveLineElements.includes(nextLineEl)) {
1990
+ this.preActiveLineElements.push(nextLineEl);
1991
+ }
1967
1992
  }
1968
1993
  this.clearPreActiveClasses(i);
1969
1994
  const slowScrollDuration = Math.max(SCROLL_ANIMATION_DURATION_MS, timeUntilStart);
@@ -1993,6 +2018,9 @@ let AmLyrics$1 = class AmLyrics extends i {
1993
2018
  if (lineEl)
1994
2019
  lineEl.classList.add('active');
1995
2020
  }
2021
+ // Trigger a faux time-change so that updateSyllablesForLine fires
2022
+ // to setup inline syllable CSS wipe animations for whatever the current time is
2023
+ this._onTimeChanged(0, this.currentTime);
1996
2024
  }
1997
2025
  }
1998
2026
  // Handle duration reset (-1 stops playback and resets currentTime to 0)
@@ -2005,6 +2033,9 @@ let AmLyrics$1 = class AmLyrics extends i {
2005
2033
  this.backgroundWordProgress.clear();
2006
2034
  this.mainWordAnimations.clear();
2007
2035
  this.backgroundWordAnimations.clear();
2036
+ this.preActiveLineElements = [];
2037
+ this.positionedLineElements = [];
2038
+ this.activeGapLineElements = [];
2008
2039
  this.setUserScrolling(false);
2009
2040
  // Cancel any running animations
2010
2041
  if (this.animationFrameId) {
@@ -2058,7 +2089,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2058
2089
  const gap = this.lyrics[targetLineIndex].timestamp -
2059
2090
  this.lyrics[prevPrimaryIndex].endtime;
2060
2091
  if (gap > 200) {
2061
- scrollDuration = Math.min(Math.max(gap * 0.6, SCROLL_ANIMATION_DURATION_MS), 2000);
2092
+ scrollDuration = Math.min(Math.max(gap * 0.85, SCROLL_ANIMATION_DURATION_MS), 4000);
2062
2093
  }
2063
2094
  }
2064
2095
  this.focusLine(targetLine, forceScroll, scrollDuration);
@@ -2120,6 +2151,9 @@ let AmLyrics$1 = class AmLyrics extends i {
2120
2151
  this.cachedLineData = null;
2121
2152
  this.lineElementCache.clear();
2122
2153
  this.gapElementCache.clear();
2154
+ this.preActiveLineElements = [];
2155
+ this.positionedLineElements = [];
2156
+ this.activeGapLineElements = [];
2123
2157
  }
2124
2158
  _updateCachedIsUnsynced() {
2125
2159
  this.cachedIsUnsynced =
@@ -2132,13 +2166,23 @@ let AmLyrics$1 = class AmLyrics extends i {
2132
2166
  return;
2133
2167
  this.cachedLineData = this.lyrics.map(line => {
2134
2168
  const wordGroups = [];
2135
- for (const syllable of line.text) {
2136
- if (syllable.part && wordGroups.length > 0) {
2137
- wordGroups[wordGroups.length - 1].push(syllable);
2138
- }
2139
- else {
2140
- wordGroups.push([syllable]);
2169
+ let currentGroupBuffer = [];
2170
+ line.text.forEach((syllable, idx) => {
2171
+ currentGroupBuffer.push(syllable);
2172
+ const nextSyllable = line.text[idx + 1];
2173
+ const endsWithDelimiter = !nextSyllable ||
2174
+ syllable.part === false ||
2175
+ /\s$/.test(syllable.text) ||
2176
+ (nextSyllable &&
2177
+ syllable.isBackground !==
2178
+ nextSyllable.isBackground);
2179
+ if (endsWithDelimiter) {
2180
+ wordGroups.push(currentGroupBuffer);
2181
+ currentGroupBuffer = [];
2141
2182
  }
2183
+ });
2184
+ if (currentGroupBuffer.length > 0) {
2185
+ wordGroups.push(currentGroupBuffer);
2142
2186
  }
2143
2187
  const groupGrowable = new Array(wordGroups.length).fill(false);
2144
2188
  const groupGlowing = new Array(wordGroups.length).fill(false);
@@ -2147,6 +2191,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2147
2191
  const vwCharOffset = new Array(wordGroups.length).fill(0);
2148
2192
  const vwStartMs = new Array(wordGroups.length).fill(0);
2149
2193
  const vwEndMs = new Array(wordGroups.length).fill(0);
2194
+ let lineIsRTL = false;
2150
2195
  let vwStart = 0;
2151
2196
  while (vwStart < wordGroups.length) {
2152
2197
  let vwEnd = vwStart;
@@ -2168,9 +2213,11 @@ let AmLyrics$1 = class AmLyrics extends i {
2168
2213
  const combinedDuration = combinedEnd - combinedStart;
2169
2214
  const isCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(combinedText);
2170
2215
  const isRTL = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF]/.test(combinedText);
2216
+ if (isRTL)
2217
+ lineIsRTL = true;
2171
2218
  const hasHyphen = combinedText.includes('-');
2172
2219
  const wordLen = combinedText.length;
2173
- let isGrowableVW = !isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 12;
2220
+ let isGrowableVW = !isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 7;
2174
2221
  if (isGrowableVW) {
2175
2222
  if (wordLen < 3) {
2176
2223
  isGrowableVW =
@@ -2181,7 +2228,8 @@ let AmLyrics$1 = class AmLyrics extends i {
2181
2228
  combinedDuration >= 850 && combinedDuration >= wordLen * 190;
2182
2229
  }
2183
2230
  }
2184
- const isGlowingVW = isGrowableVW;
2231
+ const isLineSynced = line.isWordSynced === false || line.text.some(s => s.lineSynced);
2232
+ const isGlowingVW = isGrowableVW && !isLineSynced;
2185
2233
  let charOff = 0;
2186
2234
  for (let gi = vwStart; gi <= vwEnd; gi += 1) {
2187
2235
  groupGrowable[gi] = isGrowableVW;
@@ -2205,6 +2253,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2205
2253
  vwCharOffset,
2206
2254
  vwStartMs,
2207
2255
  vwEndMs,
2256
+ lineIsRTL,
2208
2257
  };
2209
2258
  });
2210
2259
  }
@@ -2285,15 +2334,17 @@ let AmLyrics$1 = class AmLyrics extends i {
2285
2334
  clearPreActiveClasses(exceptLineIndex = null) {
2286
2335
  if (!this.lyricsContainer)
2287
2336
  return;
2288
- this.lyricsContainer
2289
- .querySelectorAll('.lyrics-line.pre-active')
2290
- .forEach(element => {
2291
- const lineElement = element;
2337
+ const keptLines = [];
2338
+ for (const lineElement of this.preActiveLineElements) {
2292
2339
  const lineIndex = AmLyrics.getLineIndexFromElement(lineElement);
2293
- if (lineIndex !== exceptLineIndex) {
2340
+ if (lineIndex === exceptLineIndex) {
2341
+ keptLines.push(lineElement);
2342
+ }
2343
+ else {
2294
2344
  lineElement.classList.remove('pre-active');
2295
2345
  }
2296
- });
2346
+ }
2347
+ this.preActiveLineElements = keptLines;
2297
2348
  }
2298
2349
  getPrimaryActiveLineIndex(activeIndices) {
2299
2350
  if (activeIndices.length === 0)
@@ -2743,6 +2794,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2743
2794
  // Clean up any lingering scroll animations before smooth scroll
2744
2795
  for (const line of animatingLines) {
2745
2796
  line.classList.remove('scroll-animate');
2797
+ line.style.removeProperty('will-change');
2746
2798
  line.style.removeProperty('--scroll-delta');
2747
2799
  line.style.removeProperty('--lyrics-line-delay');
2748
2800
  line.style.removeProperty('--scroll-duration');
@@ -2801,6 +2853,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2801
2853
  // --- Step 4: Re-add scroll-animate class to start fresh animations ---
2802
2854
  for (const line of newAnimatingLines) {
2803
2855
  line.classList.add('scroll-animate');
2856
+ line.style.willChange = 'transform';
2804
2857
  animatingLines.push(line);
2805
2858
  }
2806
2859
  animState.isAnimating = true;
@@ -2817,6 +2870,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2817
2870
  for (let i = 0; i < animatingLines.length; i += 1) {
2818
2871
  const line = animatingLines[i];
2819
2872
  line.classList.remove('scroll-animate');
2873
+ line.style.removeProperty('will-change');
2820
2874
  line.style.removeProperty('--scroll-delta');
2821
2875
  line.style.removeProperty('--lyrics-line-delay');
2822
2876
  line.style.removeProperty('--scroll-duration');
@@ -2845,12 +2899,14 @@ let AmLyrics$1 = class AmLyrics extends i {
2845
2899
  'next-3',
2846
2900
  'next-4',
2847
2901
  ];
2848
- // Remove old position classes
2849
- this.lyricsContainer
2850
- .querySelectorAll(`.${positionClasses.join(', .')}`)
2851
- .forEach(el => el.classList.remove(...positionClasses));
2902
+ // Remove old position classes from tracked elements
2903
+ for (const el of this.positionedLineElements) {
2904
+ el.classList.remove(...positionClasses);
2905
+ }
2906
+ this.positionedLineElements = [];
2852
2907
  // Add new position classes
2853
2908
  lineToScroll.classList.add('lyrics-activest');
2909
+ this.positionedLineElements.push(lineToScroll);
2854
2910
  const lineElements = Array.from(this.lyricsContainer.querySelectorAll('.lyrics-line'));
2855
2911
  const scrollLineIndex = lineElements.indexOf(lineToScroll);
2856
2912
  for (let i = Math.max(0, scrollLineIndex - 4); i <= Math.min(lineElements.length - 1, scrollLineIndex + 4); i += 1) {
@@ -2865,6 +2921,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2865
2921
  element.classList.add(`prev-${Math.abs(position)}`);
2866
2922
  else
2867
2923
  element.classList.add(`next-${position}`);
2924
+ this.positionedLineElements.push(element);
2868
2925
  }
2869
2926
  }
2870
2927
  }
@@ -2884,11 +2941,12 @@ let AmLyrics$1 = class AmLyrics extends i {
2884
2941
  paddingTop) < 1) {
2885
2942
  return;
2886
2943
  }
2887
- // Skip scroll if near the bottom of content (prevents footer jitter)
2888
- if (!forceScroll) {
2944
+ // Skip scroll if near the bottom of content and we aren't trying to scroll back up
2945
+ if (!forceScroll && !activeLine.classList.contains('lyrics-footer')) {
2889
2946
  const parent = this.lyricsContainer;
2890
2947
  const atBottom = parent.scrollTop + parent.clientHeight >= parent.scrollHeight - 50;
2891
- if (atBottom) {
2948
+ const targetTop = Math.max(0, -(paddingTop - activeLine.offsetTop));
2949
+ if (atBottom && targetTop > parent.scrollTop - 50) {
2892
2950
  return;
2893
2951
  }
2894
2952
  }
@@ -2909,7 +2967,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2909
2967
  * Update syllable highlight animation - apply CSS wipe animation
2910
2968
  * (Exact copy from YouLyPlus _updateSyllableAnimation)
2911
2969
  */
2912
- static updateSyllableAnimation(syllable) {
2970
+ static updateSyllableAnimation(syllable, elapsedTimeMs = 0) {
2913
2971
  if (syllable.classList.contains('highlight'))
2914
2972
  return;
2915
2973
  const { classList } = syllable;
@@ -2937,8 +2995,8 @@ let AmLyrics$1 = class AmLyrics extends i {
2937
2995
  const baseDelayPerChar = finalDuration * 0.09;
2938
2996
  const growDurationMs = finalDuration * 1.5;
2939
2997
  allWordCharSpans.forEach(span => {
2940
- const horizontalOffset = parseFloat(span.dataset.horizontalOffset || '0');
2941
- const maxScale = span.dataset.maxScale || '1.1';
2998
+ const matrixScale = span.dataset.matrixScale || '1.1';
2999
+ const charOffsetX = span.dataset.charOffsetX || '0';
2942
3000
  const shadowIntensity = span.dataset.shadowIntensity || '0.6';
2943
3001
  const translateYPeak = span.dataset.translateYPeak || '-2';
2944
3002
  const syllableCharIndex = parseFloat(span.dataset.syllableCharIndex || '0');
@@ -2946,13 +3004,13 @@ let AmLyrics$1 = class AmLyrics extends i {
2946
3004
  charAnimationsMap.set(span, `grow-dynamic ${growDurationMs}ms ease-in-out ${growDelay}ms forwards`);
2947
3005
  styleUpdates.push({
2948
3006
  element: span,
2949
- property: '--char-offset-x',
2950
- value: `${horizontalOffset}`,
3007
+ property: '--matrix-scale',
3008
+ value: matrixScale,
2951
3009
  });
2952
3010
  styleUpdates.push({
2953
3011
  element: span,
2954
- property: '--max-scale',
2955
- value: maxScale,
3012
+ property: '--char-offset-x',
3013
+ value: `${charOffsetX}px`,
2956
3014
  });
2957
3015
  styleUpdates.push({
2958
3016
  element: span,
@@ -2962,7 +3020,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2962
3020
  styleUpdates.push({
2963
3021
  element: span,
2964
3022
  property: '--translate-y-peak',
2965
- value: `${translateYPeak}`,
3023
+ value: `${translateYPeak}px`,
2966
3024
  });
2967
3025
  });
2968
3026
  }
@@ -2971,7 +3029,7 @@ let AmLyrics$1 = class AmLyrics extends i {
2971
3029
  charSpans.forEach((span, charIndex) => {
2972
3030
  const startPct = parseFloat(span.dataset.wipeStart || '0');
2973
3031
  const durationPct = parseFloat(span.dataset.wipeDuration || '0');
2974
- const wipeDelay = syllableDurationMs * startPct;
3032
+ const wipeDelay = syllableDurationMs * startPct - elapsedTimeMs;
2975
3033
  const wipeDuration = syllableDurationMs * durationPct;
2976
3034
  const useStartAnimation = isFirstInContainer && charIndex === 0;
2977
3035
  let charWipeAnimation = 'wipe';
@@ -2987,9 +3045,9 @@ let AmLyrics$1 = class AmLyrics extends i {
2987
3045
  animationParts.push(existingAnimation.split(',')[0].trim());
2988
3046
  }
2989
3047
  if (charIndex > 0) {
2990
- const arrivalTime = span.dataset.preWipeArrival
3048
+ const arrivalTime = (span.dataset.preWipeArrival
2991
3049
  ? parseFloat(span.dataset.preWipeArrival)
2992
- : wipeDelay;
3050
+ : syllableDurationMs * startPct) - elapsedTimeMs;
2993
3051
  const constantDuration = parseFloat(span.dataset.preWipeDuration || '100');
2994
3052
  const animDelay = arrivalTime - constantDuration;
2995
3053
  if (constantDuration > 0) {
@@ -3019,12 +3077,13 @@ let AmLyrics$1 = class AmLyrics extends i {
3019
3077
  return;
3020
3078
  const currentWipeAnimation = isGap ? 'fade-gap' : wipeAnimation;
3021
3079
  // eslint-disable-next-line no-param-reassign
3022
- syllable.style.animation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} forwards`;
3080
+ syllable.style.animation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} ${-elapsedTimeMs}ms forwards`;
3023
3081
  }
3024
3082
  // --- WRITE PHASE ---
3025
3083
  classList.remove('pre-highlight');
3026
3084
  classList.add('highlight');
3027
3085
  for (const [span, animationString] of charAnimationsMap.entries()) {
3086
+ span.style.willChange = 'transform';
3028
3087
  span.style.animation = animationString;
3029
3088
  }
3030
3089
  // Apply style updates
@@ -3051,6 +3110,7 @@ let AmLyrics$1 = class AmLyrics extends i {
3051
3110
  syllable.querySelectorAll('span.char').forEach(span => {
3052
3111
  const el = span;
3053
3112
  el.style.animation = '';
3113
+ el.style.willChange = '';
3054
3114
  el.style.transition = 'none';
3055
3115
  el.style.backgroundColor = 'var(--lyplus-text-secondary)';
3056
3116
  });
@@ -3064,6 +3124,7 @@ let AmLyrics$1 = class AmLyrics extends i {
3064
3124
  const el = span;
3065
3125
  el.style.removeProperty('background-color');
3066
3126
  el.style.removeProperty('transition');
3127
+ el.style.removeProperty('will-change');
3067
3128
  });
3068
3129
  });
3069
3130
  }
@@ -3093,7 +3154,7 @@ let AmLyrics$1 = class AmLyrics extends i {
3093
3154
  const syllable = syllables[i];
3094
3155
  const startTime = parseFloat(syllable.getAttribute('data-start-time') || '0');
3095
3156
  const endTime = parseFloat(syllable.getAttribute('data-end-time') || '0');
3096
- if (startTime) {
3157
+ if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
3097
3158
  const { classList } = syllable;
3098
3159
  const hasHighlight = classList.contains('highlight');
3099
3160
  const hasFinished = classList.contains('finished');
@@ -3117,7 +3178,7 @@ let AmLyrics$1 = class AmLyrics extends i {
3117
3178
  if (currentTimeMs >= startTime && currentTimeMs <= endTime) {
3118
3179
  // Currently active
3119
3180
  if (!hasHighlight) {
3120
- AmLyrics.updateSyllableAnimation(syllable);
3181
+ AmLyrics.updateSyllableAnimation(syllable, currentTimeMs - startTime);
3121
3182
  }
3122
3183
  if (hasFinished) {
3123
3184
  classList.remove('finished');
@@ -3127,7 +3188,7 @@ let AmLyrics$1 = class AmLyrics extends i {
3127
3188
  // Finished
3128
3189
  if (!hasFinished) {
3129
3190
  if (!hasHighlight) {
3130
- AmLyrics.updateSyllableAnimation(syllable);
3191
+ AmLyrics.updateSyllableAnimation(syllable, currentTimeMs - startTime);
3131
3192
  }
3132
3193
  classList.add('finished');
3133
3194
  }
@@ -3379,7 +3440,6 @@ let AmLyrics$1 = class AmLyrics extends i {
3379
3440
  }
3380
3441
  // Set both old internal CSS variables (for backward compatibility)
3381
3442
  // and new public CSS variables (which take precedence)
3382
- this.style.setProperty('--hover-background-color', this.hoverBackgroundColor);
3383
3443
  this.style.setProperty('--highlight-color', this.highlightColor);
3384
3444
  const sourceLabel = this.lyricsSource ?? 'Unavailable';
3385
3445
  const isUnsynced = this.cachedIsUnsynced;
@@ -3421,7 +3481,7 @@ let AmLyrics$1 = class AmLyrics extends i {
3421
3481
  syllable.romanizedText &&
3422
3482
  syllable.romanizedText.trim() !== syllable.text.trim()
3423
3483
  ? b `<span
3424
- class="lyrics-syllable transliteration ${syllable.lineSynced
3484
+ class="lyrics-syllable transliteration no-chars ${syllable.lineSynced
3425
3485
  ? 'line-synced'
3426
3486
  : ''}"
3427
3487
  data-start-time="${startTimeMs}"
@@ -3432,21 +3492,24 @@ let AmLyrics$1 = class AmLyrics extends i {
3432
3492
  >${syllable.romanizedText}</span
3433
3493
  >`
3434
3494
  : '';
3435
- return b `<span class="lyrics-word">
3436
- <span class="lyrics-syllable-wrap">
3437
- <span
3438
- class="lyrics-syllable ${syllable.lineSynced
3439
- ? 'line-synced'
3495
+ return b `<span class="lyrics-word"
3496
+ ><span
3497
+ class="lyrics-syllable-wrap${bgRomanizedText
3498
+ ? ' has-transliteration'
3499
+ : ''}"
3500
+ ><span
3501
+ class="lyrics-syllable no-chars${syllable.lineSynced
3502
+ ? ' line-synced'
3440
3503
  : ''}"
3441
3504
  data-start-time="${startTimeMs}"
3442
3505
  data-end-time="${endTimeMs}"
3443
3506
  data-duration="${durationMs}"
3444
3507
  data-syllable-index="${syllableIndex}"
3508
+ data-wipe-ratio="1"
3445
3509
  >${syllable.text}</span
3446
- >
3447
- ${bgRomanizedText}
3448
- </span>
3449
- </span>`;
3510
+ >${bgRomanizedText}</span
3511
+ ></span
3512
+ >`;
3450
3513
  })}
3451
3514
  </p>`
3452
3515
  : '';
@@ -3464,8 +3527,11 @@ let AmLyrics$1 = class AmLyrics extends i {
3464
3527
  const vwFullText = lineData?.vwFullText ?? [];
3465
3528
  const vwFullDuration = lineData?.vwFullDuration ?? [];
3466
3529
  const vwCharOffset = lineData?.vwCharOffset ?? [];
3530
+ const lineIsRTL = lineData?.lineIsRTL ?? false;
3467
3531
  // Create main vocals using YouLyPlus syllable structure
3468
- const mainVocalElement = b `<p class="main-vocal-container">
3532
+ const mainVocalElement = b `<p
3533
+ class="main-vocal-container ${lineIsRTL ? 'rtl-text' : ''}"
3534
+ >
3469
3535
  ${wordGroups.map((group, groupIdx) => {
3470
3536
  const isGrowable = groupGrowable[groupIdx];
3471
3537
  const isGlowing = groupGlowing[groupIdx];
@@ -3475,12 +3541,21 @@ let AmLyrics$1 = class AmLyrics extends i {
3475
3541
  const wordNumChars = wordText.length;
3476
3542
  const groupCharOffset = isGrowable ? vwCharOffset[groupIdx] : 0;
3477
3543
  let sylCharAccumulator = 0;
3544
+ const groupText = group.map(s => s.text).join('');
3545
+ const shouldAllowBreak = groupText.trim().length >= 16 ||
3546
+ /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(groupText);
3547
+ // Calculate dynamic rise duration based on the audio duration of the word
3548
+ const wordStartTimeMs = group[0].timestamp;
3549
+ const wordEndTimeMs = group[group.length - 1].endtime;
3550
+ const actualDurationMs = wordEndTimeMs - wordStartTimeMs;
3551
+ // Base float is 0.8s, plus a portion of the audio duration, capped between 1.0s and 2.5s
3552
+ const riseDuration = Math.max(1.2, Math.min(2.5, 1.2 + (actualDurationMs / 1000) * 0.6));
3478
3553
  return b `<span
3479
- class="lyrics-word ${isGrowable ? 'growable' : ''} ${isGlowing
3480
- ? 'glowing'
3481
- : ''} ${group.length > 1 ? 'allow-break' : ''}"
3482
- >
3483
- ${group.map((syllable, sylIdx) => {
3554
+ class="lyrics-word${isGrowable ? ' growable' : ''}${isGlowing
3555
+ ? ' glowing'
3556
+ : ''}${shouldAllowBreak ? ' allow-break' : ''}"
3557
+ style="--rise-duration: ${riseDuration}s"
3558
+ >${group.map((syllable, sylIdx) => {
3484
3559
  const startTimeMs = syllable.timestamp;
3485
3560
  const endTimeMs = syllable.endtime;
3486
3561
  const durationMs = endTimeMs - startTimeMs;
@@ -3489,7 +3564,7 @@ let AmLyrics$1 = class AmLyrics extends i {
3489
3564
  syllable.romanizedText &&
3490
3565
  syllable.romanizedText.trim() !== syllable.text.trim()
3491
3566
  ? b `<span
3492
- class="lyrics-syllable transliteration ${groupLineSynced
3567
+ class="lyrics-syllable transliteration no-chars ${groupLineSynced
3493
3568
  ? 'line-synced'
3494
3569
  : ''}"
3495
3570
  data-start-time="${startTimeMs}"
@@ -3566,17 +3641,22 @@ let AmLyrics$1 = class AmLyrics extends i {
3566
3641
  data-wipe-duration="${(1 / numCharsInSyllable).toFixed(4)}"
3567
3642
  data-horizontal-offset="${horizontalOffset.toFixed(2)}"
3568
3643
  data-max-scale="${charMaxScale.toFixed(3)}"
3644
+ data-matrix-scale="${(charMaxScale * 0.98).toFixed(3)}"
3645
+ data-char-offset-x="${(horizontalOffset * 0.98).toFixed(2)}"
3569
3646
  data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
3570
3647
  data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
3571
3648
  >${char}</span
3572
3649
  >`;
3573
3650
  })}`;
3574
3651
  }
3575
- return b `<span class="lyrics-syllable-wrap">
3576
- <span
3577
- class="lyrics-syllable ${groupLineSynced
3578
- ? 'line-synced'
3652
+ return b `<span
3653
+ class="lyrics-syllable-wrap${romanizedText
3654
+ ? ' has-transliteration'
3579
3655
  : ''}"
3656
+ ><span
3657
+ class="lyrics-syllable${groupLineSynced
3658
+ ? ' line-synced'
3659
+ : ''}${isGrowable ? ' has-chars' : ' no-chars'}"
3580
3660
  data-start-time="${startTimeMs}"
3581
3661
  data-end-time="${endTimeMs}"
3582
3662
  data-duration="${durationMs}"
@@ -3584,11 +3664,10 @@ let AmLyrics$1 = class AmLyrics extends i {
3584
3664
  data-syllable-index="${sylIdx}"
3585
3665
  data-wipe-ratio="1"
3586
3666
  >${syllableContent}</span
3587
- >
3588
- ${romanizedText}
3589
- </span>`;
3590
- })}
3591
- </span>`;
3667
+ >${romanizedText}</span
3668
+ >`;
3669
+ })}</span
3670
+ >`;
3592
3671
  })}
3593
3672
  </p>`;
3594
3673
  // Translation container (if enabled)
@@ -3610,7 +3689,11 @@ let AmLyrics$1 = class AmLyrics extends i {
3610
3689
  line.romanizedText &&
3611
3690
  !line.text.some(s => s.romanizedText) &&
3612
3691
  line.romanizedText.trim() !== fullLineText
3613
- ? b `<div class="lyrics-romanization-container">
3692
+ ? b `<div
3693
+ class="lyrics-romanization-container ${lineIsRTL
3694
+ ? 'rtl-text'
3695
+ : ''}"
3696
+ >
3614
3697
  ${line.romanizedText}
3615
3698
  </div>`
3616
3699
  : '';
@@ -3630,42 +3713,37 @@ let AmLyrics$1 = class AmLyrics extends i {
3630
3713
  data-end-time="${gapForLine.gapEnd}"
3631
3714
  style="--gap-pulse-duration: ${GAP_PULSE_DURATION_MS}ms; --gap-loop-delay: -${gapLoopDelay}ms; --gap-exit-duration: ${GAP_EXIT_LEAD_MS}ms; --gap-exit-scale: ${GAP_MIN_SCALE};"
3632
3715
  >
3633
- <div class="lyrics-line-container">
3634
- <p class="main-vocal-container">
3635
- <span class="lyrics-word">
3636
- <span class="lyrics-syllable-wrap">
3637
- <span
3638
- class="lyrics-syllable"
3639
- data-start-time="${gapForLine.gapStart}"
3640
- data-end-time="${gapForLine.gapStart + dotDuration}"
3641
- data-duration="${dotDuration}"
3642
- data-wipe-ratio="1"
3643
- data-syllable-index="0"
3644
- ></span>
3645
- </span>
3646
- <span class="lyrics-syllable-wrap">
3647
- <span
3648
- class="lyrics-syllable"
3649
- data-start-time="${gapForLine.gapStart + dotDuration}"
3650
- data-end-time="${gapForLine.gapStart + dotDuration * 2}"
3651
- data-duration="${dotDuration}"
3652
- data-wipe-ratio="1"
3653
- data-syllable-index="1"
3654
- ></span>
3655
- </span>
3656
- <span class="lyrics-syllable-wrap">
3657
- <span
3658
- class="lyrics-syllable"
3659
- data-start-time="${gapForLine.gapStart + dotDuration * 2}"
3660
- data-end-time="${gapForLine.gapEnd}"
3661
- data-duration="${dotDuration}"
3662
- data-wipe-ratio="1"
3663
- data-syllable-index="2"
3664
- ></span>
3665
- </span>
3666
- </span>
3667
- </p>
3668
- </div>
3716
+ <p class="main-vocal-container">
3717
+ <span class="lyrics-word"
3718
+ ><span class="lyrics-syllable-wrap"
3719
+ ><span
3720
+ class="lyrics-syllable"
3721
+ data-start-time="${gapForLine.gapStart}"
3722
+ data-end-time="${gapForLine.gapStart + dotDuration}"
3723
+ data-duration="${dotDuration}"
3724
+ data-wipe-ratio="1"
3725
+ data-syllable-index="0"
3726
+ ></span></span
3727
+ ><span class="lyrics-syllable-wrap"
3728
+ ><span
3729
+ class="lyrics-syllable"
3730
+ data-start-time="${gapForLine.gapStart + dotDuration}"
3731
+ data-end-time="${gapForLine.gapStart + dotDuration * 2}"
3732
+ data-duration="${dotDuration}"
3733
+ data-wipe-ratio="1"
3734
+ data-syllable-index="1"
3735
+ ></span></span
3736
+ ><span class="lyrics-syllable-wrap"
3737
+ ><span
3738
+ class="lyrics-syllable"
3739
+ data-start-time="${gapForLine.gapStart + dotDuration * 2}"
3740
+ data-end-time="${gapForLine.gapEnd}"
3741
+ data-duration="${dotDuration}"
3742
+ data-wipe-ratio="1"
3743
+ data-syllable-index="2"
3744
+ ></span></span
3745
+ ></span>
3746
+ </p>
3669
3747
  </div>`;
3670
3748
  }
3671
3749
  return b `
@@ -3674,7 +3752,7 @@ let AmLyrics$1 = class AmLyrics extends i {
3674
3752
  id="${lineId}"
3675
3753
  class="lyrics-line ${line.alignment === 'end'
3676
3754
  ? 'singer-right'
3677
- : 'singer-left'}"
3755
+ : 'singer-left'} ${lineIsRTL ? 'rtl-text' : ''}"
3678
3756
  data-start-time="${lineStartTime}"
3679
3757
  data-end-time="${lineEndTime}"
3680
3758
  @click=${() => this.handleLineClick(line)}
@@ -3685,11 +3763,11 @@ let AmLyrics$1 = class AmLyrics extends i {
3685
3763
  }
3686
3764
  }}
3687
3765
  >
3688
- <div class="lyrics-line-container">
3766
+ <div class="lyrics-line-container ${lineIsRTL ? 'rtl-text' : ''}">
3689
3767
  ${bgPlacement === 'before' ? backgroundVocalElement : ''}
3690
3768
  ${mainVocalElement}
3691
3769
  ${bgPlacement === 'after' ? backgroundVocalElement : ''}
3692
- ${translationElement} ${lineRomanizationElement}
3770
+ ${lineRomanizationElement} ${translationElement}
3693
3771
  </div>
3694
3772
  </div>
3695
3773
  `;
@@ -3802,13 +3880,13 @@ let AmLyrics$1 = class AmLyrics extends i {
3802
3880
  ${renderContent()}
3803
3881
  ${!this.isLoading
3804
3882
  ? b `
3805
- <footer class="lyrics-footer">
3883
+ <footer class="lyrics-footer lyrics-line">
3806
3884
  <div class="footer-content">
3807
3885
  <span
3808
3886
  class="source-info"
3809
3887
  style="display: flex; align-items: center; gap: 8px;"
3810
3888
  >
3811
- Source: ${sourceLabel}
3889
+ <b style="font-weight: 750;">Source</b> ${sourceLabel}
3812
3890
  ${(this.availableSources &&
3813
3891
  this.availableSources.length > 1) ||
3814
3892
  !this.hasFetchedAllProviders
@@ -3851,15 +3929,25 @@ let AmLyrics$1 = class AmLyrics extends i {
3851
3929
  `
3852
3930
  : ''}
3853
3931
  </span>
3854
- <span class="version-info">
3855
- v${VERSION}
3932
+ ${this.songwriters
3933
+ ? b `<span
3934
+ class="songwriters-info"
3935
+ style="margin-top: 4px; font-weight: normal; font-size: 0.9em;"
3936
+ >
3937
+ <b style="font-weight: 750;">Songwriters</b> ${this
3938
+ .songwriters}
3939
+ </span>`
3940
+ : ''}
3941
+ <span class="version-info" style="margin-top: 8px;">
3942
+ <b style="font-weight: 750;">am-lyrics</b> v${VERSION} •
3856
3943
 
3857
3944
  <a
3858
3945
  href="https://github.com/uimaxbai/apple-music-web-components"
3859
3946
  target="_blank"
3860
3947
  rel="noopener noreferrer"
3861
- >Star me on GitHub</a
3862
- >
3948
+ style="display: inline-flex; align-items: center; gap: 4px;"
3949
+ >Star me on GitHub
3950
+ </a>
3863
3951
  </span>
3864
3952
  </div>
3865
3953
  </footer>
@@ -3896,6 +3984,7 @@ AmLyrics$1.styles = i$3 `
3896
3984
  --lyplus-font-size-base: 32px;
3897
3985
  --lyplus-font-size-base-grow: 24.5;
3898
3986
  --lyplus-font-size-subtext: 0.6em;
3987
+ --char-rise-y: calc(-0.035 * var(--lyplus-font-size-base));
3899
3988
 
3900
3989
  --lyplus-blur-amount: 0.07em;
3901
3990
  --lyplus-blur-amount-near: 0.035em;
@@ -3929,7 +4018,6 @@ AmLyrics$1.styles = i$3 `
3929
4018
  -webkit-overflow-scrolling: touch;
3930
4019
  box-sizing: border-box;
3931
4020
  scrollbar-width: none;
3932
- transform: translateZ(0);
3933
4021
  }
3934
4022
 
3935
4023
  .lyrics-container::-webkit-scrollbar {
@@ -3940,11 +4028,13 @@ AmLyrics$1.styles = i$3 `
3940
4028
  .lyrics-container.touch-scrolling .lyrics-line,
3941
4029
  .lyrics-container.touch-scrolling .lyrics-plus-metadata {
3942
4030
  transition: none !important;
4031
+ filter: none !important;
3943
4032
  }
3944
4033
 
3945
4034
  /* Apply smooth gliding transition for mouse-wheel scrolling */
3946
4035
  .lyrics-container.wheel-scrolling .lyrics-line {
3947
4036
  transition: transform 0.3s ease-out !important;
4037
+ filter: none !important;
3948
4038
  }
3949
4039
 
3950
4040
  .lyrics-line.scroll-animate {
@@ -3971,18 +4061,13 @@ AmLyrics$1.styles = i$3 `
3971
4061
  font-size: var(--lyplus-font-size-base);
3972
4062
  cursor: pointer;
3973
4063
  transform-origin: left;
3974
- transform: translateZ(1px);
3975
4064
  transition:
3976
4065
  opacity 0.3s ease,
3977
4066
  transform 0.4s cubic-bezier(0.41, 0, 0.12, 0.99)
3978
4067
  var(--lyrics-line-delay, 0ms),
3979
4068
  filter 0.3s ease;
3980
- will-change: transform, filter, opacity;
3981
4069
  content-visibility: auto;
3982
4070
  text-rendering: optimizeLegibility;
3983
- overflow-wrap: break-word;
3984
- mix-blend-mode: lighten;
3985
- border-radius: var(--lyplus-border-radius-base);
3986
4071
  }
3987
4072
 
3988
4073
  .lyrics-line:not(.scroll-animate) {
@@ -4002,8 +4087,7 @@ AmLyrics$1.styles = i$3 `
4002
4087
 
4003
4088
  .lyrics-line.active .lyrics-line-container,
4004
4089
  .lyrics-line.pre-active .lyrics-line-container {
4005
- transform: scale3d(1.001, 1.001, 1);
4006
- will-change: transform;
4090
+ transform: scale3d(1.001, 1.001, 1) translateZ(0);
4007
4091
  transition:
4008
4092
  transform 0.5s ease,
4009
4093
  background-color 0.18s,
@@ -4048,12 +4132,10 @@ AmLyrics$1.styles = i$3 `
4048
4132
  .lyrics-line.active {
4049
4133
  opacity: 1;
4050
4134
  color: var(--lyplus-text-primary);
4051
- will-change: transform, opacity;
4052
4135
  }
4053
4136
 
4054
4137
  .lyrics-line.pre-active {
4055
4138
  opacity: 1;
4056
- will-change: transform, opacity;
4057
4139
  }
4058
4140
 
4059
4141
  .lyrics-line.singer-right {
@@ -4067,6 +4149,18 @@ AmLyrics$1.styles = i$3 `
4067
4149
 
4068
4150
  .lyrics-line.rtl-text {
4069
4151
  direction: rtl;
4152
+ text-align: right !important;
4153
+ transform-origin: right;
4154
+ }
4155
+
4156
+ .lyrics-line.rtl-text .lyrics-line-container,
4157
+ .lyrics-line.rtl-text .main-vocal-container {
4158
+ transform-origin: right;
4159
+ }
4160
+
4161
+ .lyrics-line.rtl-text .lyrics-romanization-container,
4162
+ .lyrics-line.rtl-text .lyrics-translation-container {
4163
+ text-align: right;
4070
4164
  }
4071
4165
 
4072
4166
  /* --- Unsynced (Plain Text) Lyrics Overrides --- */
@@ -4098,7 +4192,8 @@ AmLyrics$1.styles = i$3 `
4098
4192
 
4099
4193
  @media (hover: hover) and (pointer: fine) {
4100
4194
  .lyrics-line:hover {
4101
- background: var(--hover-background-color, rgba(255, 255, 255, 0.13));
4195
+ filter: none !important;
4196
+ opacity: 1 !important;
4102
4197
  }
4103
4198
  .lyrics-container.is-unsynced .lyrics-line:hover {
4104
4199
  background: transparent !important;
@@ -4128,6 +4223,7 @@ AmLyrics$1.styles = i$3 `
4128
4223
 
4129
4224
  /* Unblur all lines when user is scrolling */
4130
4225
  .lyrics-container.user-scrolling .lyrics-line {
4226
+ transition: none !important;
4131
4227
  filter: none !important;
4132
4228
  opacity: 0.8 !important;
4133
4229
  }
@@ -4144,6 +4240,7 @@ AmLyrics$1.styles = i$3 `
4144
4240
  .lyrics-word:not(.allow-break) {
4145
4241
  display: inline-block;
4146
4242
  vertical-align: baseline;
4243
+ white-space: nowrap;
4147
4244
  }
4148
4245
 
4149
4246
  .lyrics-word.allow-break {
@@ -4154,7 +4251,7 @@ AmLyrics$1.styles = i$3 `
4154
4251
  display: inline;
4155
4252
  }
4156
4253
 
4157
- .lyrics-syllable-wrap:has(.lyrics-syllable.transliteration) {
4254
+ .lyrics-syllable-wrap.has-transliteration {
4158
4255
  display: inline-flex;
4159
4256
  flex-direction: column;
4160
4257
  align-items: start;
@@ -4182,7 +4279,7 @@ AmLyrics$1.styles = i$3 `
4182
4279
  transition: transform 1s ease !important;
4183
4280
  }
4184
4281
 
4185
- .lyrics-syllable.finished:has(.char) {
4282
+ .lyrics-syllable.finished.has-chars {
4186
4283
  background-color: transparent;
4187
4284
  }
4188
4285
 
@@ -4191,19 +4288,16 @@ AmLyrics$1.styles = i$3 `
4191
4288
  }
4192
4289
 
4193
4290
  .lyrics-line.active:not(.lyrics-gap) .lyrics-syllable {
4194
- transform: translateY(0.001%) translateZ(1px);
4195
4291
  transition:
4196
4292
  transform 1s ease,
4197
4293
  background-color 0.5s,
4198
4294
  color 0.5s;
4199
- will-change: transform, background;
4200
4295
  }
4201
4296
 
4202
4297
  /* --- Wipe Highlight Effect --- */
4298
+ .lyrics-line.active:not(.lyrics-gap) .lyrics-syllable.highlight.no-chars,
4203
4299
  .lyrics-line.active:not(.lyrics-gap)
4204
- .lyrics-syllable.highlight:not(:has(.char)),
4205
- .lyrics-line.active:not(.lyrics-gap)
4206
- .lyrics-syllable.pre-highlight:not(:has(.char)) {
4300
+ .lyrics-syllable.pre-highlight.no-chars {
4207
4301
  background-repeat: no-repeat;
4208
4302
  background-image:
4209
4303
  linear-gradient(
@@ -4245,11 +4339,19 @@ AmLyrics$1.styles = i$3 `
4245
4339
  right;
4246
4340
  }
4247
4341
 
4342
+ /* Non-growable words float up with a gentle curve */
4248
4343
  .lyrics-line.active:not(.lyrics-gap)
4249
4344
  .lyrics-word:not(.growable)
4250
- .lyrics-syllable.highlight,
4345
+ .lyrics-syllable.highlight {
4346
+ transform: translateY(-3.5%);
4347
+ transition:
4348
+ transform var(--rise-duration, 1.5s) cubic-bezier(0.22, 1, 0.36, 1),
4349
+ background-color 0.5s,
4350
+ color 0.5s;
4351
+ }
4352
+
4251
4353
  .lyrics-word.growable .lyrics-syllable.cleanup .char {
4252
- transform: translateY(-3.5%) translateZ(1px);
4354
+ transform: translateY(-3.5%);
4253
4355
  }
4254
4356
 
4255
4357
  .lyrics-line.active:not(.lyrics-gap) .lyrics-syllable.highlight.finished {
@@ -4276,7 +4378,7 @@ AmLyrics$1.styles = i$3 `
4276
4378
  }
4277
4379
 
4278
4380
  /* Syllable with chars: make syllable transparent, chars handle color */
4279
- .lyrics-line .lyrics-syllable:has(span.char):not(.finished) {
4381
+ .lyrics-line .lyrics-syllable.has-chars:not(.finished) {
4280
4382
  background-color: transparent;
4281
4383
  color: transparent;
4282
4384
  }
@@ -4289,6 +4391,7 @@ AmLyrics$1.styles = i$3 `
4289
4391
  font-feature-settings: 'liga' 0;
4290
4392
  background-clip: text;
4291
4393
  -webkit-background-clip: text;
4394
+ backface-visibility: hidden;
4292
4395
  transition:
4293
4396
  color 0.7s,
4294
4397
  background-color 0.7s,
@@ -4324,11 +4427,9 @@ AmLyrics$1.styles = i$3 `
4324
4427
  -0.5em 0%,
4325
4428
  -0.25em 0%;
4326
4429
  transform-origin: 50% 80%;
4327
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
4328
4430
  transition:
4329
4431
  transform 0.7s ease,
4330
4432
  color 0.18s;
4331
- will-change: background, transform;
4332
4433
  }
4333
4434
 
4334
4435
  .lyrics-line.active .lyrics-syllable span.char.highlight {
@@ -4380,6 +4481,8 @@ AmLyrics$1.styles = i$3 `
4380
4481
  box-sizing: content-box;
4381
4482
  background-clip: unset;
4382
4483
  transform-origin: top;
4484
+ content-visibility: visible !important;
4485
+ contain: none !important;
4383
4486
  transition:
4384
4487
  opacity 160ms ease-out,
4385
4488
  transform var(--scroll-duration, 280ms) var(--lyrics-line-delay, 0ms);
@@ -4390,41 +4493,35 @@ AmLyrics$1.styles = i$3 `
4390
4493
  transition:
4391
4494
  opacity 160ms ease-out,
4392
4495
  transform var(--scroll-duration, 280ms);
4393
- will-change: opacity;
4394
4496
  }
4395
4497
 
4396
4498
  /* Exiting state: quickly collapse width and height so dots don't distort page, or remove max-height transition */
4397
4499
  .lyrics-gap.gap-exiting {
4398
4500
  opacity: 1;
4399
- transition: transform var(--scroll-duration, 280ms);
4400
4501
  }
4401
4502
 
4402
4503
  .lyrics-gap .main-vocal-container {
4403
- transform: translateY(-25%) scale(1) translateZ(0);
4504
+ transform: translateY(-25%) scale(1);
4404
4505
  transition: transform 400ms cubic-bezier(0.22, 1, 0.36, 1);
4405
4506
  }
4406
4507
 
4407
- /* Jump animation plays during exit */
4408
- .lyrics-gap.gap-exiting .main-vocal-container {
4409
- animation: gap-ended var(--gap-exit-duration, 360ms)
4410
- cubic-bezier(0.33, 1, 0.68, 1) forwards;
4411
- }
4412
-
4413
4508
  .lyrics-gap:not(.active):not(.gap-exiting) .main-vocal-container {
4414
- transform: translateY(-25%) scale(0) translateZ(0);
4415
- }
4416
-
4417
- .lyrics-gap:not(.active):not(.gap-exiting)
4418
- .main-vocal-container
4419
- .lyrics-word {
4420
- animation-play-state: paused;
4509
+ transform: translateY(-25%) scale(0);
4421
4510
  }
4422
4511
 
4423
- .lyrics-gap.active .main-vocal-container .lyrics-word {
4512
+ /* Pulse — must come BEFORE .gap-exiting so exiting wins via specificity+order */
4513
+ .lyrics-gap.active .main-vocal-container {
4424
4514
  animation: gap-loop var(--gap-pulse-duration, 4000ms) ease-in-out infinite
4425
4515
  alternate;
4426
4516
  animation-delay: var(--gap-loop-delay, 0ms);
4427
- will-change: transform;
4517
+ }
4518
+
4519
+ /* Jump animation plays during exit — disable transition so animation wins.
4520
+ Placed AFTER .active so it wins when both classes are present briefly. */
4521
+ .lyrics-gap.gap-exiting .main-vocal-container {
4522
+ animation: gap-ended var(--gap-exit-duration, 360ms)
4523
+ cubic-bezier(0.33, 1, 0.68, 1) forwards;
4524
+ transition: none !important;
4428
4525
  }
4429
4526
 
4430
4527
  .lyrics-gap .lyrics-syllable {
@@ -4475,20 +4572,17 @@ AmLyrics$1.styles = i$3 `
4475
4572
  background-clip: unset;
4476
4573
  }
4477
4574
 
4478
- .lyrics-gap.active .lyrics-syllable.highlight,
4479
4575
  .lyrics-gap.active .lyrics-syllable.finished,
4480
- .lyrics-gap.gap-exiting .lyrics-syllable,
4481
- .lyrics-gap:not(.active).post-active-line .lyrics-syllable,
4482
- .lyrics-gap:not(.active).lyrics-activest .lyrics-syllable {
4576
+ .lyrics-gap.gap-exiting .lyrics-syllable.finished,
4577
+ .lyrics-gap:not(.active):not(.gap-exiting).post-active-line
4578
+ .lyrics-syllable,
4579
+ .lyrics-gap:not(.active):not(.gap-exiting).lyrics-activest
4580
+ .lyrics-syllable {
4483
4581
  background-color: var(--lyplus-text-primary);
4484
4582
  animation: none !important;
4485
4583
  opacity: 1;
4486
4584
  }
4487
4585
 
4488
- .lyrics-gap.active .lyrics-syllable.finished {
4489
- animation: none !important;
4490
- }
4491
-
4492
4586
  /* ==========================================================================
4493
4587
  METADATA & FOOTER STYLES
4494
4588
  ========================================================================== */
@@ -4517,12 +4611,49 @@ AmLyrics$1.styles = i$3 `
4517
4611
  align-items: center;
4518
4612
  flex-wrap: wrap;
4519
4613
  text-align: left;
4520
- font-size: 0.8em;
4521
- color: rgba(255, 255, 255, 0.5);
4522
- padding: 10px 0;
4523
- border-top: 1px solid rgba(255, 255, 255, 0.1);
4614
+ font-size: 1.2em;
4615
+ color: rgba(255, 255, 255, 0.6);
4616
+ padding: 20px 0 50vh 0;
4524
4617
  margin-top: 10px;
4525
- font-weight: normal;
4618
+ font-weight: 400;
4619
+ opacity: 0.8;
4620
+ transition:
4621
+ opacity 0.3s ease,
4622
+ transform 0.5s cubic-bezier(0.41, 0, 0.12, 0.99),
4623
+ filter 0.3s ease;
4624
+ transform-origin: left;
4625
+ }
4626
+
4627
+ .lyrics-footer.lyrics-line {
4628
+ font-size: 1.2em;
4629
+ padding: 20px var(--lyplus-padding-line) 50vh var(--lyplus-padding-line);
4630
+ cursor: default;
4631
+ }
4632
+
4633
+ .lyrics-footer.active {
4634
+ opacity: 1;
4635
+ color: rgba(255, 255, 255, 0.5); /* Grey instead of primary */
4636
+ }
4637
+
4638
+ .lyrics-footer.scroll-animate {
4639
+ transition: none !important;
4640
+ animation-name: lyrics-scroll;
4641
+ animation-duration: var(--scroll-duration, 280ms);
4642
+ animation-timing-function: cubic-bezier(0.41, 0, 0.12, 0.99);
4643
+ animation-fill-mode: both;
4644
+ animation-delay: var(--lyrics-line-delay, 0ms);
4645
+ }
4646
+
4647
+ .lyrics-container.blur-inactive-enabled:not(.not-focused)
4648
+ .lyrics-footer:not(.active) {
4649
+ filter: blur(var(--lyplus-blur-amount));
4650
+ opacity: 0.5;
4651
+ }
4652
+
4653
+ .lyrics-container.user-scrolling .lyrics-footer {
4654
+ transition: none !important;
4655
+ filter: none !important;
4656
+ opacity: 0.8 !important;
4526
4657
  }
4527
4658
 
4528
4659
  .lyrics-footer p {
@@ -4530,12 +4661,14 @@ AmLyrics$1.styles = i$3 `
4530
4661
  }
4531
4662
 
4532
4663
  .lyrics-footer a {
4533
- color: rgba(255, 255, 255, 0.7);
4534
- text-decoration: none;
4664
+ color: var(--lyplus-text-primary); /* Stand out using primary color */
4665
+ text-underline-offset: 2px;
4666
+ opacity: 0.8;
4667
+ transition: opacity 0.2s;
4535
4668
  }
4536
4669
 
4537
4670
  .lyrics-footer a:hover {
4538
- text-decoration: underline;
4671
+ opacity: 1;
4539
4672
  }
4540
4673
 
4541
4674
  .footer-content {
@@ -4659,6 +4792,7 @@ AmLyrics$1.styles = i$3 `
4659
4792
 
4660
4793
  .lyrics-romanization-container.rtl-text {
4661
4794
  direction: rtl !important;
4795
+ text-align: right;
4662
4796
  }
4663
4797
 
4664
4798
  .lyrics-romanization-container .lyrics-syllable {
@@ -4872,23 +5006,22 @@ AmLyrics$1.styles = i$3 `
4872
5006
  /* Gap dot animations */
4873
5007
  @keyframes gap-loop {
4874
5008
  from {
4875
- transform: scale(1.12);
5009
+ transform: translateY(-25%) scale(1.12);
4876
5010
  }
4877
5011
  to {
4878
- transform: scale(var(--gap-exit-scale, 0.85));
5012
+ transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85));
4879
5013
  }
4880
5014
  }
4881
5015
 
4882
5016
  @keyframes gap-ended {
4883
5017
  0% {
4884
- transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85))
4885
- translateZ(0);
5018
+ transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85));
4886
5019
  }
4887
5020
  35% {
4888
- transform: translateY(-5%) scale(1.08) translateZ(0);
5021
+ transform: translateY(-25%) scale(1.2);
4889
5022
  }
4890
5023
  100% {
4891
- transform: translateY(-25%) scale(0) translateZ(0);
5024
+ transform: translateY(-25%) scale(0);
4892
5025
  }
4893
5026
  }
4894
5027
 
@@ -4905,17 +5038,18 @@ AmLyrics$1.styles = i$3 `
4905
5038
  reflow in between) to reliably restart the animation each time */
4906
5039
  @keyframes lyrics-scroll {
4907
5040
  from {
4908
- transform: translateY(var(--scroll-delta)) translateZ(1px);
5041
+ transform: translate3d(0, var(--scroll-delta), 0);
4909
5042
  }
4910
5043
  to {
4911
- transform: translateY(0) translateZ(1px);
5044
+ transform: translate3d(0, 0, 0);
4912
5045
  }
4913
5046
  }
4914
5047
 
4915
- /* Character grow animation - exact copy from YouLyPlus */
5048
+ /* Character grow animation translate3d+scale3d for smooth transform,
5049
+ drop-shadow for glow (text-shadow doesn't work with background-clip:text) */
4916
5050
  @keyframes grow-dynamic {
4917
5051
  0% {
4918
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
5052
+ transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
4919
5053
  filter: drop-shadow(
4920
5054
  0 0 0
4921
5055
  color-mix(in srgb, var(--lyplus-lyrics-palette), transparent 100%)
@@ -4923,27 +5057,12 @@ AmLyrics$1.styles = i$3 `
4923
5057
  }
4924
5058
  25%,
4925
5059
  30% {
4926
- transform: matrix3d(
4927
- calc(var(--max-scale) * calc(var(--lyplus-font-size-base-grow) / 25)),
4928
- 0,
4929
- 0,
4930
- 0,
4931
- 0,
4932
- calc(var(--max-scale) * calc(var(--lyplus-font-size-base-grow) / 25)),
4933
- 0,
4934
- 0,
4935
- 0,
4936
- 0,
4937
- 1,
4938
- 0,
4939
- calc(
4940
- var(--char-offset-x, 0) *
4941
- calc(var(--lyplus-font-size-base-grow) / 25)
4942
- ),
4943
- var(--translate-y-peak, -2),
4944
- 0,
4945
- 1
4946
- );
5060
+ transform: translate3d(
5061
+ var(--char-offset-x, 0px),
5062
+ var(--translate-y-peak, -2px),
5063
+ 0
5064
+ )
5065
+ scale3d(var(--matrix-scale, 1.1), var(--matrix-scale, 1.1), 1);
4947
5066
  filter: drop-shadow(
4948
5067
  0 0 0.1em
4949
5068
  color-mix(
@@ -4953,8 +5072,10 @@ AmLyrics$1.styles = i$3 `
4953
5072
  )
4954
5073
  );
4955
5074
  }
5075
+ 75%,
4956
5076
  100% {
4957
- transform: translateY(-3.5%) translateZ(1px);
5077
+ transform: translate3d(0, var(--char-rise-y, -1.12px), 0)
5078
+ scale3d(1, 1, 1);
4958
5079
  filter: drop-shadow(
4959
5080
  0 0 0
4960
5081
  color-mix(in srgb, var(--lyplus-lyrics-palette), transparent 100%)
@@ -5086,15 +5207,15 @@ __decorate([
5086
5207
  __decorate([
5087
5208
  n({ type: String, attribute: 'song-album' })
5088
5209
  ], AmLyrics$1.prototype, "songAlbum", void 0);
5210
+ __decorate([
5211
+ n({ type: String, attribute: 'songwriters' })
5212
+ ], AmLyrics$1.prototype, "songwriters", void 0);
5089
5213
  __decorate([
5090
5214
  n({ type: Number, attribute: 'song-duration' })
5091
5215
  ], AmLyrics$1.prototype, "songDurationMs", void 0);
5092
5216
  __decorate([
5093
5217
  n({ type: String, attribute: 'highlight-color' })
5094
5218
  ], AmLyrics$1.prototype, "highlightColor", void 0);
5095
- __decorate([
5096
- n({ type: String, attribute: 'hover-background-color' })
5097
- ], AmLyrics$1.prototype, "hoverBackgroundColor", void 0);
5098
5219
  __decorate([
5099
5220
  n({ type: String, attribute: 'font-family' })
5100
5221
  ], AmLyrics$1.prototype, "fontFamily", void 0);