@scarlett-player/ui 1.15.3 → 1.15.4

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/README.md CHANGED
@@ -187,6 +187,25 @@ uiPlugin({
187
187
  });
188
188
  ```
189
189
 
190
+ `accentColor` writes `--sp-accent`, which colours the progress fill, the big
191
+ play button and focus rings - all non-text, where WCAG asks for 3:1.
192
+
193
+ Accent **text** (the LIVE label, the active row in the settings and quality
194
+ menus, the active captions and cast glyphs) reads a second token first:
195
+
196
+ ```css
197
+ .my-player {
198
+ --sp-accent: #e50914; /* fills, rings */
199
+ --sp-accent-text: #ff4d57; /* labels and active-state glyphs */
200
+ }
201
+ ```
202
+
203
+ `--sp-accent-text` falls back to `--sp-accent`, so theming through
204
+ `accentColor` alone still colours that text. Set it when your accent is dark
205
+ enough that 11-13px labels miss the 4.5:1 AA threshold on the controls: the
206
+ brand red is 4.38:1 on black and 3.84:1 on the menus' own background, which is
207
+ why the default here is the lighter tone rather than `--sp-accent`.
208
+
190
209
  ## Big Play Button
191
210
 
192
211
  ```typescript
package/dist/index.cjs CHANGED
@@ -146,10 +146,22 @@ var styles = `
146
146
  object-fit: contain;
147
147
  }
148
148
 
149
+ /* The container is a tab stop - the plugin sets tabindex="0" on it so the
150
+ keyboard shortcuts have somewhere to land - so it must show where focus is.
151
+ A pointer press focuses it too, which is why the ring is on :focus-visible
152
+ and the plain :focus keeps outline:none: clicking the video should not draw
153
+ a box around the player. The offset is negative so the ring is painted
154
+ inside the player box; drawn outside it, a host page's own overflow or a
155
+ flush-fitting wrapper can clip it away. */
149
156
  .sp-container:focus {
150
157
  outline: none;
151
158
  }
152
159
 
160
+ .sp-container:focus-visible {
161
+ outline: 3px solid var(--sp-accent, #e50914);
162
+ outline-offset: -3px;
163
+ }
164
+
153
165
  /* ============================================
154
166
  Gradient Overlay
155
167
  ============================================ */
@@ -657,6 +669,14 @@ var styles = `
657
669
  /* ============================================
658
670
  Live Indicator
659
671
  ============================================ */
672
+ /* Accent text uses --sp-accent-text, not --sp-accent: the brand red as TEXT
673
+ is 4.38:1 on black and 3.84:1 on the menus' own rgba(20,20,20,.95), under
674
+ the 4.5:1 WCAG AA wants for 11-13px labels. The lighter default clears it at
675
+ 6.4:1 and 5.7:1. --sp-accent keeps the brand red for progress fills, big
676
+ play and focus rings, which are non-text and need only 3:1. A host that
677
+ themes --sp-accent still colours this text - the chain reads its token
678
+ first - so setting --sp-accent-text is only needed when that colour is too
679
+ dark to read. */
660
680
  .sp-live {
661
681
  display: flex;
662
682
  align-items: center;
@@ -665,7 +685,7 @@ var styles = `
665
685
  font-weight: 600;
666
686
  text-transform: uppercase;
667
687
  letter-spacing: 0.05em;
668
- color: var(--sp-accent, #e50914);
688
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
669
689
  cursor: pointer;
670
690
  padding: 6px 10px;
671
691
  border-radius: 4px;
@@ -777,7 +797,7 @@ var styles = `
777
797
  }
778
798
 
779
799
  .sp-quality-menu__item--active {
780
- color: var(--sp-accent, #e50914);
800
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
781
801
  }
782
802
 
783
803
  .sp-quality-menu__check {
@@ -949,7 +969,7 @@ var styles = `
949
969
  }
950
970
 
951
971
  .sp-settings-panel__item--active {
952
- color: var(--sp-accent, #e50914);
972
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
953
973
  }
954
974
 
955
975
  .sp-settings-panel__check {
@@ -974,14 +994,14 @@ var styles = `
974
994
  Captions Button
975
995
  ============================================ */
976
996
  .sp-captions--active {
977
- color: var(--sp-accent, #e50914);
997
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
978
998
  }
979
999
 
980
1000
  /* ============================================
981
1001
  Cast Button States
982
1002
  ============================================ */
983
1003
  .sp-cast--active {
984
- color: var(--sp-accent, #e50914);
1004
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
985
1005
  }
986
1006
 
987
1007
  .sp-cast--unavailable {
@@ -1264,6 +1284,12 @@ var styles = `
1264
1284
 
1265
1285
  setTheme() writes the same names onto the player's container, which still
1266
1286
  wins over the fallbacks.
1287
+
1288
+ --sp-accent-text is the one token with no setTheme() equivalent: it is the
1289
+ accent applied to TEXT and active-state glyphs, split from --sp-accent
1290
+ because the same colour has to clear 4.5:1 there and only 3:1 as a fill.
1291
+ It falls back to --sp-accent, so a themed player needs it only when the
1292
+ host's accent is too dark to read against the controls.
1267
1293
  ============================================ */
1268
1294
  `;
1269
1295
 
@@ -3643,7 +3669,7 @@ function resetControlRegistry() {
3643
3669
  }
3644
3670
 
3645
3671
  // src/version.ts
3646
- var PKG_VERSION = true ? "1.15.3" : "0.0.0-dev";
3672
+ var PKG_VERSION = true ? "1.15.4" : "0.0.0-dev";
3647
3673
 
3648
3674
  // src/index.ts
3649
3675
  var DEFAULT_LAYOUT = [
package/dist/index.d.cts CHANGED
@@ -594,7 +594,7 @@ declare const icons: {
594
594
  * Modern, minimal design inspired by Mux Player and Vidstack.
595
595
  * Uses CSS custom properties for theming.
596
596
  */
597
- 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/* ============================================\n Timeline extension layer (timeline-registry.ts)\n\n A zero-height line lying exactly on the rail's centre, spanning exactly the\n rail's width, so an extension can position by percentage and land on the\n same pixels the seek slider maps a press to. The 10px offset is the rail\n centre in BOTH wrapper modes: the fine-pointer wrapper is 20px tall with the\n 3px rail centred (8.5..11.5 from the bottom), and the coarse-pointer wrapper\n is 44px tall with 8.5px of bottom padding and align-items: flex-end, which\n puts the rail in the same place.\n\n Inert by default: only the explicit hit targets an extension puts inside it\n take pointer input, so an ordinary press on the rail still seeks.\n ============================================ */\n.sp-progress__extension {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 10px;\n height: 0;\n z-index: 3;\n pointer-events: none;\n}\n\n/* Editing reserves a lane below the rail for an extension's second handle, by\n lifting the whole wrapper clear of the control bar. The lane above needs no\n reservation - it is over the picture. */\n.sp-progress-wrapper--editing {\n bottom: calc(92px + var(--sp-inset-bottom, 0px));\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/* While an extension owns the pointer the bar must not also look like it is\n scrubbing: the tooltip is suppressed inline by the control, and the handle\n stops responding to hover growth.\n\n This has to come after the :hover and --dragging rules AND match their\n specificity, which is why the hover case is spelled out rather than left to\n the bare class: an extension drag is a pointer drag, so the pointer is over\n the wrapper the whole time and the hover rule is always the competing one. */\n.sp-progress-wrapper--ext-dragging .sp-progress__handle,\n.sp-progress-wrapper--ext-dragging:hover .sp-progress__handle {\n transform: translate(-50%, -50%);\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";
597
+ 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/* The container is a tab stop - the plugin sets tabindex=\"0\" on it so the\n keyboard shortcuts have somewhere to land - so it must show where focus is.\n A pointer press focuses it too, which is why the ring is on :focus-visible\n and the plain :focus keeps outline:none: clicking the video should not draw\n a box around the player. The offset is negative so the ring is painted\n inside the player box; drawn outside it, a host page's own overflow or a\n flush-fitting wrapper can clip it away. */\n.sp-container:focus {\n outline: none;\n}\n\n.sp-container:focus-visible {\n outline: 3px solid var(--sp-accent, #e50914);\n outline-offset: -3px;\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/* ============================================\n Timeline extension layer (timeline-registry.ts)\n\n A zero-height line lying exactly on the rail's centre, spanning exactly the\n rail's width, so an extension can position by percentage and land on the\n same pixels the seek slider maps a press to. The 10px offset is the rail\n centre in BOTH wrapper modes: the fine-pointer wrapper is 20px tall with the\n 3px rail centred (8.5..11.5 from the bottom), and the coarse-pointer wrapper\n is 44px tall with 8.5px of bottom padding and align-items: flex-end, which\n puts the rail in the same place.\n\n Inert by default: only the explicit hit targets an extension puts inside it\n take pointer input, so an ordinary press on the rail still seeks.\n ============================================ */\n.sp-progress__extension {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 10px;\n height: 0;\n z-index: 3;\n pointer-events: none;\n}\n\n/* Editing reserves a lane below the rail for an extension's second handle, by\n lifting the whole wrapper clear of the control bar. The lane above needs no\n reservation - it is over the picture. */\n.sp-progress-wrapper--editing {\n bottom: calc(92px + var(--sp-inset-bottom, 0px));\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/* While an extension owns the pointer the bar must not also look like it is\n scrubbing: the tooltip is suppressed inline by the control, and the handle\n stops responding to hover growth.\n\n This has to come after the :hover and --dragging rules AND match their\n specificity, which is why the hover case is spelled out rather than left to\n the bare class: an extension drag is a pointer drag, so the pointer is over\n the wrapper the whole time and the hover rule is always the competing one. */\n.sp-progress-wrapper--ext-dragging .sp-progress__handle,\n.sp-progress-wrapper--ext-dragging:hover .sp-progress__handle {\n transform: translate(-50%, -50%);\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/* Accent text uses --sp-accent-text, not --sp-accent: the brand red as TEXT\n is 4.38:1 on black and 3.84:1 on the menus' own rgba(20,20,20,.95), under\n the 4.5:1 WCAG AA wants for 11-13px labels. The lighter default clears it at\n 6.4:1 and 5.7:1. --sp-accent keeps the brand red for progress fills, big\n play and focus rings, which are non-text and need only 3:1. A host that\n themes --sp-accent still colours this text - the chain reads its token\n first - so setting --sp-accent-text is only needed when that colour is too\n dark to read. */\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-text, var(--sp-accent, #ff4d57));\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-text, var(--sp-accent, #ff4d57));\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-text, var(--sp-accent, #ff4d57));\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-text, var(--sp-accent, #ff4d57));\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent-text, var(--sp-accent, #ff4d57));\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 --sp-accent-text is the one token with no setTheme() equivalent: it is the\n accent applied to TEXT and active-state glyphs, split from --sp-accent\n because the same colour has to clear 4.5:1 there and only 3:1 as a fill.\n It falls back to --sp-accent, so a themed player needs it only when the\n host's accent is too dark to read against the controls.\n ============================================ */\n";
598
598
 
599
599
  /**
600
600
  * UI Controls Plugin for Scarlett Player
package/dist/index.d.ts CHANGED
@@ -594,7 +594,7 @@ declare const icons: {
594
594
  * Modern, minimal design inspired by Mux Player and Vidstack.
595
595
  * Uses CSS custom properties for theming.
596
596
  */
597
- 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/* ============================================\n Timeline extension layer (timeline-registry.ts)\n\n A zero-height line lying exactly on the rail's centre, spanning exactly the\n rail's width, so an extension can position by percentage and land on the\n same pixels the seek slider maps a press to. The 10px offset is the rail\n centre in BOTH wrapper modes: the fine-pointer wrapper is 20px tall with the\n 3px rail centred (8.5..11.5 from the bottom), and the coarse-pointer wrapper\n is 44px tall with 8.5px of bottom padding and align-items: flex-end, which\n puts the rail in the same place.\n\n Inert by default: only the explicit hit targets an extension puts inside it\n take pointer input, so an ordinary press on the rail still seeks.\n ============================================ */\n.sp-progress__extension {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 10px;\n height: 0;\n z-index: 3;\n pointer-events: none;\n}\n\n/* Editing reserves a lane below the rail for an extension's second handle, by\n lifting the whole wrapper clear of the control bar. The lane above needs no\n reservation - it is over the picture. */\n.sp-progress-wrapper--editing {\n bottom: calc(92px + var(--sp-inset-bottom, 0px));\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/* While an extension owns the pointer the bar must not also look like it is\n scrubbing: the tooltip is suppressed inline by the control, and the handle\n stops responding to hover growth.\n\n This has to come after the :hover and --dragging rules AND match their\n specificity, which is why the hover case is spelled out rather than left to\n the bare class: an extension drag is a pointer drag, so the pointer is over\n the wrapper the whole time and the hover rule is always the competing one. */\n.sp-progress-wrapper--ext-dragging .sp-progress__handle,\n.sp-progress-wrapper--ext-dragging:hover .sp-progress__handle {\n transform: translate(-50%, -50%);\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";
597
+ 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/* The container is a tab stop - the plugin sets tabindex=\"0\" on it so the\n keyboard shortcuts have somewhere to land - so it must show where focus is.\n A pointer press focuses it too, which is why the ring is on :focus-visible\n and the plain :focus keeps outline:none: clicking the video should not draw\n a box around the player. The offset is negative so the ring is painted\n inside the player box; drawn outside it, a host page's own overflow or a\n flush-fitting wrapper can clip it away. */\n.sp-container:focus {\n outline: none;\n}\n\n.sp-container:focus-visible {\n outline: 3px solid var(--sp-accent, #e50914);\n outline-offset: -3px;\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/* ============================================\n Timeline extension layer (timeline-registry.ts)\n\n A zero-height line lying exactly on the rail's centre, spanning exactly the\n rail's width, so an extension can position by percentage and land on the\n same pixels the seek slider maps a press to. The 10px offset is the rail\n centre in BOTH wrapper modes: the fine-pointer wrapper is 20px tall with the\n 3px rail centred (8.5..11.5 from the bottom), and the coarse-pointer wrapper\n is 44px tall with 8.5px of bottom padding and align-items: flex-end, which\n puts the rail in the same place.\n\n Inert by default: only the explicit hit targets an extension puts inside it\n take pointer input, so an ordinary press on the rail still seeks.\n ============================================ */\n.sp-progress__extension {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 10px;\n height: 0;\n z-index: 3;\n pointer-events: none;\n}\n\n/* Editing reserves a lane below the rail for an extension's second handle, by\n lifting the whole wrapper clear of the control bar. The lane above needs no\n reservation - it is over the picture. */\n.sp-progress-wrapper--editing {\n bottom: calc(92px + var(--sp-inset-bottom, 0px));\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/* While an extension owns the pointer the bar must not also look like it is\n scrubbing: the tooltip is suppressed inline by the control, and the handle\n stops responding to hover growth.\n\n This has to come after the :hover and --dragging rules AND match their\n specificity, which is why the hover case is spelled out rather than left to\n the bare class: an extension drag is a pointer drag, so the pointer is over\n the wrapper the whole time and the hover rule is always the competing one. */\n.sp-progress-wrapper--ext-dragging .sp-progress__handle,\n.sp-progress-wrapper--ext-dragging:hover .sp-progress__handle {\n transform: translate(-50%, -50%);\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/* Accent text uses --sp-accent-text, not --sp-accent: the brand red as TEXT\n is 4.38:1 on black and 3.84:1 on the menus' own rgba(20,20,20,.95), under\n the 4.5:1 WCAG AA wants for 11-13px labels. The lighter default clears it at\n 6.4:1 and 5.7:1. --sp-accent keeps the brand red for progress fills, big\n play and focus rings, which are non-text and need only 3:1. A host that\n themes --sp-accent still colours this text - the chain reads its token\n first - so setting --sp-accent-text is only needed when that colour is too\n dark to read. */\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-text, var(--sp-accent, #ff4d57));\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-text, var(--sp-accent, #ff4d57));\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-text, var(--sp-accent, #ff4d57));\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-text, var(--sp-accent, #ff4d57));\n}\n\n/* ============================================\n Cast Button States\n ============================================ */\n.sp-cast--active {\n color: var(--sp-accent-text, var(--sp-accent, #ff4d57));\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 --sp-accent-text is the one token with no setTheme() equivalent: it is the\n accent applied to TEXT and active-state glyphs, split from --sp-accent\n because the same colour has to clear 4.5:1 there and only 3:1 as a fill.\n It falls back to --sp-accent, so a themed player needs it only when the\n host's accent is too dark to read against the controls.\n ============================================ */\n";
598
598
 
599
599
  /**
600
600
  * UI Controls Plugin for Scarlett Player
package/dist/index.js CHANGED
@@ -109,10 +109,22 @@ var styles = `
109
109
  object-fit: contain;
110
110
  }
111
111
 
112
+ /* The container is a tab stop - the plugin sets tabindex="0" on it so the
113
+ keyboard shortcuts have somewhere to land - so it must show where focus is.
114
+ A pointer press focuses it too, which is why the ring is on :focus-visible
115
+ and the plain :focus keeps outline:none: clicking the video should not draw
116
+ a box around the player. The offset is negative so the ring is painted
117
+ inside the player box; drawn outside it, a host page's own overflow or a
118
+ flush-fitting wrapper can clip it away. */
112
119
  .sp-container:focus {
113
120
  outline: none;
114
121
  }
115
122
 
123
+ .sp-container:focus-visible {
124
+ outline: 3px solid var(--sp-accent, #e50914);
125
+ outline-offset: -3px;
126
+ }
127
+
116
128
  /* ============================================
117
129
  Gradient Overlay
118
130
  ============================================ */
@@ -620,6 +632,14 @@ var styles = `
620
632
  /* ============================================
621
633
  Live Indicator
622
634
  ============================================ */
635
+ /* Accent text uses --sp-accent-text, not --sp-accent: the brand red as TEXT
636
+ is 4.38:1 on black and 3.84:1 on the menus' own rgba(20,20,20,.95), under
637
+ the 4.5:1 WCAG AA wants for 11-13px labels. The lighter default clears it at
638
+ 6.4:1 and 5.7:1. --sp-accent keeps the brand red for progress fills, big
639
+ play and focus rings, which are non-text and need only 3:1. A host that
640
+ themes --sp-accent still colours this text - the chain reads its token
641
+ first - so setting --sp-accent-text is only needed when that colour is too
642
+ dark to read. */
623
643
  .sp-live {
624
644
  display: flex;
625
645
  align-items: center;
@@ -628,7 +648,7 @@ var styles = `
628
648
  font-weight: 600;
629
649
  text-transform: uppercase;
630
650
  letter-spacing: 0.05em;
631
- color: var(--sp-accent, #e50914);
651
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
632
652
  cursor: pointer;
633
653
  padding: 6px 10px;
634
654
  border-radius: 4px;
@@ -740,7 +760,7 @@ var styles = `
740
760
  }
741
761
 
742
762
  .sp-quality-menu__item--active {
743
- color: var(--sp-accent, #e50914);
763
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
744
764
  }
745
765
 
746
766
  .sp-quality-menu__check {
@@ -912,7 +932,7 @@ var styles = `
912
932
  }
913
933
 
914
934
  .sp-settings-panel__item--active {
915
- color: var(--sp-accent, #e50914);
935
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
916
936
  }
917
937
 
918
938
  .sp-settings-panel__check {
@@ -937,14 +957,14 @@ var styles = `
937
957
  Captions Button
938
958
  ============================================ */
939
959
  .sp-captions--active {
940
- color: var(--sp-accent, #e50914);
960
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
941
961
  }
942
962
 
943
963
  /* ============================================
944
964
  Cast Button States
945
965
  ============================================ */
946
966
  .sp-cast--active {
947
- color: var(--sp-accent, #e50914);
967
+ color: var(--sp-accent-text, var(--sp-accent, #ff4d57));
948
968
  }
949
969
 
950
970
  .sp-cast--unavailable {
@@ -1227,6 +1247,12 @@ var styles = `
1227
1247
 
1228
1248
  setTheme() writes the same names onto the player's container, which still
1229
1249
  wins over the fallbacks.
1250
+
1251
+ --sp-accent-text is the one token with no setTheme() equivalent: it is the
1252
+ accent applied to TEXT and active-state glyphs, split from --sp-accent
1253
+ because the same colour has to clear 4.5:1 there and only 3:1 as a fill.
1254
+ It falls back to --sp-accent, so a themed player needs it only when the
1255
+ host's accent is too dark to read against the controls.
1230
1256
  ============================================ */
1231
1257
  `;
1232
1258
 
@@ -3606,7 +3632,7 @@ function resetControlRegistry() {
3606
3632
  }
3607
3633
 
3608
3634
  // src/version.ts
3609
- var PKG_VERSION = true ? "1.15.3" : "0.0.0-dev";
3635
+ var PKG_VERSION = true ? "1.15.4" : "0.0.0-dev";
3610
3636
 
3611
3637
  // src/index.ts
3612
3638
  var DEFAULT_LAYOUT = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/ui",
3
- "version": "1.15.3",
3
+ "version": "1.15.4",
4
4
  "description": "UI Controls Plugin for Scarlett Player",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -29,7 +29,7 @@
29
29
  "typescript": "^5.3.0",
30
30
  "vitest": "^1.6.0",
31
31
  "jsdom": "^24.0.0",
32
- "@scarlett-player/core": "1.15.3"
32
+ "@scarlett-player/core": "1.15.4"
33
33
  },
34
34
  "keywords": [
35
35
  "video",