@scarlett-player/ui 1.9.0 → 1.11.0

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/index.cjs CHANGED
@@ -23,8 +23,8 @@ __export(index_exports, {
23
23
  DEFAULT_PRIORITY: () => DEFAULT_PRIORITY,
24
24
  assertFitLayout: () => assertFitLayout,
25
25
  default: () => index_default,
26
- formatLiveTime: () => formatLiveTime,
27
- formatTime: () => formatTime,
26
+ formatLiveTime: () => import_core2.formatLiveTime,
27
+ formatTime: () => import_core2.formatTime,
28
28
  getControlFactory: () => getControlFactory,
29
29
  icons: () => icons,
30
30
  planFit: () => planFit,
@@ -37,7 +37,7 @@ __export(index_exports, {
37
37
  unregisterControlsFor: () => unregisterControlsFor
38
38
  });
39
39
  module.exports = __toCommonJS(index_exports);
40
- var import_core2 = require("@scarlett-player/core");
40
+ var import_core4 = require("@scarlett-player/core");
41
41
 
42
42
  // src/fit.ts
43
43
  var UNKNOWN_RANK = 3;
@@ -1204,24 +1204,30 @@ var styles = `
1204
1204
 
1205
1205
  /* ============================================
1206
1206
  CSS Custom Properties (Theming)
1207
+ ============================================
1208
+
1209
+ Token defaults live at the use sites as var() fallbacks, not in a :root
1210
+ block. A :root declaration wrote --sp-accent, --sp-color, --sp-bg,
1211
+ --sp-control-height and --sp-icon-size into the HOST document, so mounting a
1212
+ player changed variables the page may own. A .sp-container declaration would
1213
+ be no better: the container rule wins for every descendant, so it would
1214
+ override a host that themes the player from its own :root. A var() fallback
1215
+ does neither - it applies only where nothing else defines the token.
1216
+
1217
+ setTheme() writes the same names onto the player's container, which still
1218
+ wins over the fallbacks.
1207
1219
  ============================================ */
1208
- :root {
1209
- --sp-accent: #e50914;
1210
- --sp-color: #fff;
1211
- --sp-bg: rgba(0, 0, 0, 0.8);
1212
- --sp-control-height: 48px;
1213
- --sp-icon-size: 24px;
1214
- }
1215
1220
  `;
1216
1221
 
1217
1222
  // src/icons.ts
1223
+ var import_core = require("@scarlett-player/core");
1218
1224
  var icons = {
1219
- play: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`,
1225
+ play: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${import_core.SHARED_ICON_PATHS.play}"/></svg>`,
1220
1226
  pause: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>`,
1221
1227
  replay: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>`,
1222
- volumeHigh: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>`,
1228
+ volumeHigh: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${import_core.SHARED_ICON_PATHS.volumeHigh}"/></svg>`,
1223
1229
  volumeLow: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>`,
1224
- volumeMute: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>`,
1230
+ volumeMute: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${import_core.SHARED_ICON_PATHS.volumeMuted}"/></svg>`,
1225
1231
  fullscreen: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>`,
1226
1232
  exitFullscreen: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>`,
1227
1233
  pip: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>`,
@@ -1294,29 +1300,7 @@ function setAttr(el, name, value) {
1294
1300
  }
1295
1301
 
1296
1302
  // src/utils/format.ts
1297
- function formatTime(seconds) {
1298
- if (!isFinite(seconds) || isNaN(seconds)) {
1299
- return "0:00";
1300
- }
1301
- const absSeconds = Math.abs(seconds);
1302
- const h = Math.floor(absSeconds / 3600);
1303
- const m = Math.floor(absSeconds % 3600 / 60);
1304
- const s = Math.floor(absSeconds % 60);
1305
- const sign = seconds < 0 ? "-" : "";
1306
- if (h > 0) {
1307
- return `${sign}${h}:${pad(m)}:${pad(s)}`;
1308
- }
1309
- return `${sign}${m}:${pad(s)}`;
1310
- }
1311
- function pad(n) {
1312
- return n < 10 ? `0${n}` : `${n}`;
1313
- }
1314
- function formatLiveTime(behindLive) {
1315
- if (behindLive <= 0) {
1316
- return "LIVE";
1317
- }
1318
- return `-${formatTime(behindLive)}`;
1319
- }
1303
+ var import_core2 = require("@scarlett-player/core");
1320
1304
 
1321
1305
  // src/controls/PlayButton.ts
1322
1306
  var PlayButton = class {
@@ -1621,7 +1605,7 @@ var ProgressBar = class {
1621
1605
  video.addEventListener("seeked", resumePlayback);
1622
1606
  }
1623
1607
  }
1624
- this.tooltip.style.opacity = "0";
1608
+ this.tooltip.style.opacity = "";
1625
1609
  this.thumbnailPreview.hide();
1626
1610
  }
1627
1611
  };
@@ -1630,7 +1614,7 @@ var ProgressBar = class {
1630
1614
  };
1631
1615
  this.onMouseLeave = () => {
1632
1616
  if (!this.isDragging) {
1633
- this.tooltip.style.opacity = "0";
1617
+ this.tooltip.style.opacity = "";
1634
1618
  this.thumbnailPreview.hide();
1635
1619
  }
1636
1620
  };
@@ -1774,7 +1758,7 @@ var ProgressBar = class {
1774
1758
  }
1775
1759
  this.el.setAttribute("aria-valuemax", String(Math.floor(duration)));
1776
1760
  this.el.setAttribute("aria-valuenow", String(Math.floor(currentTime)));
1777
- this.el.setAttribute("aria-valuetext", formatTime(currentTime));
1761
+ this.el.setAttribute("aria-valuetext", (0, import_core2.formatTime)(currentTime));
1778
1762
  }
1779
1763
  }
1780
1764
  /**
@@ -1863,9 +1847,9 @@ var ProgressBar = class {
1863
1847
  const seekableRange = this.api.getState("seekableRange");
1864
1848
  if (live && seekableRange) {
1865
1849
  const behindLive = seekableRange.end - time;
1866
- this.tooltip.textContent = formatLiveTime(behindLive);
1850
+ this.tooltip.textContent = (0, import_core2.formatLiveTime)(behindLive);
1867
1851
  } else {
1868
- this.tooltip.textContent = formatTime(time);
1852
+ this.tooltip.textContent = (0, import_core2.formatTime)(time);
1869
1853
  }
1870
1854
  const chapterLabel = this.chapterLabelAt(time);
1871
1855
  if (chapterLabel) {
@@ -1902,6 +1886,7 @@ var ProgressBar = class {
1902
1886
  this.wrapper.removeEventListener("mousemove", this.onMouseMove);
1903
1887
  this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
1904
1888
  this.wrapper.removeEventListener("touchstart", this.onTouchStart);
1889
+ this.el.removeEventListener("keydown", this.onKeyDown);
1905
1890
  document.removeEventListener("mousemove", this.onDocMouseMove);
1906
1891
  document.removeEventListener("mouseup", this.onMouseUp);
1907
1892
  document.removeEventListener("touchmove", this.onDocTouchMove);
@@ -1930,12 +1915,12 @@ var TimeDisplay = class {
1930
1915
  const seekableRange = this.api.getState("seekableRange");
1931
1916
  if (seekableRange) {
1932
1917
  const behindLive = seekableRange.end - currentTime;
1933
- this.el.textContent = formatLiveTime(behindLive);
1918
+ this.el.textContent = (0, import_core2.formatLiveTime)(behindLive);
1934
1919
  } else {
1935
- this.el.textContent = formatLiveTime(0);
1920
+ this.el.textContent = (0, import_core2.formatLiveTime)(0);
1936
1921
  }
1937
1922
  } else {
1938
- this.el.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
1923
+ this.el.textContent = `${(0, import_core2.formatTime)(currentTime)} / ${(0, import_core2.formatTime)(duration)}`;
1939
1924
  }
1940
1925
  }
1941
1926
  destroy() {
@@ -2420,7 +2405,7 @@ var PipButton = class {
2420
2405
  };
2421
2406
 
2422
2407
  // src/controls/FullscreenButton.ts
2423
- var import_core = require("@scarlett-player/core");
2408
+ var import_core3 = require("@scarlett-player/core");
2424
2409
  var FullscreenButton = class {
2425
2410
  constructor(api) {
2426
2411
  this.clickHandler = () => {
@@ -2455,10 +2440,10 @@ var FullscreenButton = class {
2455
2440
  async toggle() {
2456
2441
  const container = this.api.container;
2457
2442
  try {
2458
- if ((0, import_core.isFullscreen)(container)) {
2459
- await (0, import_core.exitFullscreen)(container);
2443
+ if ((0, import_core3.isFullscreen)(container)) {
2444
+ await (0, import_core3.exitFullscreen)(container);
2460
2445
  } else {
2461
- await (0, import_core.enterFullscreen)(container);
2446
+ await (0, import_core3.enterFullscreen)(container);
2462
2447
  }
2463
2448
  } catch {
2464
2449
  }
@@ -3446,7 +3431,7 @@ function resetControlRegistry() {
3446
3431
  }
3447
3432
 
3448
3433
  // src/version.ts
3449
- var PKG_VERSION = true ? "1.9.0" : "0.0.0-dev";
3434
+ var PKG_VERSION = true ? "1.11.0" : "0.0.0-dev";
3450
3435
 
3451
3436
  // src/index.ts
3452
3437
  var DEFAULT_LAYOUT = [
@@ -3483,6 +3468,7 @@ var FALLBACK_VOLUME_SLIDER_WIDTH = 64;
3483
3468
  var OVERFLOW_BUTTON_WIDTH = 44;
3484
3469
  var FALLBACK_BAR_PADDING_X = 24;
3485
3470
  var FALLBACK_BAR_GAP = 4;
3471
+ var RESIZE_FALLBACK_DEBOUNCE_MS = 100;
3486
3472
  var MENU_HEIGHT_RESERVE = 16;
3487
3473
  var FALLBACK_BAR_HEIGHT = 56;
3488
3474
  var MIN_MENU_HEIGHT = 120;
@@ -3509,6 +3495,8 @@ function uiPlugin(config = {}) {
3509
3495
  let entries = [];
3510
3496
  let timeEntry = null;
3511
3497
  let resizeObserver = null;
3498
+ let windowResizeTimer = null;
3499
+ let onWindowResize = null;
3512
3500
  let barPaddingX = FALLBACK_BAR_PADDING_X;
3513
3501
  let barGap = FALLBACK_BAR_GAP;
3514
3502
  let volumeSliderWidth = FALLBACK_VOLUME_SLIDER_WIDTH;
@@ -3873,11 +3861,11 @@ function uiPlugin(config = {}) {
3873
3861
  break;
3874
3862
  case "f":
3875
3863
  e.preventDefault();
3876
- if ((0, import_core2.isFullscreen)(api.container)) {
3877
- (0, import_core2.exitFullscreen)(api.container).catch(() => {
3864
+ if ((0, import_core4.isFullscreen)(api.container)) {
3865
+ (0, import_core4.exitFullscreen)(api.container).catch(() => {
3878
3866
  });
3879
3867
  } else {
3880
- (0, import_core2.enterFullscreen)(api.container).catch(() => {
3868
+ (0, import_core4.enterFullscreen)(api.container).catch(() => {
3881
3869
  });
3882
3870
  }
3883
3871
  break;
@@ -3918,7 +3906,7 @@ function uiPlugin(config = {}) {
3918
3906
  version: PKG_VERSION,
3919
3907
  async init(pluginApi) {
3920
3908
  api = pluginApi;
3921
- releaseStyles = (0, import_core2.injectSharedStyles)(STYLE_ID, styles);
3909
+ releaseStyles = (0, import_core4.injectSharedStyles)(STYLE_ID, styles);
3922
3910
  if (config.theme) {
3923
3911
  this.setTheme(config.theme);
3924
3912
  }
@@ -3987,6 +3975,17 @@ function uiPlugin(config = {}) {
3987
3975
  scheduleUpdate();
3988
3976
  });
3989
3977
  resizeObserver.observe(container);
3978
+ } else if (responsive && typeof window !== "undefined") {
3979
+ onWindowResize = () => {
3980
+ if (windowResizeTimer) clearTimeout(windowResizeTimer);
3981
+ windowResizeTimer = setTimeout(() => {
3982
+ windowResizeTimer = null;
3983
+ applyMenuBounds(container.clientHeight);
3984
+ fitPending = true;
3985
+ scheduleUpdate();
3986
+ }, RESIZE_FALLBACK_DEBOUNCE_MS);
3987
+ };
3988
+ window.addEventListener("resize", onWindowResize);
3990
3989
  }
3991
3990
  controlRegistryUnsubscribe = onControlRegistered((id, owner) => {
3992
3991
  if (!layout.includes(id)) {
@@ -4029,6 +4028,14 @@ function uiPlugin(config = {}) {
4029
4028
  }
4030
4029
  resizeObserver?.disconnect();
4031
4030
  resizeObserver = null;
4031
+ if (onWindowResize) {
4032
+ window.removeEventListener("resize", onWindowResize);
4033
+ onWindowResize = null;
4034
+ }
4035
+ if (windowResizeTimer) {
4036
+ clearTimeout(windowResizeTimer);
4037
+ windowResizeTimer = null;
4038
+ }
4032
4039
  api?.container?.style.removeProperty("--sp-menu-max-height");
4033
4040
  stateUnsubscribe?.();
4034
4041
  stateUnsubscribe = null;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Plugin, IPluginAPI } from '@scarlett-player/core';
2
+ export { formatLiveTime, formatTime } from '@scarlett-player/core';
2
3
 
3
4
  /**
4
5
  * Control bar fit planning.
@@ -394,6 +395,10 @@ declare function resetControlRegistry(): void;
394
395
  *
395
396
  * Clean, minimal icons - 24x24 viewBox with currentColor fill.
396
397
  * Optimized for clarity at small sizes.
398
+ *
399
+ * The three glyphs `audio-ui` draws identically come from core's
400
+ * `SHARED_ICON_PATHS`; the `<svg>` wrapper stays here because this package
401
+ * paints with `fill="currentColor"` and `audio-ui` does not.
397
402
  */
398
403
  declare const icons: {
399
404
  readonly play: "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>";
@@ -431,19 +436,7 @@ declare const icons: {
431
436
  * Modern, minimal design inspired by Mux Player and Vidstack.
432
437
  * Uses CSS custom properties for theming.
433
438
  */
434
- declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n /* Own stacking context: the control bar, menus and overlays all carry\n z-index, and without this they compete with the host page's own layers\n instead of staying inside the player. */\n isolation: isolate;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n /* Composed through a variable so the fullscreen rule below can add the\n device's own inset without restating the 12px. */\n padding-bottom: calc(12px + var(--sp-inset-bottom, 0px));\n gap: 4px;\n /* Declared on the bar because the fit needs the same number the volume rules\n below use: the slider expands mid-interaction and the plugin reserves the\n room in advance (see interactionReserve() in index.ts, which reads this\n property off this element at init). One declaration, so the stylesheet and\n the arithmetic cannot drift. Both the bar and the overflow tray are inside\n .sp-controls, so a volume control inherits it wherever the fit put it. */\n --sp-volume-slider-width: 64px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Safe Area (fullscreen only)\n\n Scoped to :fullscreen on purpose. Applied unconditionally, the inset would\n push an inline player's controls up on any page whose viewport meta says\n viewport-fit=cover, where there is no notch or home indicator over the\n player at all. Both the bar and the progress wrapper are direct children of\n the container, which is the element that goes fullscreen.\n\n The :-webkit-full-screen twin is a separate rule because an unknown\n pseudo-class anywhere in a selector list invalidates the whole rule.\n\n Nothing is needed in packages/embed/iframe.html: viewport-fit has no effect\n inside an iframe.\n ============================================ */\n:fullscreen > .sp-controls,\n:fullscreen > .sp-progress-wrapper {\n --sp-inset-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n:-webkit-full-screen > .sp-controls,\n:-webkit-full-screen > .sp-progress-wrapper {\n --sp-inset-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: calc(48px + var(--sp-inset-bottom, 0px));\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* Touch: a 20px wrapper is not a 20px target. The control bar is a later\n sibling at the same z-index and spans 0..56px from the bottom, so it wins\n hit-testing in the 48..56 overlap and the exclusive region for scrubbing is\n 12px. The wrapper grows UPWARD to 44px (48..92) because growing downward\n would be swallowed by the bar; the 3px bar itself stays exactly where it was\n (centred 8.5px above the wrapper's bottom edge, which is what\n align-items: center gave it inside 20px). The handle and tooltip\n enlargements are gated behind (hover: hover) and never match a finger, but\n .sp-progress--dragging is not, so the handle still appears mid-drag.\n\n any-pointer, not pointer: (pointer: coarse) describes the PRIMARY pointer\n only, so a hybrid laptop with a mouse and a touchscreen reports fine and kept\n the 12px exclusive region under a finger. (any-pointer: coarse) is true\n whenever a coarse pointer is available at all, which is the population that\n needs the target. The cost on such a machine is 24px of extra hit area for\n the mouse, over the player's own bottom edge. */\n@media (any-pointer: coarse) {\n .sp-progress-wrapper {\n height: 44px;\n align-items: flex-end;\n padding-bottom: 8.5px;\n box-sizing: border-box;\n }\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n/* Chapter markers */\n.sp-progress__markers {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n\n.sp-progress__marker {\n position: absolute;\n top: 0;\n width: 2px;\n height: 100%;\n margin-left: -1px;\n background: rgba(0, 0, 0, 0.65);\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n.sp-progress__tooltip-chapter {\n display: block;\n max-width: 220px;\n overflow: hidden;\n color: rgba(255, 255, 255, 0.75);\n font-weight: 400;\n font-variant-numeric: normal;\n text-overflow: ellipsis;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Overflow Tray\n\n The wrapper is deliberately unpositioned: the strip is absolutely\n positioned against .sp-controls (the nearest positioned ancestor), so it\n spans the bar's width and sits directly above it instead of hanging off a\n 44px button.\n\n The strip wraps horizontally and keeps overflow visible. A vertical menu of\n 44px rows would be taller than a portrait phone player (211px at 375px wide,\n measured 2026-09-05) and a scrolling one would clip the popovers registered\n controls own.\n ============================================ */\n.sp-overflow {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n.sp-overflow-tray {\n position: absolute;\n bottom: 100%;\n left: 0;\n right: 0;\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 4px;\n padding: 8px 12px;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n overflow: visible;\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-overflow-tray--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Beats a control's own inline style.display = '' on its next update(), so a\n control the fit took off screen stays off screen until the fit says\n otherwise. */\n.sp-control--collapsed {\n display: none !important;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n/* Both widths come from --sp-volume-slider-width on .sp-controls, which is also\n what the fit reserves for this control. focus-within is deliberately not\n gated on hover: a tap on the mute button focuses it, which is how the slider\n opens on a phone. */\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: var(--sp-volume-slider-width);\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: var(--sp-volume-slider-width);\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n /* Bounded to the player, see .sp-settings-panel. border-box because the\n bound is a content-box height by default and this menu adds 8px of padding\n top and bottom: at the 139px bound a 211px player gives, it rendered 155px\n and the host clipped the last 16px of it. */\n box-sizing: border-box;\n max-height: var(--sp-menu-max-height, none);\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n /* Bounded to the room above the control bar, written by the UI plugin's\n ResizeObserver as max(120px, container height - the bar's measured height\n - 16px). The bar is measured rather than assumed because its\n padding-bottom carries the safe-area inset in fullscreen, which moves the\n anchor these menus hang from. The Speed sub-panel is 253px (a\n 37px header plus six 36px rows) against a 211px portrait phone player, so\n without this the host's overflow: hidden cuts off the Back header and the\n first three speeds and playback speed is unreachable (measured at 375x211\n on 2026-09-05). With the variable unset the panel behaves exactly as it\n did before.\n\n Not applied to .sp-overflow-tray: that one has to keep overflow visible so\n the popovers its adopted controls own are not clipped.\n\n border-box because max-height bounds the content box: .sp-settings-panel--main\n is this same element with 4px of padding top and bottom, so wherever the\n bound binds the main menu, it rendered 8px past it and the host clipped the\n difference. The --sub views set padding: 0 and were already exact, which is\n why the browser harness's speed-panel check could not see this. */\n box-sizing: border-box;\n max-height: var(--sp-menu-max-height, none);\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Big Play Button\n\n z-index 12 puts it above the gradient (5) and above the gestures plugin's\n tap surface (6), so a tap lands on the button and starts playback instead\n of being read as a tap-to-toggle-controls gesture - exactly how the control\n bar's play button (10) already behaves. It stays below the spinner (15) and\n the error overlay (25), both of which own the middle of the picture when\n they are up.\n\n Hidden with visibility, not opacity alone, so it takes no pointer events\n while it is away.\n ============================================ */\n.sp-big-play {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 12;\n display: flex;\n align-items: center;\n justify-content: center;\n /* Comfortably past the 44px minimum touch target the control bar uses. */\n width: 72px;\n height: 72px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--sp-accent, #e50914);\n color: #fff;\n cursor: pointer;\n opacity: 0;\n visibility: hidden;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);\n transition: opacity 0.2s ease, visibility 0.2s, transform 0.15s ease,\n background 0.15s ease;\n}\n\n.sp-big-play--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-big-play svg {\n width: 36px;\n height: 36px;\n fill: currentColor;\n /* Optical centring: the play triangle's mass sits left of the glyph box. */\n margin-left: 3px;\n}\n\n.sp-big-play:hover {\n transform: translate(-50%, -50%) scale(1.06);\n}\n\n.sp-big-play:active {\n transform: translate(-50%, -50%) scale(0.96);\n}\n\n.sp-big-play:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 3px;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n/* Reconnecting: pulse the icon so the overlay reads as active work,\n not a dead-end error */\n.sp-error-overlay--reconnecting .sp-error-overlay__icon {\n animation: sp-reconnect-pulse 1.2s ease-in-out infinite;\n}\n\n@keyframes sp-reconnect-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-overflow-tray,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-big-play,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-big-play:hover,\n .sp-big-play:active {\n transform: translate(-50%, -50%);\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
435
-
436
- /**
437
- * Formatting utility functions
438
- */
439
- /**
440
- * Format seconds as time string (mm:ss or h:mm:ss).
441
- */
442
- declare function formatTime(seconds: number): string;
443
- /**
444
- * Format time for live streams (relative to live edge).
445
- */
446
- declare function formatLiveTime(behindLive: number): string;
439
+ declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n /* Own stacking context: the control bar, menus and overlays all carry\n z-index, and without this they compete with the host page's own layers\n instead of staying inside the player. */\n isolation: isolate;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n /* Composed through a variable so the fullscreen rule below can add the\n device's own inset without restating the 12px. */\n padding-bottom: calc(12px + var(--sp-inset-bottom, 0px));\n gap: 4px;\n /* Declared on the bar because the fit needs the same number the volume rules\n below use: the slider expands mid-interaction and the plugin reserves the\n room in advance (see interactionReserve() in index.ts, which reads this\n property off this element at init). One declaration, so the stylesheet and\n the arithmetic cannot drift. Both the bar and the overflow tray are inside\n .sp-controls, so a volume control inherits it wherever the fit put it. */\n --sp-volume-slider-width: 64px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Safe Area (fullscreen only)\n\n Scoped to :fullscreen on purpose. Applied unconditionally, the inset would\n push an inline player's controls up on any page whose viewport meta says\n viewport-fit=cover, where there is no notch or home indicator over the\n player at all. Both the bar and the progress wrapper are direct children of\n the container, which is the element that goes fullscreen.\n\n The :-webkit-full-screen twin is a separate rule because an unknown\n pseudo-class anywhere in a selector list invalidates the whole rule.\n\n Nothing is needed in packages/embed/iframe.html: viewport-fit has no effect\n inside an iframe.\n ============================================ */\n:fullscreen > .sp-controls,\n:fullscreen > .sp-progress-wrapper {\n --sp-inset-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n:-webkit-full-screen > .sp-controls,\n:-webkit-full-screen > .sp-progress-wrapper {\n --sp-inset-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: calc(48px + var(--sp-inset-bottom, 0px));\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* Touch: a 20px wrapper is not a 20px target. The control bar is a later\n sibling at the same z-index and spans 0..56px from the bottom, so it wins\n hit-testing in the 48..56 overlap and the exclusive region for scrubbing is\n 12px. The wrapper grows UPWARD to 44px (48..92) because growing downward\n would be swallowed by the bar; the 3px bar itself stays exactly where it was\n (centred 8.5px above the wrapper's bottom edge, which is what\n align-items: center gave it inside 20px). The handle and tooltip\n enlargements are gated behind (hover: hover) and never match a finger, but\n .sp-progress--dragging is not, so the handle still appears mid-drag.\n\n any-pointer, not pointer: (pointer: coarse) describes the PRIMARY pointer\n only, so a hybrid laptop with a mouse and a touchscreen reports fine and kept\n the 12px exclusive region under a finger. (any-pointer: coarse) is true\n whenever a coarse pointer is available at all, which is the population that\n needs the target. The cost on such a machine is 24px of extra hit area for\n the mouse, over the player's own bottom edge. */\n@media (any-pointer: coarse) {\n .sp-progress-wrapper {\n height: 44px;\n align-items: flex-end;\n padding-bottom: 8.5px;\n box-sizing: border-box;\n }\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n/* Chapter markers */\n.sp-progress__markers {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n\n.sp-progress__marker {\n position: absolute;\n top: 0;\n width: 2px;\n height: 100%;\n margin-left: -1px;\n background: rgba(0, 0, 0, 0.65);\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n.sp-progress__tooltip-chapter {\n display: block;\n max-width: 220px;\n overflow: hidden;\n color: rgba(255, 255, 255, 0.75);\n font-weight: 400;\n font-variant-numeric: normal;\n text-overflow: ellipsis;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Overflow Tray\n\n The wrapper is deliberately unpositioned: the strip is absolutely\n positioned against .sp-controls (the nearest positioned ancestor), so it\n spans the bar's width and sits directly above it instead of hanging off a\n 44px button.\n\n The strip wraps horizontally and keeps overflow visible. A vertical menu of\n 44px rows would be taller than a portrait phone player (211px at 375px wide,\n measured 2026-09-05) and a scrolling one would clip the popovers registered\n controls own.\n ============================================ */\n.sp-overflow {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n.sp-overflow-tray {\n position: absolute;\n bottom: 100%;\n left: 0;\n right: 0;\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 4px;\n padding: 8px 12px;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n overflow: visible;\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-overflow-tray--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Beats a control's own inline style.display = '' on its next update(), so a\n control the fit took off screen stays off screen until the fit says\n otherwise. */\n.sp-control--collapsed {\n display: none !important;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n/* Both widths come from --sp-volume-slider-width on .sp-controls, which is also\n what the fit reserves for this control. focus-within is deliberately not\n gated on hover: a tap on the mute button focuses it, which is how the slider\n opens on a phone. */\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: var(--sp-volume-slider-width);\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: var(--sp-volume-slider-width);\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n /* Bounded to the player, see .sp-settings-panel. border-box because the\n bound is a content-box height by default and this menu adds 8px of padding\n top and bottom: at the 139px bound a 211px player gives, it rendered 155px\n and the host clipped the last 16px of it. */\n box-sizing: border-box;\n max-height: var(--sp-menu-max-height, none);\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n /* Bounded to the room above the control bar, written by the UI plugin's\n ResizeObserver as max(120px, container height - the bar's measured height\n - 16px). The bar is measured rather than assumed because its\n padding-bottom carries the safe-area inset in fullscreen, which moves the\n anchor these menus hang from. The Speed sub-panel is 253px (a\n 37px header plus six 36px rows) against a 211px portrait phone player, so\n without this the host's overflow: hidden cuts off the Back header and the\n first three speeds and playback speed is unreachable (measured at 375x211\n on 2026-09-05). With the variable unset the panel behaves exactly as it\n did before.\n\n Not applied to .sp-overflow-tray: that one has to keep overflow visible so\n the popovers its adopted controls own are not clipped.\n\n border-box because max-height bounds the content box: .sp-settings-panel--main\n is this same element with 4px of padding top and bottom, so wherever the\n bound binds the main menu, it rendered 8px past it and the host clipped the\n difference. The --sub views set padding: 0 and were already exact, which is\n why the browser harness's speed-panel check could not see this. */\n box-sizing: border-box;\n max-height: var(--sp-menu-max-height, none);\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Big Play Button\n\n z-index 12 puts it above the gradient (5) and above the gestures plugin's\n tap surface (6), so a tap lands on the button and starts playback instead\n of being read as a tap-to-toggle-controls gesture - exactly how the control\n bar's play button (10) already behaves. It stays below the spinner (15) and\n the error overlay (25), both of which own the middle of the picture when\n they are up.\n\n Hidden with visibility, not opacity alone, so it takes no pointer events\n while it is away.\n ============================================ */\n.sp-big-play {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 12;\n display: flex;\n align-items: center;\n justify-content: center;\n /* Comfortably past the 44px minimum touch target the control bar uses. */\n width: 72px;\n height: 72px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--sp-accent, #e50914);\n color: #fff;\n cursor: pointer;\n opacity: 0;\n visibility: hidden;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);\n transition: opacity 0.2s ease, visibility 0.2s, transform 0.15s ease,\n background 0.15s ease;\n}\n\n.sp-big-play--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-big-play svg {\n width: 36px;\n height: 36px;\n fill: currentColor;\n /* Optical centring: the play triangle's mass sits left of the glyph box. */\n margin-left: 3px;\n}\n\n.sp-big-play:hover {\n transform: translate(-50%, -50%) scale(1.06);\n}\n\n.sp-big-play:active {\n transform: translate(-50%, -50%) scale(0.96);\n}\n\n.sp-big-play:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 3px;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n/* Reconnecting: pulse the icon so the overlay reads as active work,\n not a dead-end error */\n.sp-error-overlay--reconnecting .sp-error-overlay__icon {\n animation: sp-reconnect-pulse 1.2s ease-in-out infinite;\n}\n\n@keyframes sp-reconnect-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-overflow-tray,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-big-play,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-big-play:hover,\n .sp-big-play:active {\n transform: translate(-50%, -50%);\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================\n\n Token defaults live at the use sites as var() fallbacks, not in a :root\n block. A :root declaration wrote --sp-accent, --sp-color, --sp-bg,\n --sp-control-height and --sp-icon-size into the HOST document, so mounting a\n player changed variables the page may own. A .sp-container declaration would\n be no better: the container rule wins for every descendant, so it would\n override a host that themes the player from its own :root. A var() fallback\n does neither - it applies only where nothing else defines the token.\n\n setTheme() writes the same names onto the player's container, which still\n wins over the fallbacks.\n ============================================ */\n";
447
440
 
448
441
  /**
449
442
  * UI Controls Plugin for Scarlett Player
@@ -480,4 +473,4 @@ declare function formatLiveTime(behindLive: number): string;
480
473
  */
481
474
  declare function uiPlugin(config?: UIPluginConfig): IUIPlugin;
482
475
 
483
- export { type BuiltinControlSlot, type Control, type ControlFactory, type ControlSlot, DEFAULT_PRIORITY, type FitExit, type FitItem, type FitPlan, type FitRank, type FitRule, type FitTemplate, type IUIPlugin, type LayoutConfig, type RegisterControlOptions, type ThemeConfig, type UIPluginConfig, assertFitLayout, uiPlugin as default, formatLiveTime, formatTime, getControlFactory, icons, planFit, registerControl, resetControlRegistry, resolveFitItems, styles, uiPlugin, unregisterControl, unregisterControlsFor };
476
+ export { type BuiltinControlSlot, type Control, type ControlFactory, type ControlSlot, DEFAULT_PRIORITY, type FitExit, type FitItem, type FitPlan, type FitRank, type FitRule, type FitTemplate, type IUIPlugin, type LayoutConfig, type RegisterControlOptions, type ThemeConfig, type UIPluginConfig, assertFitLayout, uiPlugin as default, getControlFactory, icons, planFit, registerControl, resetControlRegistry, resolveFitItems, styles, uiPlugin, unregisterControl, unregisterControlsFor };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Plugin, IPluginAPI } from '@scarlett-player/core';
2
+ export { formatLiveTime, formatTime } from '@scarlett-player/core';
2
3
 
3
4
  /**
4
5
  * Control bar fit planning.
@@ -394,6 +395,10 @@ declare function resetControlRegistry(): void;
394
395
  *
395
396
  * Clean, minimal icons - 24x24 viewBox with currentColor fill.
396
397
  * Optimized for clarity at small sizes.
398
+ *
399
+ * The three glyphs `audio-ui` draws identically come from core's
400
+ * `SHARED_ICON_PATHS`; the `<svg>` wrapper stays here because this package
401
+ * paints with `fill="currentColor"` and `audio-ui` does not.
397
402
  */
398
403
  declare const icons: {
399
404
  readonly play: "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>";
@@ -431,19 +436,7 @@ declare const icons: {
431
436
  * Modern, minimal design inspired by Mux Player and Vidstack.
432
437
  * Uses CSS custom properties for theming.
433
438
  */
434
- declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n /* Own stacking context: the control bar, menus and overlays all carry\n z-index, and without this they compete with the host page's own layers\n instead of staying inside the player. */\n isolation: isolate;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n /* Composed through a variable so the fullscreen rule below can add the\n device's own inset without restating the 12px. */\n padding-bottom: calc(12px + var(--sp-inset-bottom, 0px));\n gap: 4px;\n /* Declared on the bar because the fit needs the same number the volume rules\n below use: the slider expands mid-interaction and the plugin reserves the\n room in advance (see interactionReserve() in index.ts, which reads this\n property off this element at init). One declaration, so the stylesheet and\n the arithmetic cannot drift. Both the bar and the overflow tray are inside\n .sp-controls, so a volume control inherits it wherever the fit put it. */\n --sp-volume-slider-width: 64px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Safe Area (fullscreen only)\n\n Scoped to :fullscreen on purpose. Applied unconditionally, the inset would\n push an inline player's controls up on any page whose viewport meta says\n viewport-fit=cover, where there is no notch or home indicator over the\n player at all. Both the bar and the progress wrapper are direct children of\n the container, which is the element that goes fullscreen.\n\n The :-webkit-full-screen twin is a separate rule because an unknown\n pseudo-class anywhere in a selector list invalidates the whole rule.\n\n Nothing is needed in packages/embed/iframe.html: viewport-fit has no effect\n inside an iframe.\n ============================================ */\n:fullscreen > .sp-controls,\n:fullscreen > .sp-progress-wrapper {\n --sp-inset-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n:-webkit-full-screen > .sp-controls,\n:-webkit-full-screen > .sp-progress-wrapper {\n --sp-inset-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: calc(48px + var(--sp-inset-bottom, 0px));\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* Touch: a 20px wrapper is not a 20px target. The control bar is a later\n sibling at the same z-index and spans 0..56px from the bottom, so it wins\n hit-testing in the 48..56 overlap and the exclusive region for scrubbing is\n 12px. The wrapper grows UPWARD to 44px (48..92) because growing downward\n would be swallowed by the bar; the 3px bar itself stays exactly where it was\n (centred 8.5px above the wrapper's bottom edge, which is what\n align-items: center gave it inside 20px). The handle and tooltip\n enlargements are gated behind (hover: hover) and never match a finger, but\n .sp-progress--dragging is not, so the handle still appears mid-drag.\n\n any-pointer, not pointer: (pointer: coarse) describes the PRIMARY pointer\n only, so a hybrid laptop with a mouse and a touchscreen reports fine and kept\n the 12px exclusive region under a finger. (any-pointer: coarse) is true\n whenever a coarse pointer is available at all, which is the population that\n needs the target. The cost on such a machine is 24px of extra hit area for\n the mouse, over the player's own bottom edge. */\n@media (any-pointer: coarse) {\n .sp-progress-wrapper {\n height: 44px;\n align-items: flex-end;\n padding-bottom: 8.5px;\n box-sizing: border-box;\n }\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n/* Chapter markers */\n.sp-progress__markers {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n\n.sp-progress__marker {\n position: absolute;\n top: 0;\n width: 2px;\n height: 100%;\n margin-left: -1px;\n background: rgba(0, 0, 0, 0.65);\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n.sp-progress__tooltip-chapter {\n display: block;\n max-width: 220px;\n overflow: hidden;\n color: rgba(255, 255, 255, 0.75);\n font-weight: 400;\n font-variant-numeric: normal;\n text-overflow: ellipsis;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Overflow Tray\n\n The wrapper is deliberately unpositioned: the strip is absolutely\n positioned against .sp-controls (the nearest positioned ancestor), so it\n spans the bar's width and sits directly above it instead of hanging off a\n 44px button.\n\n The strip wraps horizontally and keeps overflow visible. A vertical menu of\n 44px rows would be taller than a portrait phone player (211px at 375px wide,\n measured 2026-09-05) and a scrolling one would clip the popovers registered\n controls own.\n ============================================ */\n.sp-overflow {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n.sp-overflow-tray {\n position: absolute;\n bottom: 100%;\n left: 0;\n right: 0;\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 4px;\n padding: 8px 12px;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n overflow: visible;\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-overflow-tray--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Beats a control's own inline style.display = '' on its next update(), so a\n control the fit took off screen stays off screen until the fit says\n otherwise. */\n.sp-control--collapsed {\n display: none !important;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n/* Both widths come from --sp-volume-slider-width on .sp-controls, which is also\n what the fit reserves for this control. focus-within is deliberately not\n gated on hover: a tap on the mute button focuses it, which is how the slider\n opens on a phone. */\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: var(--sp-volume-slider-width);\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: var(--sp-volume-slider-width);\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n /* Bounded to the player, see .sp-settings-panel. border-box because the\n bound is a content-box height by default and this menu adds 8px of padding\n top and bottom: at the 139px bound a 211px player gives, it rendered 155px\n and the host clipped the last 16px of it. */\n box-sizing: border-box;\n max-height: var(--sp-menu-max-height, none);\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n /* Bounded to the room above the control bar, written by the UI plugin's\n ResizeObserver as max(120px, container height - the bar's measured height\n - 16px). The bar is measured rather than assumed because its\n padding-bottom carries the safe-area inset in fullscreen, which moves the\n anchor these menus hang from. The Speed sub-panel is 253px (a\n 37px header plus six 36px rows) against a 211px portrait phone player, so\n without this the host's overflow: hidden cuts off the Back header and the\n first three speeds and playback speed is unreachable (measured at 375x211\n on 2026-09-05). With the variable unset the panel behaves exactly as it\n did before.\n\n Not applied to .sp-overflow-tray: that one has to keep overflow visible so\n the popovers its adopted controls own are not clipped.\n\n border-box because max-height bounds the content box: .sp-settings-panel--main\n is this same element with 4px of padding top and bottom, so wherever the\n bound binds the main menu, it rendered 8px past it and the host clipped the\n difference. The --sub views set padding: 0 and were already exact, which is\n why the browser harness's speed-panel check could not see this. */\n box-sizing: border-box;\n max-height: var(--sp-menu-max-height, none);\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Big Play Button\n\n z-index 12 puts it above the gradient (5) and above the gestures plugin's\n tap surface (6), so a tap lands on the button and starts playback instead\n of being read as a tap-to-toggle-controls gesture - exactly how the control\n bar's play button (10) already behaves. It stays below the spinner (15) and\n the error overlay (25), both of which own the middle of the picture when\n they are up.\n\n Hidden with visibility, not opacity alone, so it takes no pointer events\n while it is away.\n ============================================ */\n.sp-big-play {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 12;\n display: flex;\n align-items: center;\n justify-content: center;\n /* Comfortably past the 44px minimum touch target the control bar uses. */\n width: 72px;\n height: 72px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--sp-accent, #e50914);\n color: #fff;\n cursor: pointer;\n opacity: 0;\n visibility: hidden;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);\n transition: opacity 0.2s ease, visibility 0.2s, transform 0.15s ease,\n background 0.15s ease;\n}\n\n.sp-big-play--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-big-play svg {\n width: 36px;\n height: 36px;\n fill: currentColor;\n /* Optical centring: the play triangle's mass sits left of the glyph box. */\n margin-left: 3px;\n}\n\n.sp-big-play:hover {\n transform: translate(-50%, -50%) scale(1.06);\n}\n\n.sp-big-play:active {\n transform: translate(-50%, -50%) scale(0.96);\n}\n\n.sp-big-play:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 3px;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n/* Reconnecting: pulse the icon so the overlay reads as active work,\n not a dead-end error */\n.sp-error-overlay--reconnecting .sp-error-overlay__icon {\n animation: sp-reconnect-pulse 1.2s ease-in-out infinite;\n}\n\n@keyframes sp-reconnect-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-overflow-tray,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-big-play,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-big-play:hover,\n .sp-big-play:active {\n transform: translate(-50%, -50%);\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================ */\n:root {\n --sp-accent: #e50914;\n --sp-color: #fff;\n --sp-bg: rgba(0, 0, 0, 0.8);\n --sp-control-height: 48px;\n --sp-icon-size: 24px;\n}\n";
435
-
436
- /**
437
- * Formatting utility functions
438
- */
439
- /**
440
- * Format seconds as time string (mm:ss or h:mm:ss).
441
- */
442
- declare function formatTime(seconds: number): string;
443
- /**
444
- * Format time for live streams (relative to live edge).
445
- */
446
- declare function formatLiveTime(behindLive: number): string;
439
+ declare const styles = "\n/* ============================================\n Container & Base\n ============================================ */\n.sp-container {\n position: relative;\n /* Own stacking context: the control bar, menus and overlays all carry\n z-index, and without this they compete with the host page's own layers\n instead of staying inside the player. */\n isolation: isolate;\n width: 100%;\n height: 100%;\n background: #000;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-container video {\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n}\n\n.sp-container:focus {\n outline: none;\n}\n\n/* ============================================\n Gradient Overlay\n ============================================ */\n.sp-gradient {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 160px;\n background: linear-gradient(\n to top,\n rgba(0, 0, 0, 0.8) 0%,\n rgba(0, 0, 0, 0.4) 50%,\n transparent 100%\n );\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.25s ease;\n z-index: 5;\n}\n\n.sp-gradient--visible {\n opacity: 1;\n}\n\n/* ============================================\n Controls Container\n ============================================ */\n.sp-controls {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n align-items: center;\n padding: 0 12px 12px;\n /* Composed through a variable so the fullscreen rule below can add the\n device's own inset without restating the 12px. */\n padding-bottom: calc(12px + var(--sp-inset-bottom, 0px));\n gap: 4px;\n /* Declared on the bar because the fit needs the same number the volume rules\n below use: the slider expands mid-interaction and the plugin reserves the\n room in advance (see interactionReserve() in index.ts, which reads this\n property off this element at init). One declaration, so the stylesheet and\n the arithmetic cannot drift. Both the bar and the overflow tray are inside\n .sp-controls, so a volume control inherits it wherever the fit put it. */\n --sp-volume-slider-width: 64px;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 0.25s ease, transform 0.25s ease;\n z-index: 10;\n}\n\n.sp-controls--visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n.sp-controls--hidden {\n opacity: 0;\n transform: translateY(4px);\n pointer-events: none;\n}\n\n/* ============================================\n Safe Area (fullscreen only)\n\n Scoped to :fullscreen on purpose. Applied unconditionally, the inset would\n push an inline player's controls up on any page whose viewport meta says\n viewport-fit=cover, where there is no notch or home indicator over the\n player at all. Both the bar and the progress wrapper are direct children of\n the container, which is the element that goes fullscreen.\n\n The :-webkit-full-screen twin is a separate rule because an unknown\n pseudo-class anywhere in a selector list invalidates the whole rule.\n\n Nothing is needed in packages/embed/iframe.html: viewport-fit has no effect\n inside an iframe.\n ============================================ */\n:fullscreen > .sp-controls,\n:fullscreen > .sp-progress-wrapper {\n --sp-inset-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n:-webkit-full-screen > .sp-controls,\n:-webkit-full-screen > .sp-progress-wrapper {\n --sp-inset-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n/* ============================================\n Progress Bar (Above Controls)\n ============================================ */\n.sp-progress-wrapper {\n position: absolute;\n bottom: calc(48px + var(--sp-inset-bottom, 0px));\n left: 12px;\n right: 12px;\n height: 20px;\n display: flex;\n align-items: center;\n cursor: pointer;\n z-index: 10;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.25s ease;\n}\n\n.sp-progress-wrapper--visible {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* Touch: a 20px wrapper is not a 20px target. The control bar is a later\n sibling at the same z-index and spans 0..56px from the bottom, so it wins\n hit-testing in the 48..56 overlap and the exclusive region for scrubbing is\n 12px. The wrapper grows UPWARD to 44px (48..92) because growing downward\n would be swallowed by the bar; the 3px bar itself stays exactly where it was\n (centred 8.5px above the wrapper's bottom edge, which is what\n align-items: center gave it inside 20px). The handle and tooltip\n enlargements are gated behind (hover: hover) and never match a finger, but\n .sp-progress--dragging is not, so the handle still appears mid-drag.\n\n any-pointer, not pointer: (pointer: coarse) describes the PRIMARY pointer\n only, so a hybrid laptop with a mouse and a touchscreen reports fine and kept\n the 12px exclusive region under a finger. (any-pointer: coarse) is true\n whenever a coarse pointer is available at all, which is the population that\n needs the target. The cost on such a machine is 24px of extra hit area for\n the mouse, over the player's own bottom edge. */\n@media (any-pointer: coarse) {\n .sp-progress-wrapper {\n height: 44px;\n align-items: flex-end;\n padding-bottom: 8.5px;\n box-sizing: border-box;\n }\n}\n\n.sp-progress {\n position: relative;\n width: 100%;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n transition: height 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress {\n height: 5px;\n }\n}\n\n.sp-progress--dragging {\n height: 5px;\n}\n\n.sp-progress__track {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n border-radius: inherit;\n overflow: hidden;\n}\n\n.sp-progress__buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n\n.sp-progress__filled {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--sp-accent, #e50914);\n border-radius: inherit;\n}\n\n/* Chapter markers */\n.sp-progress__markers {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n\n.sp-progress__marker {\n position: absolute;\n top: 0;\n width: 2px;\n height: 100%;\n margin-left: -1px;\n background: rgba(0, 0, 0, 0.65);\n}\n\n.sp-progress__handle {\n position: absolute;\n top: 50%;\n width: 14px;\n height: 14px;\n background: var(--sp-accent, #e50914);\n border-radius: 50%;\n transform: translate(-50%, -50%) scale(0);\n transition: transform 0.15s ease;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n }\n}\n\n.sp-progress--dragging .sp-progress__handle {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* Thumbnail Preview */\n.sp-thumbnail-preview {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n pointer-events: none;\n display: none;\n z-index: 21;\n border-radius: 4px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n border: 2px solid rgba(255, 255, 255, 0.2);\n}\n\n.sp-thumbnail-preview__img {\n background-repeat: no-repeat;\n}\n\n/* Progress Tooltip */\n.sp-progress__tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n padding: 6px 10px;\n background: rgba(20, 20, 20, 0.95);\n color: #fff;\n font-size: 12px;\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n border-radius: 4px;\n white-space: nowrap;\n transform: translateX(-50%);\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.15s ease;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n.sp-progress__tooltip-chapter {\n display: block;\n max-width: 220px;\n overflow: hidden;\n color: rgba(255, 255, 255, 0.75);\n font-weight: 400;\n font-variant-numeric: normal;\n text-overflow: ellipsis;\n}\n\n@media (hover: hover) {\n .sp-progress-wrapper:hover .sp-progress__tooltip {\n opacity: 1;\n }\n}\n\n/* ============================================\n Control Buttons\n ============================================ */\n.sp-control {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n padding: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;\n flex-shrink: 0;\n min-width: 44px;\n min-height: 44px;\n}\n\n@media (hover: hover) {\n .sp-control:hover {\n color: #fff;\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-control:active {\n transform: scale(0.92);\n}\n\n.sp-control:focus-visible {\n outline: 2px solid var(--sp-accent, #e50914);\n outline-offset: 2px;\n}\n\n.sp-control:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n transform: none;\n}\n\n.sp-control:disabled:hover {\n background: none;\n}\n\n.sp-control svg {\n width: 24px;\n height: 24px;\n fill: currentColor;\n display: block;\n}\n\n.sp-control--small svg {\n width: 20px;\n height: 20px;\n}\n\n/* ============================================\n Spacer\n ============================================ */\n.sp-spacer {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================\n Overflow Tray\n\n The wrapper is deliberately unpositioned: the strip is absolutely\n positioned against .sp-controls (the nearest positioned ancestor), so it\n spans the bar's width and sits directly above it instead of hanging off a\n 44px button.\n\n The strip wraps horizontally and keeps overflow visible. A vertical menu of\n 44px rows would be taller than a portrait phone player (211px at 375px wide,\n measured 2026-09-05) and a scrolling one would clip the popovers registered\n controls own.\n ============================================ */\n.sp-overflow {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n.sp-overflow-tray {\n position: absolute;\n bottom: 100%;\n left: 0;\n right: 0;\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 4px;\n padding: 8px 12px;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n overflow: visible;\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-overflow-tray--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Beats a control's own inline style.display = '' on its next update(), so a\n control the fit took off screen stays off screen until the fit says\n otherwise. */\n.sp-control--collapsed {\n display: none !important;\n}\n\n/* ============================================\n Time Display\n ============================================ */\n.sp-time {\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n color: rgba(255, 255, 255, 0.9);\n white-space: nowrap;\n padding: 0 4px;\n letter-spacing: 0.02em;\n}\n\n/* ============================================\n Volume Control\n ============================================ */\n.sp-volume {\n display: flex;\n align-items: center;\n position: relative;\n}\n\n.sp-volume__slider-wrap {\n width: 0;\n overflow: hidden;\n transition: width 0.2s ease;\n}\n\n/* Both widths come from --sp-volume-slider-width on .sp-controls, which is also\n what the fit reserves for this control. focus-within is deliberately not\n gated on hover: a tap on the mute button focuses it, which is how the slider\n opens on a phone. */\n@media (hover: hover) {\n .sp-volume:hover .sp-volume__slider-wrap {\n width: var(--sp-volume-slider-width);\n }\n}\n\n.sp-volume:focus-within .sp-volume__slider-wrap {\n width: var(--sp-volume-slider-width);\n}\n\n.sp-volume__slider {\n width: 64px;\n height: 3px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 1.5px;\n cursor: pointer;\n position: relative;\n margin: 0 8px 0 4px;\n}\n\n.sp-volume__level {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: #fff;\n border-radius: inherit;\n transition: width 0.1s ease;\n}\n\n/* ============================================\n Live Indicator\n ============================================ */\n.sp-live {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--sp-accent, #e50914);\n cursor: pointer;\n padding: 6px 10px;\n border-radius: 4px;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n@media (hover: hover) {\n .sp-live:hover {\n background: rgba(255, 255, 255, 0.1);\n }\n}\n\n.sp-live__dot {\n width: 8px;\n height: 8px;\n background: currentColor;\n border-radius: 50%;\n animation: sp-pulse 2s ease-in-out infinite;\n}\n\n.sp-live--behind {\n opacity: 0.6;\n}\n\n.sp-live--behind .sp-live__dot {\n animation: none;\n}\n\n.sp-live--behind span {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Progress bar live mode: accent color for filled bar */\n.sp-progress--live .sp-progress__filled {\n background: var(--sp-accent, #e50914);\n}\n\n@keyframes sp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n}\n\n/* ============================================\n Quality / Settings Menu\n ============================================ */\n.sp-quality {\n position: relative;\n}\n\n.sp-quality__btn {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.sp-quality__label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.9;\n}\n\n.sp-quality-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n /* Bounded to the player, see .sp-settings-panel. border-box because the\n bound is a content-box height by default and this menu adds 8px of padding\n top and bottom: at the 139px bound a 211px player gives, it rendered 155px\n and the host clipped the last 16px of it. */\n box-sizing: border-box;\n max-height: var(--sp-menu-max-height, none);\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n padding: 8px 0;\n min-width: 150px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-quality-menu--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n.sp-quality-menu__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-quality-menu__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-quality-menu__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-quality-menu__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-quality-menu__item--active .sp-quality-menu__check {\n opacity: 1;\n}\n\n/* ============================================\n Settings Menu (Gear Icon)\n ============================================ */\n.sp-settings {\n position: relative;\n}\n\n.sp-settings__btn {\n display: flex;\n align-items: center;\n}\n\n.sp-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n /* Bounded to the room above the control bar, written by the UI plugin's\n ResizeObserver as max(120px, container height - the bar's measured height\n - 16px). The bar is measured rather than assumed because its\n padding-bottom carries the safe-area inset in fullscreen, which moves the\n anchor these menus hang from. The Speed sub-panel is 253px (a\n 37px header plus six 36px rows) against a 211px portrait phone player, so\n without this the host's overflow: hidden cuts off the Back header and the\n first three speeds and playback speed is unreachable (measured at 375x211\n on 2026-09-05). With the variable unset the panel behaves exactly as it\n did before.\n\n Not applied to .sp-overflow-tray: that one has to keep overflow visible so\n the popovers its adopted controls own are not clipped.\n\n border-box because max-height bounds the content box: .sp-settings-panel--main\n is this same element with 4px of padding top and bottom, so wherever the\n bound binds the main menu, it rendered 8px past it and the host clipped the\n difference. The --sub views set padding: 0 and were already exact, which is\n why the browser harness's speed-panel check could not see this. */\n box-sizing: border-box;\n max-height: var(--sp-menu-max-height, none);\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n background: rgba(20, 20, 20, 0.95);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border-radius: 8px;\n min-width: 200px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);\n opacity: 0;\n visibility: hidden;\n transform: translateY(8px);\n transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;\n z-index: 20;\n}\n\n.sp-settings-panel--open {\n opacity: 1;\n visibility: visible;\n transform: translateY(0);\n}\n\n/* Main menu rows */\n.sp-settings-panel--main {\n padding: 4px 0;\n}\n\n.sp-settings-panel__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__row:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__label {\n font-weight: 500;\n}\n\n.sp-settings-panel__value {\n display: flex;\n align-items: center;\n gap: 4px;\n color: rgba(255, 255, 255, 0.6);\n font-size: 12px;\n}\n\n.sp-settings-panel__arrow {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__arrow svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n/* Sub-menu panels */\n.sp-settings-panel--sub {\n padding: 0;\n}\n\n.sp-settings-panel__header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 16px;\n font-size: 13px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n cursor: pointer;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n transition: background 0.1s ease;\n}\n\n.sp-settings-panel__header:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.sp-settings-panel__back {\n display: flex;\n align-items: center;\n transform: rotate(-90deg);\n}\n\n.sp-settings-panel__back svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__header-label {\n flex: 1;\n}\n\n.sp-settings-panel__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.8);\n cursor: pointer;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.sp-settings-panel__item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #fff;\n}\n\n.sp-settings-panel__item--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-settings-panel__check {\n width: 16px;\n height: 16px;\n fill: currentColor;\n margin-left: 8px;\n opacity: 0;\n}\n\n.sp-settings-panel__check svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n}\n\n.sp-settings-panel__item--active .sp-settings-panel__check {\n opacity: 1;\n}\n\n/* ============================================\n Captions Button\n ============================================ */\n.sp-captions--active {\n color: var(--sp-accent, #e50914);\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent, #e50914);\n}\n\n.sp-cast--unavailable {\n opacity: 0.4;\n}\n\n/* ============================================\n Big Play Button\n\n z-index 12 puts it above the gradient (5) and above the gestures plugin's\n tap surface (6), so a tap lands on the button and starts playback instead\n of being read as a tap-to-toggle-controls gesture - exactly how the control\n bar's play button (10) already behaves. It stays below the spinner (15) and\n the error overlay (25), both of which own the middle of the picture when\n they are up.\n\n Hidden with visibility, not opacity alone, so it takes no pointer events\n while it is away.\n ============================================ */\n.sp-big-play {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 12;\n display: flex;\n align-items: center;\n justify-content: center;\n /* Comfortably past the 44px minimum touch target the control bar uses. */\n width: 72px;\n height: 72px;\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--sp-accent, #e50914);\n color: #fff;\n cursor: pointer;\n opacity: 0;\n visibility: hidden;\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);\n transition: opacity 0.2s ease, visibility 0.2s, transform 0.15s ease,\n background 0.15s ease;\n}\n\n.sp-big-play--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-big-play svg {\n width: 36px;\n height: 36px;\n fill: currentColor;\n /* Optical centring: the play triangle's mass sits left of the glyph box. */\n margin-left: 3px;\n}\n\n.sp-big-play:hover {\n transform: translate(-50%, -50%) scale(1.06);\n}\n\n.sp-big-play:active {\n transform: translate(-50%, -50%) scale(0.96);\n}\n\n.sp-big-play:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 3px;\n}\n\n/* ============================================\n Error Overlay\n ============================================ */\n.sp-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 25;\n opacity: 0;\n visibility: hidden;\n transition: opacity 0.25s ease, visibility 0.25s;\n}\n\n.sp-error-overlay--visible {\n opacity: 1;\n visibility: visible;\n}\n\n.sp-error-overlay__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n padding: 24px;\n max-width: 360px;\n}\n\n.sp-error-overlay__icon {\n color: rgba(255, 255, 255, 0.7);\n margin-bottom: 16px;\n}\n\n.sp-error-overlay__icon svg {\n width: 48px;\n height: 48px;\n fill: currentColor;\n}\n\n/* Reconnecting: pulse the icon so the overlay reads as active work,\n not a dead-end error */\n.sp-error-overlay--reconnecting .sp-error-overlay__icon {\n animation: sp-reconnect-pulse 1.2s ease-in-out infinite;\n}\n\n@keyframes sp-reconnect-pulse {\n 0%, 100% { opacity: 0.4; }\n 50% { opacity: 1; }\n}\n\n.sp-error-overlay__message {\n color: rgba(255, 255, 255, 0.9);\n font-size: 15px;\n line-height: 1.5;\n margin: 0 0 24px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.sp-error-overlay__actions {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n justify-content: center;\n}\n\n.sp-error-overlay__retry {\n background: var(--sp-accent, #e50914);\n color: #fff;\n border: none;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n border-radius: 6px;\n cursor: pointer;\n min-width: 120px;\n min-height: 44px;\n transition: background 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__retry:hover {\n filter: brightness(1.1);\n}\n\n.sp-error-overlay__retry:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__retry:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n.sp-error-overlay__dismiss {\n background: none;\n color: rgba(255, 255, 255, 0.7);\n border: 1px solid rgba(255, 255, 255, 0.3);\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n min-width: 100px;\n min-height: 44px;\n transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;\n font-family: inherit;\n}\n\n.sp-error-overlay__dismiss:hover {\n color: #fff;\n border-color: rgba(255, 255, 255, 0.5);\n}\n\n.sp-error-overlay__dismiss:active {\n transform: scale(0.96);\n}\n\n.sp-error-overlay__dismiss:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n\n/* ============================================\n Buffering Indicator\n ============================================ */\n.sp-buffering {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 15;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n\n.sp-buffering--visible {\n opacity: 1;\n}\n\n.sp-buffering svg {\n width: 48px;\n height: 48px;\n fill: rgba(255, 255, 255, 0.9);\n filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n}\n\n@keyframes sp-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n.sp-spin {\n animation: sp-spin 0.8s linear infinite;\n}\n\n/* ============================================\n Reduced Motion\n ============================================ */\n@media (prefers-reduced-motion: reduce) {\n .sp-gradient,\n .sp-controls,\n .sp-progress-wrapper,\n .sp-progress,\n .sp-progress__handle,\n .sp-progress__tooltip,\n .sp-control,\n .sp-volume__slider-wrap,\n .sp-quality-menu,\n .sp-overflow-tray,\n .sp-settings-panel,\n .sp-settings-panel__row,\n .sp-settings-panel__item,\n .sp-settings-panel__header,\n .sp-buffering,\n .sp-big-play,\n .sp-error-overlay,\n .sp-error-overlay__retry,\n .sp-error-overlay__dismiss {\n transition: none;\n }\n\n .sp-big-play:hover,\n .sp-big-play:active {\n transform: translate(-50%, -50%);\n }\n\n .sp-live__dot,\n .sp-spin {\n animation: none;\n }\n}\n\n/* ============================================\n CSS Custom Properties (Theming)\n ============================================\n\n Token defaults live at the use sites as var() fallbacks, not in a :root\n block. A :root declaration wrote --sp-accent, --sp-color, --sp-bg,\n --sp-control-height and --sp-icon-size into the HOST document, so mounting a\n player changed variables the page may own. A .sp-container declaration would\n be no better: the container rule wins for every descendant, so it would\n override a host that themes the player from its own :root. A var() fallback\n does neither - it applies only where nothing else defines the token.\n\n setTheme() writes the same names onto the player's container, which still\n wins over the fallbacks.\n ============================================ */\n";
447
440
 
448
441
  /**
449
442
  * UI Controls Plugin for Scarlett Player
@@ -480,4 +473,4 @@ declare function formatLiveTime(behindLive: number): string;
480
473
  */
481
474
  declare function uiPlugin(config?: UIPluginConfig): IUIPlugin;
482
475
 
483
- export { type BuiltinControlSlot, type Control, type ControlFactory, type ControlSlot, DEFAULT_PRIORITY, type FitExit, type FitItem, type FitPlan, type FitRank, type FitRule, type FitTemplate, type IUIPlugin, type LayoutConfig, type RegisterControlOptions, type ThemeConfig, type UIPluginConfig, assertFitLayout, uiPlugin as default, formatLiveTime, formatTime, getControlFactory, icons, planFit, registerControl, resetControlRegistry, resolveFitItems, styles, uiPlugin, unregisterControl, unregisterControlsFor };
476
+ export { type BuiltinControlSlot, type Control, type ControlFactory, type ControlSlot, DEFAULT_PRIORITY, type FitExit, type FitItem, type FitPlan, type FitRank, type FitRule, type FitTemplate, type IUIPlugin, type LayoutConfig, type RegisterControlOptions, type ThemeConfig, type UIPluginConfig, assertFitLayout, uiPlugin as default, getControlFactory, icons, planFit, registerControl, resetControlRegistry, resolveFitItems, styles, uiPlugin, unregisterControl, unregisterControlsFor };
package/dist/index.js CHANGED
@@ -1171,24 +1171,30 @@ var styles = `
1171
1171
 
1172
1172
  /* ============================================
1173
1173
  CSS Custom Properties (Theming)
1174
+ ============================================
1175
+
1176
+ Token defaults live at the use sites as var() fallbacks, not in a :root
1177
+ block. A :root declaration wrote --sp-accent, --sp-color, --sp-bg,
1178
+ --sp-control-height and --sp-icon-size into the HOST document, so mounting a
1179
+ player changed variables the page may own. A .sp-container declaration would
1180
+ be no better: the container rule wins for every descendant, so it would
1181
+ override a host that themes the player from its own :root. A var() fallback
1182
+ does neither - it applies only where nothing else defines the token.
1183
+
1184
+ setTheme() writes the same names onto the player's container, which still
1185
+ wins over the fallbacks.
1174
1186
  ============================================ */
1175
- :root {
1176
- --sp-accent: #e50914;
1177
- --sp-color: #fff;
1178
- --sp-bg: rgba(0, 0, 0, 0.8);
1179
- --sp-control-height: 48px;
1180
- --sp-icon-size: 24px;
1181
- }
1182
1187
  `;
1183
1188
 
1184
1189
  // src/icons.ts
1190
+ import { SHARED_ICON_PATHS } from "@scarlett-player/core";
1185
1191
  var icons = {
1186
- play: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`,
1192
+ play: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${SHARED_ICON_PATHS.play}"/></svg>`,
1187
1193
  pause: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>`,
1188
1194
  replay: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>`,
1189
- volumeHigh: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>`,
1195
+ volumeHigh: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${SHARED_ICON_PATHS.volumeHigh}"/></svg>`,
1190
1196
  volumeLow: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>`,
1191
- volumeMute: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>`,
1197
+ volumeMute: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${SHARED_ICON_PATHS.volumeMuted}"/></svg>`,
1192
1198
  fullscreen: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>`,
1193
1199
  exitFullscreen: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>`,
1194
1200
  pip: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>`,
@@ -1261,29 +1267,7 @@ function setAttr(el, name, value) {
1261
1267
  }
1262
1268
 
1263
1269
  // src/utils/format.ts
1264
- function formatTime(seconds) {
1265
- if (!isFinite(seconds) || isNaN(seconds)) {
1266
- return "0:00";
1267
- }
1268
- const absSeconds = Math.abs(seconds);
1269
- const h = Math.floor(absSeconds / 3600);
1270
- const m = Math.floor(absSeconds % 3600 / 60);
1271
- const s = Math.floor(absSeconds % 60);
1272
- const sign = seconds < 0 ? "-" : "";
1273
- if (h > 0) {
1274
- return `${sign}${h}:${pad(m)}:${pad(s)}`;
1275
- }
1276
- return `${sign}${m}:${pad(s)}`;
1277
- }
1278
- function pad(n) {
1279
- return n < 10 ? `0${n}` : `${n}`;
1280
- }
1281
- function formatLiveTime(behindLive) {
1282
- if (behindLive <= 0) {
1283
- return "LIVE";
1284
- }
1285
- return `-${formatTime(behindLive)}`;
1286
- }
1270
+ import { formatTime, formatLiveTime } from "@scarlett-player/core";
1287
1271
 
1288
1272
  // src/controls/PlayButton.ts
1289
1273
  var PlayButton = class {
@@ -1588,7 +1572,7 @@ var ProgressBar = class {
1588
1572
  video.addEventListener("seeked", resumePlayback);
1589
1573
  }
1590
1574
  }
1591
- this.tooltip.style.opacity = "0";
1575
+ this.tooltip.style.opacity = "";
1592
1576
  this.thumbnailPreview.hide();
1593
1577
  }
1594
1578
  };
@@ -1597,7 +1581,7 @@ var ProgressBar = class {
1597
1581
  };
1598
1582
  this.onMouseLeave = () => {
1599
1583
  if (!this.isDragging) {
1600
- this.tooltip.style.opacity = "0";
1584
+ this.tooltip.style.opacity = "";
1601
1585
  this.thumbnailPreview.hide();
1602
1586
  }
1603
1587
  };
@@ -1869,6 +1853,7 @@ var ProgressBar = class {
1869
1853
  this.wrapper.removeEventListener("mousemove", this.onMouseMove);
1870
1854
  this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
1871
1855
  this.wrapper.removeEventListener("touchstart", this.onTouchStart);
1856
+ this.el.removeEventListener("keydown", this.onKeyDown);
1872
1857
  document.removeEventListener("mousemove", this.onDocMouseMove);
1873
1858
  document.removeEventListener("mouseup", this.onMouseUp);
1874
1859
  document.removeEventListener("touchmove", this.onDocTouchMove);
@@ -3413,7 +3398,7 @@ function resetControlRegistry() {
3413
3398
  }
3414
3399
 
3415
3400
  // src/version.ts
3416
- var PKG_VERSION = true ? "1.9.0" : "0.0.0-dev";
3401
+ var PKG_VERSION = true ? "1.11.0" : "0.0.0-dev";
3417
3402
 
3418
3403
  // src/index.ts
3419
3404
  var DEFAULT_LAYOUT = [
@@ -3450,6 +3435,7 @@ var FALLBACK_VOLUME_SLIDER_WIDTH = 64;
3450
3435
  var OVERFLOW_BUTTON_WIDTH = 44;
3451
3436
  var FALLBACK_BAR_PADDING_X = 24;
3452
3437
  var FALLBACK_BAR_GAP = 4;
3438
+ var RESIZE_FALLBACK_DEBOUNCE_MS = 100;
3453
3439
  var MENU_HEIGHT_RESERVE = 16;
3454
3440
  var FALLBACK_BAR_HEIGHT = 56;
3455
3441
  var MIN_MENU_HEIGHT = 120;
@@ -3476,6 +3462,8 @@ function uiPlugin(config = {}) {
3476
3462
  let entries = [];
3477
3463
  let timeEntry = null;
3478
3464
  let resizeObserver = null;
3465
+ let windowResizeTimer = null;
3466
+ let onWindowResize = null;
3479
3467
  let barPaddingX = FALLBACK_BAR_PADDING_X;
3480
3468
  let barGap = FALLBACK_BAR_GAP;
3481
3469
  let volumeSliderWidth = FALLBACK_VOLUME_SLIDER_WIDTH;
@@ -3954,6 +3942,17 @@ function uiPlugin(config = {}) {
3954
3942
  scheduleUpdate();
3955
3943
  });
3956
3944
  resizeObserver.observe(container);
3945
+ } else if (responsive && typeof window !== "undefined") {
3946
+ onWindowResize = () => {
3947
+ if (windowResizeTimer) clearTimeout(windowResizeTimer);
3948
+ windowResizeTimer = setTimeout(() => {
3949
+ windowResizeTimer = null;
3950
+ applyMenuBounds(container.clientHeight);
3951
+ fitPending = true;
3952
+ scheduleUpdate();
3953
+ }, RESIZE_FALLBACK_DEBOUNCE_MS);
3954
+ };
3955
+ window.addEventListener("resize", onWindowResize);
3957
3956
  }
3958
3957
  controlRegistryUnsubscribe = onControlRegistered((id, owner) => {
3959
3958
  if (!layout.includes(id)) {
@@ -3996,6 +3995,14 @@ function uiPlugin(config = {}) {
3996
3995
  }
3997
3996
  resizeObserver?.disconnect();
3998
3997
  resizeObserver = null;
3998
+ if (onWindowResize) {
3999
+ window.removeEventListener("resize", onWindowResize);
4000
+ onWindowResize = null;
4001
+ }
4002
+ if (windowResizeTimer) {
4003
+ clearTimeout(windowResizeTimer);
4004
+ windowResizeTimer = null;
4005
+ }
3999
4006
  api?.container?.style.removeProperty("--sp-menu-max-height");
4000
4007
  stateUnsubscribe?.();
4001
4008
  stateUnsubscribe = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/ui",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "UI Controls Plugin for Scarlett Player",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -22,14 +22,14 @@
22
22
  "dist"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@scarlett-player/core": "^1.8.0"
25
+ "@scarlett-player/core": "^1.10.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "tsup": "^8.0.0",
29
29
  "typescript": "^5.3.0",
30
30
  "vitest": "^1.6.0",
31
31
  "jsdom": "^24.0.0",
32
- "@scarlett-player/core": "1.9.0"
32
+ "@scarlett-player/core": "1.11.0"
33
33
  },
34
34
  "keywords": [
35
35
  "video",