saltfish 0.2.77 → 0.2.79

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.
@@ -2565,7 +2565,7 @@ class PlaylistOrchestrator {
2565
2565
  * Start a playlist with given options
2566
2566
  */
2567
2567
  async startPlaylist(playlistId, options) {
2568
- var _a, _b, _c, _d;
2568
+ var _a, _b, _c, _d, _e;
2569
2569
  try {
2570
2570
  const needsManagerRecreation = this.isInitialized() && !this.managers.uiManager.getPlayerElement();
2571
2571
  if (!this.isInitialized() && this.playerInitializationService) {
@@ -2728,7 +2728,13 @@ class PlaylistOrchestrator {
2728
2728
  if (updatedStore.manifest.cursorColor) {
2729
2729
  this.managers.cursorManager.setColor(updatedStore.manifest.cursorColor);
2730
2730
  }
2731
- if (updatedStore.manifest.idleMode) {
2731
+ const isTriggeredAutomatically = finalOptions._triggeredByTriggerManager === true;
2732
+ const isFirstStep = updatedStore.currentStepId === ((_e = updatedStore.manifest.steps[0]) == null ? void 0 : _e.id);
2733
+ if (updatedStore.manifest.compactFirstStep && isFirstStep && isTriggeredAutomatically) {
2734
+ const playerElement = this.managers.uiManager.getPlayerElement();
2735
+ playerElement == null ? void 0 : playerElement.classList.add("sf-player--compact");
2736
+ }
2737
+ if (updatedStore.manifest.idleMode && isTriggeredAutomatically) {
2732
2738
  store.setIdleMode();
2733
2739
  } else {
2734
2740
  store.play();
@@ -2984,6 +2990,8 @@ class StateMachineActionHandler {
2984
2990
  }
2985
2991
  }
2986
2992
  this.managers.uiManager.updatePosition();
2993
+ const playerElement = this.managers.uiManager.getPlayerElement();
2994
+ playerElement == null ? void 0 : playerElement.classList.remove("sf-player--compact");
2987
2995
  this.managers.uiManager.showPlayer();
2988
2996
  const videoUrl = this.getVideoUrl(currentStep);
2989
2997
  const isAudioFallback = this.isUsingAudioFallback(currentStep);
@@ -3103,6 +3111,11 @@ class StateMachineActionHandler {
3103
3111
  this.managers.videoManager.loadVideo(videoUrl).then(() => {
3104
3112
  log("StateMachineActionHandler: Video loaded successfully, playing");
3105
3113
  loadTranscriptForStep();
3114
+ if (isAudioFallback) {
3115
+ this.managers.videoManager.startAudioVisualization();
3116
+ } else {
3117
+ this.managers.videoManager.initializeAudioForVideo();
3118
+ }
3106
3119
  this.managers.videoManager.play();
3107
3120
  const nextVideoUrl = this.findNextVideoUrl(currentStep);
3108
3121
  if (nextVideoUrl) {
@@ -3153,6 +3166,7 @@ class StateMachineActionHandler {
3153
3166
  const currentStep = context.currentStep;
3154
3167
  const videoUrl = this.getVideoUrl(currentStep);
3155
3168
  const isAudioFallback = this.isUsingAudioFallback(currentStep);
3169
+ this.managers.uiManager.showPlayer();
3156
3170
  if (isAudioFallback) {
3157
3171
  const store = useSaltfishStore.getState();
3158
3172
  const manifest = store.manifest;
@@ -3162,69 +3176,19 @@ class StateMachineActionHandler {
3162
3176
  } else {
3163
3177
  this.managers.videoManager.hideAudioFallbackOverlay();
3164
3178
  }
3165
- try {
3166
- this.managers.interactionManager.clearButtons();
3167
- this.managers.interactionManager.clearDOMInteractions();
3168
- if (currentStep.domInteractions) {
3169
- this.managers.interactionManager.setupDOMInteractions(currentStep.domInteractions);
3179
+ this.managers.videoManager.loadVideo(videoUrl).then(() => {
3180
+ if (isAudioFallback) {
3181
+ this.managers.videoManager.startAudioVisualization();
3170
3182
  }
3171
- if (currentStep.buttons) {
3172
- this.managers.interactionManager.createButtons(currentStep.buttons);
3173
- }
3174
- if (currentStep.cursorAnimations && currentStep.cursorAnimations.length > 0) {
3175
- log(`StateMachineActionHandler: Setting up cursor animations for idle mode step ${currentStep.id}`);
3176
- this.managers.cursorManager.setShouldShowCursor(true);
3177
- this.managers.cursorManager.animate(currentStep.cursorAnimations[0]);
3178
- } else {
3179
- this.managers.cursorManager.setShouldShowCursor(false);
3180
- }
3181
- const hasSpecialTransitions = currentStep.buttons && currentStep.buttons.length > 0 || currentStep.transitions.some(
3182
- (t) => t.type === "dom-click" || t.type === "url-path"
3183
- );
3184
- const completionPolicy = hasSpecialTransitions ? "manual" : "auto";
3185
- if (hasSpecialTransitions) {
3186
- log("StateMachineActionHandler: Setting up transitions for idle mode step with special transitions");
3187
- this.managers.transitionManager.setupTransitions(currentStep, false);
3183
+ const videoElement = this.managers.videoManager.getVideoElement();
3184
+ if (videoElement) {
3185
+ videoElement.muted = true;
3186
+ videoElement.loop = true;
3187
+ videoElement.play().catch(() => {
3188
+ });
3188
3189
  }
3189
- this.managers.videoManager.setCompletionPolicy(completionPolicy, () => {
3190
- if (!hasSpecialTransitions) {
3191
- this.managers.transitionManager.setupTransitions(currentStep, true);
3192
- }
3193
- });
3194
- log("StateMachineActionHandler: Starting async idle mode video load");
3195
- this.managers.videoManager.loadVideo(videoUrl).then(() => {
3196
- var _a, _b;
3197
- log("StateMachineActionHandler: Idle mode video loaded successfully, configuring for ambient playback");
3198
- const videoElement = this.managers.videoManager.getVideoElement();
3199
- if (videoElement) {
3200
- videoElement.muted = true;
3201
- videoElement.loop = true;
3202
- videoElement.play().catch(() => {
3203
- log("StateMachineActionHandler: Idle mode video play failed");
3204
- });
3205
- }
3206
- const store = useSaltfishStore.getState();
3207
- const captionsEnabled = ((_a = store.manifest) == null ? void 0 : _a.captions) ?? true;
3208
- const language = (_b = store.userData) == null ? void 0 : _b.language;
3209
- let transcript = currentStep.transcript;
3210
- if (language && currentStep.translations && currentStep.translations[language]) {
3211
- const translation = currentStep.translations[language];
3212
- if (translation.transcript) {
3213
- transcript = translation.transcript;
3214
- log(`StateMachineActionHandler: Using translated transcript for idle mode, language: ${language}`);
3215
- }
3216
- }
3217
- if (transcript) {
3218
- log(`StateMachineActionHandler: Loading transcript for idle mode step ${currentStep.id}, initially visible: ${captionsEnabled}`);
3219
- this.managers.videoManager.loadTranscript(transcript, captionsEnabled);
3220
- } else {
3221
- this.managers.videoManager.loadTranscript(null, true);
3222
- }
3223
- }).catch((error2) => {
3224
- log(`StateMachineActionHandler: Error loading idle mode video: ${error2}`);
3225
- });
3226
- } catch (error2) {
3227
- }
3190
+ }).catch((error2) => {
3191
+ });
3228
3192
  }
3229
3193
  handleTrackPlaylistComplete() {
3230
3194
  var _a, _b;
@@ -3813,9 +3777,9 @@ __publicField(_ManagerOrchestrator, "prevState", {
3813
3777
  });
3814
3778
  let ManagerOrchestrator = _ManagerOrchestrator;
3815
3779
  const baseResetCss = "/* \n * CSS Reset for the Saltfish playlist Player\n * Minimal reset for the Shadow DOM to ensure consistent rendering\n */\n\n:host {\n all: initial;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n box-sizing: border-box;\n}\n\n:host *,\n:host *::before,\n:host *::after {\n box-sizing: inherit;\n margin: 0;\n padding: 0;\n}\n\nbutton {\n background: none;\n border: none;\n cursor: pointer;\n font: inherit;\n outline: none;\n padding: 0;\n}\n\n/* Utility classes for CSP-compliant styling */\n.sf-hidden {\n display: none !important;\n} ";
3816
- const baseVariablesCss = "/* \n * Variables for the Saltfish playlist Player\n * Defines all design tokens used throughout the application\n */\n\n:host {\n /* Colors */\n --sf-primary-color: #4a9bff;\n --sf-secondary-color: #6ccfff;\n --sf-background-color: #1e1e1e;\n --sf-text-color: #ffffff;\n --sf-button-bg: rgba(0, 0, 0, 0.5);\n --sf-button-hover-bg: rgba(0, 0, 0, 0.7);\n --sf-overlay-gradient: linear-gradient(180deg, rgba(0, 0, 0, 0.7) 0%, transparent 30%, transparent 70%, rgba(0, 0, 0, 0.7) 100%);\n --sf-progress-gradient: linear-gradient(90deg, var(--sf-primary-color), var(--sf-secondary-color));\n --sf-error-color: #ff4d4d;\n --sf-error-bg: rgba(255, 77, 77, 0.1);\n \n /* Spacing */\n --sf-spacing-xs: 4px;\n --sf-spacing-sm: 8px;\n --sf-spacing-md: 12px;\n --sf-spacing-lg: 16px;\n --sf-spacing-xl: 24px;\n \n /* Sizes */\n --sf-player-width: 240px;\n --sf-player-height: 336px;\n --sf-player-min-width: 80px;\n --sf-player-min-height: 80px;\n --sf-control-button-size: 24px;\n --sf-play-button-size: 60px;\n --sf-minimize-button-size: 34px;\n --sf-mute-button-size: 32px;\n --sf-cc-button-size: 32px;\n --sf-cursor-size: 32px;\n \n /* Border radius */\n --sf-border-radius-sm: 4px;\n --sf-border-radius-md: 8px;\n --sf-border-radius-lg: 16px;\n --sf-border-radius-circle: 50%;\n \n /* Transitions */\n --sf-transition-fast: 0.1s ease;\n --sf-transition-normal: 0.2s ease;\n --sf-transition-slow: 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n \n /* Shadows */\n --sf-shadow-small: 0 2px 5px rgba(0, 0, 0, 0.2);\n --sf-shadow-medium: 0 4px 8px rgba(0, 0, 0, 0.15);\n --sf-shadow-large: 0 10px 25px rgba(0, 0, 0, 0.2);\n \n /* Z-index layering */\n --sf-z-index-base: 1;\n --sf-z-index-overlay: 2;\n --sf-z-index-controls: 10;\n --sf-z-index-cursor: 9999;\n --sf-z-index-player: 2147483648;\n \n /* Font sizes */\n --sf-font-size-sm: 14px;\n --sf-font-size-md: 16px;\n --sf-font-size-lg: 18px;\n --sf-font-size-xl: 24px;\n} \n\n/* Mobile device responsive adjustments - make player smaller for mobile screens */\n@media (max-width: 768px) {\n :host {\n /* Reduce player size on mobile for better space utilization */\n --sf-player-width: 180px; /* 25% smaller than desktop (240px -> 180px) */\n --sf-player-height: 252px; /* 25% smaller than desktop (336px -> 252px) */\n --sf-player-min-width: 60px; /* Smaller when minimized (80px -> 60px) */\n --sf-player-min-height: 60px; /* Smaller when minimized (80px -> 60px) */\n \n /* Keep controls touch-friendly despite smaller player size */\n --sf-play-button-size: 44px; /* Smaller but still touch-friendly (60px -> 44px) */\n --sf-control-button-size: 28px; /* Keep larger for touch targets (24px -> 28px) */\n --sf-mute-button-size: 26px; /* Smaller for mobile (32px -> 26px) */\n --sf-cc-button-size: 26px; /* Smaller for mobile (32px -> 26px) */\n --sf-minimize-button-size: 26px; /* Match other mobile button sizes */\n }\n}\n\n/* Touch device specific adjustments (tablets and larger touch devices, excluding mobile) */\n@media (pointer: coarse) and (min-width: 769px) {\n :host {\n /* Ensure touch-friendly sizes even on larger touch devices */\n --sf-control-button-size: 28px;\n --sf-mute-button-size: 38px;\n --sf-cc-button-size: 38px;\n --sf-minimize-button-size: 38px; /* Match other touch device button sizes */\n }\n} ";
3817
- const componentsPlayerCss = "/* \n * Player component styles for the Saltfish playlist Player\n * Following BEM naming convention\n */\n\n/* Main player container */\n.sf-player {\n border-radius: var(--sf-border-radius-lg);\n box-shadow: 0 25px 50px rgba(0, 0, 0, 0.45), 0 10px 20px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(255, 255, 255, 0.08);\n transition: all var(--sf-transition-slow);\n position: relative;\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n opacity: 0;\n transition: opacity 0.3s ease-in-out;\n}\n\n/* Player visible state - show with fade in */\n.sf-player--visible {\n opacity: 1;\n}\n\n/* Dark gradient overlay at bottom of player */\n.sf-player::before {\n content: '';\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 33.33%; /* One third of player height */\n background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);\n pointer-events: none;\n z-index: var(--sf-z-index-overlay);\n border-radius: 8px;\n}\n\n/* Hide gradient overlay when minimized */\n.sf-player--minimized::before {\n display: none;\n}\n\n/* Full-size player state */\n.sf-player:not(.sf-player--minimized) {\n width: var(--sf-player-width);\n height: var(--sf-player-height);\n}\n\n/* Autoplay fallback state - ensure play button is visible */\n.sf-player--waiting-for-user-interaction .sf-controls-container__play-button {\n display: flex !important;\n opacity: 1 !important;\n visibility: visible !important;\n}\n\n/* Also show the center play button in autoplay fallback state */\n.sf-player--waiting-for-user-interaction .sf-player__center-play-button {\n display: flex !important;\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 20) !important; /* Higher z-index to appear above overlay */\n}\n\n/* Make the autoplay fallback state more prominent to indicate need for interaction */\n.sf-player--waiting-for-user-interaction::after {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.3);\n pointer-events: none;\n z-index: var(--sf-z-index-overlay);\n}\n\n/* Player state: minimized */\n.sf-player--minimized {\n /* Equal width and height are essential for maintaining a perfect circle when using border-radius: 50% */\n width: var(--sf-player-min-width);\n height: var(--sf-player-min-height);\n border-radius: var(--sf-border-radius-circle);\n box-shadow: 0 15px 30px rgba(0, 0, 0, 0.35), 0 5px 15px rgba(0, 0, 0, 0.25), 0 0 0 2px rgba(255, 255, 255, 0.08);\n cursor: pointer;\n /* Force overriding any inline styles that might be applied */\n max-width: var(--sf-player-min-width) !important;\n max-height: var(--sf-player-min-height) !important;\n min-width: var(--sf-player-min-width) !important;\n min-height: var(--sf-player-min-height) !important;\n}\n\n/* Hide controls when minimized */\n.sf-player--minimized .sf-controls-container {\n display: none;\n}\n\n/* Only show the minimize button when hovering on minimized player */\n.sf-player--minimized .sf-player__minimize-button {\n opacity: 0;\n}\n\n.sf-player--minimized:hover .sf-player__minimize-button {\n opacity: 1;\n}\n\n/* Player root element */\n#sf-player-root {\n position: fixed;\n z-index: var(--sf-z-index-player);\n}\n\n/* Fixed positioning classes */\n.sf-player-root--bottom-left {\n bottom: 20px;\n left: 20px;\n}\n\n.sf-player-root--bottom-right {\n bottom: 20px;\n right: 20px;\n}\n\n/* Player error message */\n.sf-player__error {\n padding: var(--sf-spacing-md);\n color: var(--sf-error-color);\n background-color: var(--sf-error-bg);\n border-radius: var(--sf-border-radius-md);\n margin: var(--sf-spacing-sm);\n font-size: var(--sf-font-size-sm);\n border-left: 4px solid var(--sf-error-color);\n}\n\n/* Minimize button */\n.sf-player__minimize-button {\n position: absolute;\n top: 8px;\n right: 6px;\n width: var(--sf-minimize-button-size);\n height: var(--sf-minimize-button-size);\n background-color: transparent;\n border-radius: var(--sf-border-radius-circle);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: var(--sf-z-index-controls);\n color: white;\n border: none;\n font-size: calc(var(--sf-font-size-sm) + 4px);\n transition: all var(--sf-transition-normal);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n opacity: 0;\n}\n\n/* Minimize button hover state */\n.sf-player__minimize-button:hover {\n transform: scale(1.1);\n}\n\n/* Show minimize button on player hover */\n.sf-player:hover .sf-player__minimize-button {\n opacity: 1;\n}\n\n/* Mobile and touch device overrides for minimize button visibility */\n/* Ensure minimize button is always visible on touch devices, even when minimized */\n@media (pointer: coarse) {\n .sf-player__minimize-button {\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 50) !important; /* Much higher z-index for touch devices */\n }\n \n .sf-player--minimized .sf-player__minimize-button {\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 50) !important; /* Much higher z-index for touch devices */\n }\n}\n\n/* Ensure minimize button is always visible on mobile screens under 768px */\n@media (max-width: 768px) {\n .sf-player__minimize-button {\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 50) !important; /* Much higher z-index for mobile */\n /* Position closer to top right corner on mobile */\n top: var(--sf-spacing-xs) !important; /* 4px from top instead of 16px */\n right: var(--sf-spacing-xs) !important; /* 4px from right instead of 12px */\n }\n \n .sf-player--minimized .sf-player__minimize-button {\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 50) !important; /* Much higher z-index for mobile */\n /* Position closer to top right corner on mobile */\n top: var(--sf-spacing-xs) !important; /* 4px from top instead of 16px */\n right: var(--sf-spacing-xs) !important; /* 4px from right instead of 12px */\n }\n}\n\n/* Player title */\n.sf-player__title {\n position: absolute;\n top: var(--sf-spacing-md);\n left: var(--sf-spacing-md);\n color: var(--sf-text-color);\n font-size: var(--sf-font-size-md);\n font-weight: 600;\n z-index: var(--sf-z-index-controls);\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);\n max-width: 70%;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Centered play/pause button overlay */\n.sf-player__center-play-button {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: var(--sf-play-button-size);\n height: var(--sf-play-button-size);\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: var(--sf-border-radius-circle);\n display: none; /* Hidden by default */\n justify-content: center;\n align-items: center;\n z-index: calc(var(--sf-z-index-controls) + 10); /* Ensure higher z-index than other elements */\n color: white;\n border: none;\n font-size: var(--sf-control-button-size);\n cursor: pointer;\n transition: transform var(--sf-transition-normal), background-color var(--sf-transition-normal);\n backdrop-filter: blur(3px);\n -webkit-backdrop-filter: blur(3px);\n box-shadow: 0 15px 30px rgba(0, 0, 0, 0.5), 0 5px 15px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(255, 255, 255, 0.08);\n pointer-events: auto; /* Enable pointer events to capture clicks */\n}\n\n/* Center play button visible state */\n.sf-player__center-play-button--visible {\n display: flex !important;\n}\n\n/* Center play button prominent state (for autoplay blocked, idle mode) */\n.sf-player__center-play-button--prominent {\n opacity: 1 !important;\n pointer-events: auto !important;\n}\n\n/* Center play button hover state */\n.sf-player__center-play-button:hover {\n transform: translate(-50%, -50%) scale(1.1);\n background-color: rgba(0, 0, 0, 0.7);\n}\n\n/* Hide center play button in minimized state */\n.sf-player--minimized .sf-player__center-play-button {\n display: none !important;\n}\n\n/* Center play button with adjustment for 3+ buttons */\n.sf-player__center-play-button--with-many-buttons {\n transform: translate(-50%, -85%) !important; /* Move up by adjusting Y offset */\n}\n\n/* Exit button for minimized mode */\n.sf-player__exit-button {\n position: absolute;\n top: -22px; /* Position it above the player */\n right: 0;\n width: 20px;\n height: 20px;\n background-color: var(--sf-button-bg);\n border-radius: var(--sf-border-radius-circle);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: var(--sf-z-index-controls);\n color: white;\n border: none;\n font-size: var(--sf-font-size-md);\n transition: all var(--sf-transition-normal);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.35);\n}\n\n/* Exit button hover state */\n.sf-player__exit-button:hover {\n transform: scale(1.1);\n background-color: var(--sf-button-hover-bg);\n}\n\n/* Show exit button on minimized player hover */\n.sf-player--minimized:hover .sf-player__exit-button {\n opacity: 1;\n}\n\n/* Saltfish logo */\n.sf-player__logo {\n position: absolute;\n bottom: var(--sf-spacing-xs);\n left: 0;\n right: 0;\n height: 18px;\n z-index: var(--sf-z-index-controls);\n opacity: 0.7;\n transition: opacity var(--sf-transition-normal);\n cursor: pointer;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.sf-player__logo svg {\n width: 55px;\n height: 20px;\n}\n\n/* Logo hover state */\n.sf-player:hover .sf-player__logo {\n opacity: 0.9;\n}\n\n/* Hide logo when minimized */\n.sf-player--minimized .sf-player__logo {\n display: none;\n} ";
3818
- const componentsVideoCss = "/* \n * Video component styles for the Saltfish playlist Player\n * Following BEM naming convention\n */\n\n/* Video container */\n.sf-video-container {\n position: relative;\n width: 100%;\n height: 100%;\n border-radius: var(--sf-border-radius-md);\n overflow: hidden;\n pointer-events: auto; /* Ensure clicks on video container are captured */\n background-color: #000; /* Fallback background for audio-only mode */\n}\n\n/* Audio fallback poster image */\n.sf-video-container--audio-fallback {\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n background-image: var(--sf-audio-poster-url, none);\n}\n\n/* Audio fallback overlay */\n.sf-audio-fallback-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.6);\n z-index: 2;\n pointer-events: none;\n}\n\n/* Avatar mode overlay - no background, just container */\n.sf-audio-fallback-overlay--avatar {\n background: none;\n}\n\n.sf-audio-fallback-overlay__icon {\n width: 60px;\n height: 60px;\n margin-bottom: 16px;\n color: white;\n opacity: 0.9;\n}\n\n.sf-audio-fallback-overlay__text {\n color: white;\n font-size: 14px;\n text-align: center;\n padding: 0 20px;\n text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);\n opacity: 0.9;\n}\n\n.sf-audio-fallback-overlay__avatar {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n z-index: 1;\n}\n\n/* Semi-transparent overlay on top of avatar */\n.sf-audio-fallback-overlay__dim {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: rgba(0, 0, 0, 0.3);\n z-index: 2;\n}\n\n/* Video element */\n.sf-video-container__video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n /* Ensure video is visible on mobile */\n display: block;\n /* Add explicit positioning to ensure video is visible */\n position: relative;\n z-index: 1;\n}\n\n/* Video blur effect (for end-of-video transitions) */\n.sf-video-container__video--blurred {\n filter: blur(3px);\n transition: filter 0.3s ease-out;\n}\n\n\n\n/* Mobile-specific video styles */\n@media (max-width: 768px) {\n \n .sf-video-container__video {\n /* Force video dimensions on mobile */\n width: 100% !important;\n height: 100% !important;\n object-fit: cover !important;\n /* Prevent video from being hidden */\n opacity: 1 !important;\n visibility: visible !important;\n /* Ensure video is above any potential overlays */\n z-index: 10 !important;\n position: relative !important;\n }\n \n /* Make controls more touch-friendly on mobile */\n .sf-video-container__controls {\n height: 5px; /* Thicker on mobile for easier touch */\n }\n \n .sf-video-container__mute-button {\n /* Use CSS variable for consistent sizing */\n min-width: var(--sf-mute-button-size) !important;\n min-height: var(--sf-mute-button-size) !important;\n opacity: 1; /* Always visible on mobile (no hover) */\n }\n \n .sf-video-container__cc-button {\n /* Use CSS variable for consistent sizing */\n min-width: var(--sf-cc-button-size) !important;\n min-height: var(--sf-cc-button-size) !important;\n opacity: 1; /* Always visible on mobile (no hover) */\n }\n}\n\n/* Touch device specific styles */\n@media (pointer: coarse) {\n .sf-video-container__controls:hover {\n height: 5px; /* Keep consistent height on touch devices */\n }\n \n .sf-video-container__mute-button {\n opacity: 1; /* Always show on touch devices */\n }\n \n .sf-video-container__cc-button {\n opacity: 1; /* Always show on touch devices */\n }\n \n .sf-video-container:hover .sf-video-container__mute-button {\n opacity: 1;\n }\n}\n\n/* Video in minimized state */\n.sf-player--minimized .sf-video-container {\n border-radius: var(--sf-border-radius-circle);\n cursor: pointer;\n z-index: var(--sf-z-index-base);\n width: 100%;\n height: 100%;\n}\n\n.sf-player--minimized .sf-video-container__video {\n border-radius: var(--sf-border-radius-circle);\n object-fit: cover;\n width: 100%;\n height: 100%;\n}\n\n/* Hide progress bar in minimized state */\n.sf-player--minimized .sf-video-container__controls {\n display: none !important;\n}\n\n/* Also hide mute button in minimized state */\n.sf-player--minimized .sf-video-container__mute-button {\n display: none !important;\n}\n\n/* Also hide CC button in minimized state */\n.sf-player--minimized .sf-video-container__cc-button {\n display: none !important;\n}\n\n/* Progress bar container */\n.sf-video-container__controls {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 3px;\n background-color: rgba(255, 255, 255, 0.25);\n z-index: var(--sf-z-index-controls);\n border-radius: var(--sf-border-radius-md) var(--sf-border-radius-md) 0 0;\n cursor: pointer;\n}\n\n/* Show slightly thicker progress bar on hover for better UX */\n.sf-video-container__controls:hover {\n height: 5px;\n}\n\n/* Progress indicator */\n.sf-video-container__progress {\n height: 100%;\n background: rgba(255, 255, 255, 0.8);\n width: 0%;\n transition: width 0.1s linear;\n border-radius: var(--sf-border-radius-md) 0 0 0;\n cursor: pointer;\n transform-origin: left;\n}\n\n/* Mute button */\n.sf-video-container__mute-button {\n position: absolute;\n top: calc(8px + var(--sf-minimize-button-size) + 6px);\n right: 6px;\n width: var(--sf-mute-button-size);\n height: var(--sf-mute-button-size);\n background-color: transparent;\n border-radius: var(--sf-border-radius-circle);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: var(--sf-z-index-controls);\n color: white;\n border: none;\n font-size: calc(var(--sf-font-size-md) + 2px);\n transition: all var(--sf-transition-normal);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n opacity: 0;\n}\n\n/* Mute button hover state */\n.sf-video-container__mute-button:hover {\n transform: scale(1.1);\n}\n\n/* Show mute button on container hover */\n.sf-video-container:hover .sf-video-container__mute-button {\n opacity: 1;\n}\n\n/* CC button */\n.sf-video-container__cc-button {\n position: absolute;\n top: calc(8px + var(--sf-minimize-button-size) + 6px + var(--sf-mute-button-size) + 6px);\n right: 6px;\n width: var(--sf-cc-button-size);\n height: var(--sf-cc-button-size);\n background-color: transparent;\n border-radius: 50%; /* Ensure perfect circle */\n padding: 0; /* Remove default button padding */\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: var(--sf-z-index-controls);\n color: white;\n border: none;\n font-size: calc(var(--sf-font-size-md) + 2px);\n transition: all var(--sf-transition-normal);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n opacity: 0;\n}\n\n.sf-video-container__cc-button svg {\n display: block;\n margin: auto;\n width: 60%;\n height: 60%;\n}\n\n/* CC button hover state */\n.sf-video-container__cc-button:hover {\n transform: scale(1.1);\n}\n\n/* Hide mute and cc buttons in autoplayBlocked and idleMode states */\n.sf-player--autoplayBlocked .sf-video-container__mute-button,\n.sf-player--autoplayBlocked .sf-video-container__cc-button,\n.sf-player--idleMode .sf-video-container__mute-button,\n.sf-player--idleMode .sf-video-container__cc-button {\n display: none !important;\n}\n\n/* Show mute and cc buttons on hover in playing/paused states */\n.sf-player--playing .sf-video-container:hover .sf-video-container__mute-button,\n.sf-player--playing .sf-video-container:hover .sf-video-container__cc-button,\n.sf-player--paused .sf-video-container:hover .sf-video-container__mute-button,\n.sf-player--paused .sf-video-container:hover .sf-video-container__cc-button {\n opacity: 1;\n} ";
3780
+ const baseVariablesCss = "/* \n * Variables for the Saltfish playlist Player\n * Defines all design tokens used throughout the application\n */\n\n:host {\n /* Colors */\n --sf-primary-color: #4a9bff;\n --sf-secondary-color: #6ccfff;\n --sf-background-color: #1e1e1e;\n --sf-text-color: #ffffff;\n --sf-button-bg: rgba(0, 0, 0, 0.5);\n --sf-button-hover-bg: rgba(0, 0, 0, 0.7);\n --sf-overlay-gradient: linear-gradient(180deg, rgba(0, 0, 0, 0.7) 0%, transparent 30%, transparent 70%, rgba(0, 0, 0, 0.7) 100%);\n --sf-progress-gradient: linear-gradient(90deg, var(--sf-primary-color), var(--sf-secondary-color));\n --sf-error-color: #ff4d4d;\n --sf-error-bg: rgba(255, 77, 77, 0.1);\n \n /* Spacing */\n --sf-spacing-xs: 4px;\n --sf-spacing-sm: 8px;\n --sf-spacing-md: 12px;\n --sf-spacing-lg: 16px;\n --sf-spacing-xl: 24px;\n \n /* Sizes */\n --sf-player-width: 240px;\n --sf-player-height: 336px;\n --sf-player-min-width: 80px;\n --sf-player-min-height: 80px;\n --sf-player-compact-width: 120px;\n --sf-player-compact-height: 120px;\n --sf-control-button-size: 24px;\n --sf-play-button-size: 60px;\n --sf-play-button-compact-size: 44px;\n --sf-minimize-button-size: 34px;\n --sf-mute-button-size: 32px;\n --sf-cc-button-size: 32px;\n --sf-cursor-size: 32px;\n \n /* Border radius */\n --sf-border-radius-sm: 4px;\n --sf-border-radius-md: 8px;\n --sf-border-radius-lg: 16px;\n --sf-border-radius-circle: 50%;\n \n /* Transitions */\n --sf-transition-fast: 0.1s ease;\n --sf-transition-normal: 0.2s ease;\n --sf-transition-slow: 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n \n /* Shadows */\n --sf-shadow-small: 0 2px 5px rgba(0, 0, 0, 0.2);\n --sf-shadow-medium: 0 4px 8px rgba(0, 0, 0, 0.15);\n --sf-shadow-large: 0 10px 25px rgba(0, 0, 0, 0.2);\n \n /* Z-index layering */\n --sf-z-index-base: 1;\n --sf-z-index-overlay: 2;\n --sf-z-index-controls: 10;\n --sf-z-index-cursor: 9999;\n --sf-z-index-player: 2147483648;\n \n /* Font sizes */\n --sf-font-size-sm: 14px;\n --sf-font-size-md: 16px;\n --sf-font-size-lg: 18px;\n --sf-font-size-xl: 24px;\n} \n\n/* Mobile device responsive adjustments - make player smaller for mobile screens */\n@media (max-width: 768px) {\n :host {\n /* Reduce player size on mobile for better space utilization */\n --sf-player-width: 180px; /* 25% smaller than desktop (240px -> 180px) */\n --sf-player-height: 252px; /* 25% smaller than desktop (336px -> 252px) */\n --sf-player-min-width: 60px; /* Smaller when minimized (80px -> 60px) */\n --sf-player-min-height: 60px; /* Smaller when minimized (80px -> 60px) */\n --sf-player-compact-width: 90px; /* Smaller compact mode for mobile (120px -> 90px) */\n --sf-player-compact-height: 90px; /* Smaller compact mode for mobile (120px -> 90px) */\n\n /* Keep controls touch-friendly despite smaller player size */\n --sf-play-button-size: 44px; /* Smaller but still touch-friendly (60px -> 44px) */\n --sf-play-button-compact-size: 36px; /* Touch-friendly compact play button */\n --sf-control-button-size: 28px; /* Keep larger for touch targets (24px -> 28px) */\n --sf-mute-button-size: 26px; /* Smaller for mobile (32px -> 26px) */\n --sf-cc-button-size: 26px; /* Smaller for mobile (32px -> 26px) */\n --sf-minimize-button-size: 26px; /* Match other mobile button sizes */\n }\n}\n\n/* Touch device specific adjustments (tablets and larger touch devices, excluding mobile) */\n@media (pointer: coarse) and (min-width: 769px) {\n :host {\n /* Ensure touch-friendly sizes even on larger touch devices */\n --sf-control-button-size: 28px;\n --sf-mute-button-size: 38px;\n --sf-cc-button-size: 38px;\n --sf-minimize-button-size: 38px; /* Match other touch device button sizes */\n }\n} ";
3781
+ const componentsPlayerCss = "/* \n * Player component styles for the Saltfish playlist Player\n * Following BEM naming convention\n */\n\n/* Main player container */\n.sf-player {\n border-radius: var(--sf-border-radius-lg);\n box-shadow: 0 25px 50px rgba(0, 0, 0, 0.45), 0 10px 20px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(255, 255, 255, 0.08);\n position: relative;\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n opacity: 0;\n /* Smooth transitions for size changes and opacity */\n transition: opacity 0.3s ease-in-out,\n width 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n height 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),\n border-radius 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n/* Player visible state - show with fade in */\n.sf-player--visible {\n opacity: 1;\n}\n\n/* Dark gradient overlay at bottom of player */\n.sf-player::before {\n content: '';\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 33.33%; /* One third of player height */\n background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);\n pointer-events: none;\n z-index: var(--sf-z-index-overlay);\n border-radius: 8px;\n}\n\n/* Hide gradient overlay when minimized */\n.sf-player--minimized::before {\n display: none;\n}\n\n/* Full-size player state */\n.sf-player:not(.sf-player--minimized) {\n width: var(--sf-player-width);\n height: var(--sf-player-height);\n}\n\n/* Autoplay fallback state - ensure play button is visible */\n.sf-player--waiting-for-user-interaction .sf-controls-container__play-button {\n display: flex !important;\n opacity: 1 !important;\n visibility: visible !important;\n}\n\n/* Also show the center play button in autoplay fallback state */\n.sf-player--waiting-for-user-interaction .sf-player__center-play-button {\n display: flex !important;\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 20) !important; /* Higher z-index to appear above overlay */\n}\n\n/* Make the autoplay fallback state more prominent to indicate need for interaction */\n.sf-player--waiting-for-user-interaction::after {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.3);\n pointer-events: none;\n z-index: var(--sf-z-index-overlay);\n}\n\n/* Player state: minimized */\n.sf-player--minimized {\n /* Equal width and height are essential for maintaining a perfect circle when using border-radius: 50% */\n width: var(--sf-player-min-width);\n height: var(--sf-player-min-height);\n border-radius: var(--sf-border-radius-circle);\n box-shadow: 0 15px 30px rgba(0, 0, 0, 0.35), 0 5px 15px rgba(0, 0, 0, 0.25), 0 0 0 2px rgba(255, 255, 255, 0.08);\n cursor: pointer;\n /* Force overriding any inline styles that might be applied */\n max-width: var(--sf-player-min-width) !important;\n max-height: var(--sf-player-min-height) !important;\n min-width: var(--sf-player-min-width) !important;\n min-height: var(--sf-player-min-height) !important;\n}\n\n/* Player state: compact (first step, small rounded) */\n.sf-player--compact {\n width: var(--sf-player-compact-width) !important;\n height: var(--sf-player-compact-height) !important;\n border-radius: var(--sf-border-radius-circle);\n box-shadow: 0 15px 30px rgba(0, 0, 0, 0.35), 0 5px 15px rgba(0, 0, 0, 0.25), 0 0 0 2px rgba(255, 255, 255, 0.08);\n /* Force overriding any inline styles that might be applied */\n max-width: var(--sf-player-compact-width) !important;\n max-height: var(--sf-player-compact-height) !important;\n min-width: var(--sf-player-compact-width) !important;\n min-height: var(--sf-player-compact-height) !important;\n}\n\n/* Adjust play button size in compact mode */\n.sf-player--compact .sf-player__center-play-button {\n width: var(--sf-play-button-compact-size);\n height: var(--sf-play-button-compact-size);\n}\n\n/* Hide elements in compact mode */\n.sf-player--compact .sf-player__logo,\n.sf-player--compact .sf-player__minimize-button {\n display: none;\n}\n\n/* Hide gradient overlay when in compact mode */\n.sf-player--compact::before {\n display: none;\n}\n\n/* Hide controls when minimized */\n.sf-player--minimized .sf-controls-container {\n display: none;\n}\n\n/* Hide controls when in compact mode */\n.sf-player--compact .sf-controls-container {\n display: none;\n}\n\n/* Only show the minimize button when hovering on minimized player */\n.sf-player--minimized .sf-player__minimize-button {\n opacity: 0;\n}\n\n.sf-player--minimized:hover .sf-player__minimize-button {\n opacity: 1;\n}\n\n/* Player root element */\n#sf-player-root {\n position: fixed;\n z-index: var(--sf-z-index-player);\n}\n\n/* Fixed positioning classes */\n.sf-player-root--bottom-left {\n bottom: 20px;\n left: 20px;\n}\n\n.sf-player-root--bottom-right {\n bottom: 20px;\n right: 20px;\n}\n\n/* Player error message */\n.sf-player__error {\n padding: var(--sf-spacing-md);\n color: var(--sf-error-color);\n background-color: var(--sf-error-bg);\n border-radius: var(--sf-border-radius-md);\n margin: var(--sf-spacing-sm);\n font-size: var(--sf-font-size-sm);\n border-left: 4px solid var(--sf-error-color);\n}\n\n/* Minimize button */\n.sf-player__minimize-button {\n position: absolute;\n top: 8px;\n right: 6px;\n width: var(--sf-minimize-button-size);\n height: var(--sf-minimize-button-size);\n background-color: transparent;\n border-radius: var(--sf-border-radius-circle);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: var(--sf-z-index-controls);\n color: white;\n border: none;\n font-size: calc(var(--sf-font-size-sm) + 4px);\n transition: all var(--sf-transition-normal);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n opacity: 0;\n}\n\n/* Minimize button hover state */\n.sf-player__minimize-button:hover {\n transform: scale(1.1);\n}\n\n/* Show minimize button on player hover */\n.sf-player:hover .sf-player__minimize-button {\n opacity: 1;\n}\n\n/* Mobile and touch device overrides for minimize button visibility */\n/* Ensure minimize button is always visible on touch devices, even when minimized */\n@media (pointer: coarse) {\n .sf-player__minimize-button {\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 50) !important; /* Much higher z-index for touch devices */\n }\n \n .sf-player--minimized .sf-player__minimize-button {\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 50) !important; /* Much higher z-index for touch devices */\n }\n}\n\n/* Ensure minimize button is always visible on mobile screens under 768px */\n@media (max-width: 768px) {\n .sf-player__minimize-button {\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 50) !important; /* Much higher z-index for mobile */\n /* Position closer to top right corner on mobile */\n top: var(--sf-spacing-xs) !important; /* 4px from top instead of 16px */\n right: var(--sf-spacing-xs) !important; /* 4px from right instead of 12px */\n }\n \n .sf-player--minimized .sf-player__minimize-button {\n opacity: 1 !important;\n z-index: calc(var(--sf-z-index-controls) + 50) !important; /* Much higher z-index for mobile */\n /* Position closer to top right corner on mobile */\n top: var(--sf-spacing-xs) !important; /* 4px from top instead of 16px */\n right: var(--sf-spacing-xs) !important; /* 4px from right instead of 12px */\n }\n}\n\n/* Player title */\n.sf-player__title {\n position: absolute;\n top: var(--sf-spacing-md);\n left: var(--sf-spacing-md);\n color: var(--sf-text-color);\n font-size: var(--sf-font-size-md);\n font-weight: 600;\n z-index: var(--sf-z-index-controls);\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);\n max-width: 70%;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Centered play/pause button overlay */\n.sf-player__center-play-button {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: var(--sf-play-button-size);\n height: var(--sf-play-button-size);\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: var(--sf-border-radius-circle);\n display: none; /* Hidden by default */\n justify-content: center;\n align-items: center;\n z-index: calc(var(--sf-z-index-controls) + 10); /* Ensure higher z-index than other elements */\n color: white;\n border: none;\n font-size: var(--sf-control-button-size);\n cursor: pointer;\n transition: transform var(--sf-transition-normal), background-color var(--sf-transition-normal);\n backdrop-filter: blur(3px);\n -webkit-backdrop-filter: blur(3px);\n box-shadow: 0 15px 30px rgba(0, 0, 0, 0.5), 0 5px 15px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(255, 255, 255, 0.08);\n pointer-events: auto; /* Enable pointer events to capture clicks */\n}\n\n/* Center play button visible state */\n.sf-player__center-play-button--visible {\n display: flex !important;\n}\n\n/* Center play button prominent state (for autoplay blocked, idle mode) */\n.sf-player__center-play-button--prominent {\n opacity: 1 !important;\n pointer-events: auto !important;\n}\n\n/* Center play button hover state */\n.sf-player__center-play-button:hover {\n transform: translate(-50%, -50%) scale(1.1);\n background-color: rgba(0, 0, 0, 0.7);\n}\n\n/* Hide center play button in minimized state */\n.sf-player--minimized .sf-player__center-play-button {\n display: none !important;\n}\n\n/* Center play button with adjustment for 3+ buttons */\n.sf-player__center-play-button--with-many-buttons {\n transform: translate(-50%, -85%) !important; /* Move up by adjusting Y offset */\n}\n\n/* Exit button for minimized mode */\n.sf-player__exit-button {\n position: absolute;\n top: -22px; /* Position it above the player */\n right: 0;\n width: 20px;\n height: 20px;\n background-color: var(--sf-button-bg);\n border-radius: var(--sf-border-radius-circle);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: var(--sf-z-index-controls);\n color: white;\n border: none;\n font-size: var(--sf-font-size-md);\n transition: all var(--sf-transition-normal);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.35);\n}\n\n/* Exit button hover state */\n.sf-player__exit-button:hover {\n transform: scale(1.1);\n background-color: var(--sf-button-hover-bg);\n}\n\n/* Show exit button on minimized player hover */\n.sf-player--minimized:hover .sf-player__exit-button {\n opacity: 1;\n}\n\n/* Saltfish logo */\n.sf-player__logo {\n position: absolute;\n bottom: var(--sf-spacing-xs);\n left: 0;\n right: 0;\n height: 18px;\n z-index: var(--sf-z-index-controls);\n opacity: 0.7;\n transition: opacity var(--sf-transition-normal);\n cursor: pointer;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.sf-player__logo svg {\n width: 55px;\n height: 20px;\n}\n\n/* Logo hover state */\n.sf-player:hover .sf-player__logo {\n opacity: 0.9;\n}\n\n/* Hide logo when minimized */\n.sf-player--minimized .sf-player__logo {\n display: none;\n} ";
3782
+ const componentsVideoCss = "/* \n * Video component styles for the Saltfish playlist Player\n * Following BEM naming convention\n */\n\n/* Video container */\n.sf-video-container {\n position: relative;\n width: 100%;\n height: 100%;\n border-radius: var(--sf-border-radius-md);\n overflow: hidden;\n pointer-events: auto; /* Ensure clicks on video container are captured */\n background-color: #000; /* Fallback background for audio-only mode */\n}\n\n/* Audio fallback poster image */\n.sf-video-container--audio-fallback {\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n background-image: var(--sf-audio-poster-url, none);\n}\n\n/* Audio fallback overlay */\n.sf-audio-fallback-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.6);\n z-index: 2;\n pointer-events: none;\n}\n\n/* Avatar mode overlay - no background, just container */\n.sf-audio-fallback-overlay--avatar {\n background: none;\n}\n\n.sf-audio-fallback-overlay__icon {\n width: 60px;\n height: 60px;\n margin-bottom: 16px;\n color: white;\n opacity: 0.9;\n}\n\n.sf-audio-fallback-overlay__text {\n color: white;\n font-size: 14px;\n text-align: center;\n padding: 0 20px;\n text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);\n opacity: 0.9;\n}\n\n.sf-audio-fallback-overlay__avatar {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n z-index: 1;\n}\n\n/* Semi-transparent overlay on top of avatar */\n.sf-audio-fallback-overlay__dim {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: rgba(0, 0, 0, 0.3);\n z-index: 2;\n}\n\n/* Soundbar-only mode overlay - dark background for soundbar visibility */\n.sf-audio-fallback-overlay--soundbar-only {\n background: rgba(0, 0, 0, 0.85);\n}\n\n/* Audio visualization soundbar container */\n.sf-audio-soundbar {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n height: 120px;\n width: auto;\n max-width: 300px;\n z-index: 3;\n position: relative;\n padding: 0 20px;\n}\n\n/* Individual soundbar frequency bar */\n.sf-audio-soundbar__bar {\n flex: 1;\n min-width: 8px;\n max-width: 20px;\n height: 10%; /* Default minimum height */\n background-color: hsla(180, 70%, 60%, 0.3);\n border-radius: 4px;\n transition: height 0.05s ease-out, background-color 0.1s ease-out;\n transform-origin: center;\n will-change: height, background-color;\n}\n\n/* Video element */\n.sf-video-container__video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n /* Ensure video is visible on mobile */\n display: block;\n /* Add explicit positioning to ensure video is visible */\n position: relative;\n z-index: 1;\n}\n\n/* Video blur effect (for end-of-video transitions) */\n.sf-video-container__video--blurred {\n filter: blur(3px);\n transition: filter 0.3s ease-out;\n}\n\n\n\n/* Mobile-specific video styles */\n@media (max-width: 768px) {\n \n .sf-video-container__video {\n /* Force video dimensions on mobile */\n width: 100% !important;\n height: 100% !important;\n object-fit: cover !important;\n /* Prevent video from being hidden */\n opacity: 1 !important;\n visibility: visible !important;\n /* Ensure video is above any potential overlays */\n z-index: 10 !important;\n position: relative !important;\n }\n \n /* Make controls more touch-friendly on mobile */\n .sf-video-container__controls {\n height: 5px; /* Thicker on mobile for easier touch */\n }\n \n .sf-video-container__mute-button {\n /* Use CSS variable for consistent sizing */\n min-width: var(--sf-mute-button-size) !important;\n min-height: var(--sf-mute-button-size) !important;\n opacity: 1; /* Always visible on mobile (no hover) */\n }\n \n .sf-video-container__cc-button {\n /* Use CSS variable for consistent sizing */\n min-width: var(--sf-cc-button-size) !important;\n min-height: var(--sf-cc-button-size) !important;\n opacity: 1; /* Always visible on mobile (no hover) */\n }\n}\n\n/* Touch device specific styles */\n@media (pointer: coarse) {\n .sf-video-container__controls:hover {\n height: 5px; /* Keep consistent height on touch devices */\n }\n \n .sf-video-container__mute-button {\n opacity: 1; /* Always show on touch devices */\n }\n \n .sf-video-container__cc-button {\n opacity: 1; /* Always show on touch devices */\n }\n \n .sf-video-container:hover .sf-video-container__mute-button {\n opacity: 1;\n }\n}\n\n/* Video in minimized state */\n.sf-player--minimized .sf-video-container {\n border-radius: var(--sf-border-radius-circle);\n cursor: pointer;\n z-index: var(--sf-z-index-base);\n width: 100%;\n height: 100%;\n}\n\n.sf-player--minimized .sf-video-container__video {\n border-radius: var(--sf-border-radius-circle);\n object-fit: cover;\n width: 100%;\n height: 100%;\n}\n\n/* Hide progress bar in minimized state */\n.sf-player--minimized .sf-video-container__controls {\n display: none !important;\n}\n\n/* Also hide mute button in minimized state */\n.sf-player--minimized .sf-video-container__mute-button {\n display: none !important;\n}\n\n/* Also hide CC button in minimized state */\n.sf-player--minimized .sf-video-container__cc-button {\n display: none !important;\n}\n\n/* Progress bar container */\n.sf-video-container__controls {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 3px;\n background-color: rgba(255, 255, 255, 0.25);\n z-index: var(--sf-z-index-controls);\n border-radius: var(--sf-border-radius-md) var(--sf-border-radius-md) 0 0;\n cursor: pointer;\n}\n\n/* Show slightly thicker progress bar on hover for better UX */\n.sf-video-container__controls:hover {\n height: 5px;\n}\n\n/* Progress indicator */\n.sf-video-container__progress {\n height: 100%;\n background: rgba(255, 255, 255, 0.8);\n width: 0%;\n transition: width 0.1s linear;\n border-radius: var(--sf-border-radius-md) 0 0 0;\n cursor: pointer;\n transform-origin: left;\n}\n\n/* Mute button */\n.sf-video-container__mute-button {\n position: absolute;\n top: calc(8px + var(--sf-minimize-button-size) + 6px);\n right: 6px;\n width: var(--sf-mute-button-size);\n height: var(--sf-mute-button-size);\n background-color: transparent;\n border-radius: var(--sf-border-radius-circle);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: var(--sf-z-index-controls);\n color: white;\n border: none;\n font-size: calc(var(--sf-font-size-md) + 2px);\n transition: all var(--sf-transition-normal);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n opacity: 0;\n}\n\n/* Mute button hover state */\n.sf-video-container__mute-button:hover {\n transform: scale(1.1);\n}\n\n/* Show mute button on container hover */\n.sf-video-container:hover .sf-video-container__mute-button {\n opacity: 1;\n}\n\n/* CC button */\n.sf-video-container__cc-button {\n position: absolute;\n top: calc(8px + var(--sf-minimize-button-size) + 6px + var(--sf-mute-button-size) + 6px);\n right: 6px;\n width: var(--sf-cc-button-size);\n height: var(--sf-cc-button-size);\n background-color: transparent;\n border-radius: 50%; /* Ensure perfect circle */\n padding: 0; /* Remove default button padding */\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n z-index: var(--sf-z-index-controls);\n color: white;\n border: none;\n font-size: calc(var(--sf-font-size-md) + 2px);\n transition: all var(--sf-transition-normal);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n opacity: 0;\n}\n\n.sf-video-container__cc-button svg {\n display: block;\n margin: auto;\n width: 60%;\n height: 60%;\n}\n\n/* CC button hover state */\n.sf-video-container__cc-button:hover {\n transform: scale(1.1);\n}\n\n/* Hide mute and cc buttons in autoplayBlocked and idleMode states */\n.sf-player--autoplayBlocked .sf-video-container__mute-button,\n.sf-player--autoplayBlocked .sf-video-container__cc-button,\n.sf-player--idleMode .sf-video-container__mute-button,\n.sf-player--idleMode .sf-video-container__cc-button {\n display: none !important;\n}\n\n/* Show mute and cc buttons on hover in playing/paused states */\n.sf-player--playing .sf-video-container:hover .sf-video-container__mute-button,\n.sf-player--playing .sf-video-container:hover .sf-video-container__cc-button,\n.sf-player--paused .sf-video-container:hover .sf-video-container__mute-button,\n.sf-player--paused .sf-video-container:hover .sf-video-container__cc-button {\n opacity: 1;\n} ";
3819
3783
  const componentsControlsCss = "/* \n * Controls component styles for the Saltfish playlist Player\n * Following BEM naming convention\n */\n\n/* Main controls container */\n.sf-controls-container {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: transparent;\n z-index: var(--sf-z-index-controls);\n pointer-events: auto;\n}\n\n/* Play button */\n.sf-controls-container__play-button {\n background-color: rgba(0, 0, 0, 0.6);\n border: none;\n color: var(--sf-text-color);\n font-size: var(--sf-control-button-size);\n cursor: pointer;\n width: var(--sf-play-button-size);\n height: var(--sf-play-button-size);\n border-radius: var(--sf-border-radius-circle);\n display: flex;\n justify-content: center;\n align-items: center;\n transition: transform var(--sf-transition-normal), background-color var(--sf-transition-normal);\n padding-left: 4px; /* Optical centering for play icon */\n}\n\n/* Button hover state */\n.sf-controls-container__play-button:hover {\n background-color: rgba(0, 0, 0, 0.4);\n transform: scale(1.1);\n}\n\n/* Button container for interactive buttons */\n.sf-controls-container__buttons {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: var(--sf-spacing-md);\n}\n\n/* Interactive button */\n.sf-controls-container__interactive-button {\n background-color: rgba(255, 255, 255, 0.2);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n color: white;\n border: none;\n border-radius: var(--sf-border-radius-md);\n padding: var(--sf-spacing-xs) var(--sf-spacing-md);\n font-size: var(--sf-font-size-sm);\n cursor: pointer;\n transition: background-color var(--sf-transition-normal), transform var(--sf-transition-fast);\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.sf-controls-container__interactive-button:hover {\n background-color: rgba(255, 255, 255, 0.3);\n transform: translateY(-2px);\n}\n\n/* \n * Choice buttons container and buttons - positioned inside player\n */\n\n/* Choice buttons container - positioned inside the player at the bottom */\n.sf-choice-buttons-container {\n position: absolute;\n bottom: var(--sf-spacing-xl);\n left: 50%;\n transform: translateX(-50%);\n width: calc(100% - var(--sf-spacing-lg));\n max-width: calc(100% - var(--sf-spacing-lg));\n z-index: calc(var(--sf-z-index-controls) + 1);\n gap: var(--sf-spacing-sm);\n pointer-events: auto;\n align-items: center;\n display: flex;\n flex-direction: column;\n /* Ensure container is fully transparent */\n background: transparent;\n border: none;\n outline: none;\n}\n\n/* Choice buttons container with scrolling for 4+ buttons */\n.sf-choice-buttons-container--scrollable {\n max-height: 132px; /* Show ~3 buttons with gaps (3 * 36px button + 3 * 8px gap) */\n overflow-y: auto;\n overflow-x: hidden;\n scrollbar-width: thin; /* Show thin scrollbar in Firefox */\n scrollbar-color: rgba(255, 255, 255, 0.2) transparent; /* More transparent Firefox scrollbar */\n scroll-behavior: smooth;\n /* Important: Use block display for scrollable container */\n display: block !important;\n}\n\n/* Style webkit scrollbar to be visible but subtle */\n.sf-choice-buttons-container--scrollable::-webkit-scrollbar {\n width: 4px;\n}\n\n.sf-choice-buttons-container--scrollable::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.sf-choice-buttons-container--scrollable::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.2); /* More transparent */\n border-radius: 2px;\n}\n\n.sf-choice-buttons-container--scrollable::-webkit-scrollbar-thumb:hover {\n background: rgba(255, 255, 255, 0.4); /* Still subtle on hover */\n}\n\n/* Removed fade gradient to keep container fully transparent */\n\n/* Choice button styles - solid rounded buttons matching the image */\n.sf-choice-button {\n width: 100%;\n max-width: none;\n background: rgba(0, 0, 0, 4);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n color: white;\n border: none;\n border-radius: 24px; /* More rounded for pill shape */\n padding: var(--sf-spacing-md) var(--sf-spacing-md);\n font-size: 12px;\n cursor: pointer;\n transition: all 0.2s ease;\n text-align: center;\n outline: none;\n font-family: inherit;\n margin-bottom: 0;\n position: relative;\n overflow: hidden;\n opacity: 0; /* Start hidden for animation */\n pointer-events: none;\n /* Animation will be triggered by JavaScript when video reaches 90% */\n}\n\n/* Add margin between buttons in scrollable container */\n.sf-choice-buttons-container--scrollable .sf-choice-button {\n margin-bottom: var(--sf-spacing-sm);\n}\n\n/* Remove margin from last button */\n.sf-choice-buttons-container--scrollable .sf-choice-button:last-child {\n margin-bottom: 0;\n}\n\n/* Hover state for buttons */\n.sf-choice-button:hover {\n background: rgba(0, 0, 0, 0.9);\n transform: translateY(-2px);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.8);\n}\n\n/* Active state for all buttons */\n.sf-choice-button:active {\n transform: translateY(0) scale(0.98);\n transition: all 0.1s ease;\n}\n\n/* Remove specific action type styling - use consistent dark buttons */\n.sf-choice-button--goto,\n.sf-choice-button--url,\n.sf-choice-button--next,\n.sf-choice-button--dom,\n.sf-choice-button--function {\n background: rgba(0, 0, 0, 0.4);\n border: none;\n}\n\n.sf-choice-button--goto:hover,\n.sf-choice-button--url:hover,\n.sf-choice-button--next:hover,\n.sf-choice-button--dom:hover,\n.sf-choice-button--function:hover {\n background: rgba(0, 0, 0, 0.9);\n transform: translateY(-2px);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);\n}\n\n/* Hide choice buttons in minimized state */\n.sf-player--minimized .sf-choice-buttons-container {\n display: none;\n}\n\n/* Hide choice buttons in autoplayBlocked and idleMode states */\n.sf-player--autoplayBlocked .sf-choice-buttons-container,\n.sf-player--idleMode .sf-choice-buttons-container {\n display: none !important;\n}\n\n/* Smaller mobile screens - maintain same layout but with tighter spacing */\n@media (max-width: 480px) {\n .sf-choice-buttons-container {\n bottom: var(--sf-spacing-sm);\n width: calc(100% - var(--sf-spacing-md));\n max-width: calc(100% - var(--sf-spacing-md));\n gap: calc(var(--sf-spacing-xs) + 2px);\n }\n \n .sf-choice-button {\n padding: var(--sf-spacing-sm) var(--sf-spacing-sm);\n font-size: 11px;\n border-radius: 20px;\n }\n}\n\n/* Touch device specific adjustments */\n@media (pointer: coarse) {\n .sf-choice-buttons-container {\n gap: var(--sf-spacing-sm);\n }\n \n .sf-choice-button {\n min-height: 44px; /* Apple's recommended minimum touch target size */\n padding: var(--sf-spacing-md) var(--sf-spacing-md);\n }\n}\n\n/* Button fade-in animation */\n@keyframes buttonFadeIn {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Animation class that will be added by JavaScript at 90% video progress */\n.sf-choice-button.sf-show-button {\n animation: buttonFadeIn 0.3s ease-out forwards;\n pointer-events: auto;\n}\n\n/* Staggered animation delays for multiple buttons when shown */\n.sf-choice-button.sf-show-button:nth-child(1) {\n animation-delay: 0s;\n}\n\n.sf-choice-button.sf-show-button:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.sf-choice-button.sf-show-button:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n.sf-choice-button.sf-show-button:nth-child(4) {\n animation-delay: 0.45s;\n}\n\n.sf-choice-button.sf-show-button:nth-child(5) {\n animation-delay: 0.6s;\n}\n\n.sf-choice-button.sf-show-button:nth-child(6) {\n animation-delay: 0.75s;\n}\n\n.sf-choice-button.sf-show-button:nth-child(7) {\n animation-delay: 0.9s;\n}\n\n.sf-choice-button.sf-show-button:nth-child(8) {\n animation-delay: 1.05s;\n}\n\n/* Scroll indicator arrow */\n.sf-scroll-indicator {\n position: absolute;\n bottom: calc(var(--sf-spacing-xl) - 25px);\n left: 50%;\n transform: translateX(-50%);\n color: rgba(255, 255, 255, 0.6);\n font-size: 20px;\n pointer-events: none;\n z-index: calc(var(--sf-z-index-controls) + 2);\n transition: opacity 0.3s ease;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n opacity: 0; /* Start hidden */\n}\n\n/* Show scroll indicator with animation - appears after buttons */\n.sf-scroll-indicator.sf-show-scroll-indicator {\n animation: scrollIndicatorFadeIn 0.4s ease-out 1.2s forwards, bounceArrow 1.5s ease-in-out 1.6s infinite;\n}\n\n/* Hide arrow when scrolled to bottom */\n.sf-scroll-indicator--hidden {\n opacity: 0;\n}\n\n/* Fade in animation for scroll indicator */\n@keyframes scrollIndicatorFadeIn {\n from {\n opacity: 0;\n transform: translateX(-50%) translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateX(-50%) translateY(0);\n }\n}\n\n/* Bounce animation for the arrow */\n@keyframes bounceArrow {\n 0%, 100% {\n transform: translateX(-50%) translateY(0);\n }\n 50% {\n transform: translateX(-50%) translateY(5px);\n }\n}\n\n/* Hide scroll indicator in minimized state */\n.sf-player--minimized .sf-scroll-indicator {\n display: none;\n}\n\n/* Hide scroll indicator in autoplayBlocked and idleMode states */\n.sf-player--autoplayBlocked .sf-scroll-indicator,\n.sf-player--idleMode .sf-scroll-indicator {\n display: none !important;\n} ";
3820
3784
  const componentsTranscriptCss = "/* \n * Transcript component styles for the Saltfish Playlist Player\n * Following BEM naming convention\n */\n\n/* Transcript container */\n.sf-transcript {\n position: absolute;\n bottom: 40px; /* Above the progress bar */\n left: 0;\n right: 0;\n max-height: 200px;\n background: transparent;\n margin: 0 var(--sf-spacing-md);\n overflow: hidden;\n z-index: var(--sf-z-index-overlay);\n opacity: 0;\n transform: translateY(20px);\n transition: all 0.3s ease-out;\n pointer-events: none;\n}\n\n/* Visible state */\n.sf-transcript--visible {\n opacity: 1;\n transform: translateY(0);\n pointer-events: auto;\n}\n\n/* Transcript content */\n.sf-transcript__content {\n max-height: 180px;\n overflow: visible;\n padding: 0;\n}\n\n/* Webkit scrollbar styling */\n.sf-transcript__content::-webkit-scrollbar {\n width: 4px;\n}\n\n.sf-transcript__content::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.sf-transcript__content::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.3);\n border-radius: 2px;\n}\n\n.sf-transcript__content::-webkit-scrollbar-thumb:hover {\n background: rgba(255, 255, 255, 0.5);\n}\n\n/* Word-by-word display container */\n.sf-transcript__word-container {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 0; /* Remove flex gap - spacing handled by margin on preview word */\n min-height: 60px;\n padding: 0;\n}\n\n/* Individual word styling */\n.sf-transcript__word {\n color: rgba(255, 255, 255, 0.9);\n font-size: var(--sf-font-size-lg);\n font-weight: 600;\n line-height: 1.2;\n transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);\n transform: translateY(0);\n filter: blur(0px);\n opacity: 1;\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);\n}\n\n/* Active word (currently speaking) */\n.sf-transcript__word--active {\n opacity: 1;\n filter: blur(0px);\n transform: translateY(0) scale(1);\n color: rgba(255, 255, 255, 1);\n font-weight: 600;\n}\n\n/* Preview word (clean, unblurred when shown) */\n.sf-transcript__word--preview {\n opacity: 0.8;\n filter: blur(0px);\n transform: translateY(0) scale(0.98);\n color: rgba(255, 255, 255, 0.8);\n font-weight: 400;\n margin-left: 0.2em; /* Minimal spacing - just enough for a natural gap */\n}\n\n/* Legacy segment styling (kept for compatibility) */\n.sf-transcript__segment {\n color: rgba(255, 255, 255);\n font-size: var(--sf-font-size-sm);\n line-height: 1.4;\n padding: var(--sf-spacing-xs) 0;\n cursor: pointer;\n transition: all 0.2s ease;\n padding-left: var(--sf-spacing-xs);\n padding-right: var(--sf-spacing-xs);\n}\n\n/* CC button styling removed - now handled via icon toggling */\n\n/* Mobile responsive styles */\n@media (max-width: 768px) {\n .sf-transcript {\n bottom: 50px; /* More space on mobile */\n margin: 0 var(--sf-spacing-sm);\n max-height: 150px; /* Smaller on mobile */\n }\n \n .sf-transcript__content {\n max-height: 130px;\n padding: var(--sf-spacing-sm);\n }\n \n .sf-transcript__word-container {\n min-height: 50px;\n padding: var(--sf-spacing-sm);\n gap: var(--sf-spacing-xs);\n }\n \n .sf-transcript__word {\n font-size: var(--sf-font-size-md);\n font-weight: 500;\n }\n \n .sf-transcript__segment {\n font-size: var(--sf-font-size-xs);\n padding: var(--sf-spacing-xs) var(--sf-spacing-sm);\n }\n}\n\n/* Touch device optimizations */\n@media (pointer: coarse) {\n .sf-transcript__segment {\n padding: var(--sf-spacing-sm) var(--sf-spacing-xs);\n min-height: 44px; /* Larger touch target */\n display: flex;\n align-items: center;\n }\n}\n\n/* Hide transcript in minimized state */\n.sf-player--minimized .sf-transcript {\n display: none !important;\n}\n\n/* Hide transcript in autoplayBlocked and idleMode states */\n.sf-player--autoplayBlocked .sf-transcript,\n.sf-player--idleMode .sf-transcript {\n display: none !important;\n}\n\n/* Animation for transcript appearance */\n@keyframes transcriptFadeIn {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes transcriptFadeOut {\n from {\n opacity: 1;\n transform: translateY(0);\n }\n to {\n opacity: 0;\n transform: translateY(20px);\n }\n}";
3821
3785
  const componentsErrorCss = "/* \n * Error Display component styles for the Saltfish playlist Player\n * Clean and subtle full-widget error overlay\n */\n\n/* Error display overlay - covers full widget */\n.sf-error-display {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.75);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: calc(var(--sf-z-index-controls) + 30);\n border-radius: var(--sf-border-radius-lg);\n opacity: 0;\n transition: opacity var(--sf-transition-slow);\n pointer-events: auto;\n}\n\n/* Error display visible state */\n.sf-error-display--visible {\n opacity: 1;\n}\n\n/* Error content container */\n.sf-error-display__content {\n background-color: rgba(20, 20, 20, 0.9);\n border-radius: var(--sf-border-radius-md);\n padding: var(--sf-spacing-lg) var(--sf-spacing-xl);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n border: 1px solid rgba(255, 255, 255, 0.08);\n transform: translateY(8px);\n transition: transform var(--sf-transition-slow);\n max-width: 85%;\n text-align: center;\n}\n\n/* Error content visible animation */\n.sf-error-display--visible .sf-error-display__content {\n transform: translateY(0);\n}\n\n/* Error message */\n.sf-error-display__message {\n color: rgba(255, 255, 255, 0.95);\n font-size: var(--sf-font-size-md);\n line-height: 1.5;\n margin: 0;\n text-align: center;\n font-weight: 500;\n}\n\n/* Mobile responsive adjustments */\n@media (max-width: 768px) {\n .sf-error-display__content {\n padding: var(--sf-spacing-md) var(--sf-spacing-lg);\n max-width: 90%;\n }\n\n .sf-error-display__message {\n font-size: var(--sf-font-size-sm);\n }\n}\n\n/* Minimized player state adjustments */\n.sf-player--minimized .sf-error-display__content {\n padding: var(--sf-spacing-sm) var(--sf-spacing-md);\n max-width: 80%;\n}\n\n.sf-player--minimized .sf-error-display__message {\n font-size: var(--sf-font-size-sm);\n font-weight: 400;\n}\n\n/* High contrast mode support */\n@media (prefers-contrast: high) {\n .sf-error-display__content {\n background-color: rgba(0, 0, 0, 0.95);\n border: 2px solid rgba(255, 255, 255, 0.3);\n }\n\n .sf-error-display__message {\n color: rgba(255, 255, 255, 0.95);\n }\n}\n\n/* Reduced motion support */\n@media (prefers-reduced-motion: reduce) {\n .sf-error-display,\n .sf-error-display__content {\n transition: none;\n }\n}";
@@ -5012,6 +4976,330 @@ class VideoControlsUI {
5012
4976
  return this.ccButton;
5013
4977
  }
5014
4978
  }
4979
+ class AudioVisualizationManager {
4980
+ constructor() {
4981
+ __publicField(this, "audioContext", null);
4982
+ __publicField(this, "analyser", null);
4983
+ __publicField(this, "sourceVideo1", null);
4984
+ __publicField(this, "sourceVideo2", null);
4985
+ __publicField(this, "activeSource", null);
4986
+ __publicField(this, "videoElement1", null);
4987
+ __publicField(this, "videoElement2", null);
4988
+ __publicField(this, "dataArray", null);
4989
+ __publicField(this, "animationId", null);
4990
+ __publicField(this, "isInitialized", false);
4991
+ __publicField(this, "useFallbackMode", false);
4992
+ // Use animated fallback if Web Audio fails
4993
+ __publicField(this, "isPaused", false);
4994
+ // Pause state - when true, bars stay at minimal height
4995
+ // Configuration
4996
+ __publicField(this, "fftSize", 2048);
4997
+ __publicField(this, "barCount", 12);
4998
+ // Moderate detail (10-15 range)
4999
+ __publicField(this, "smoothingTimeConstant", 0.75);
5000
+ // Smooth out rapid changes
5001
+ // Fallback animation state
5002
+ __publicField(this, "fallbackTime", 0);
5003
+ // CORS detection - wait for multiple frames before declaring failure
5004
+ __publicField(this, "zeroDataFrameCount", 0);
5005
+ __publicField(this, "maxZeroFramesBeforeFallback", 60);
5006
+ }
5007
+ // ~1 second at 60fps
5008
+ /**
5009
+ * Initializes the Web Audio API context and connects to the video element
5010
+ * Handles dual video element system by creating sources for both elements
5011
+ * @param videoElement - The video/audio element to analyze
5012
+ */
5013
+ initialize(videoElement) {
5014
+ try {
5015
+ if (!this.isInitialized) {
5016
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
5017
+ this.audioContext = new AudioContextClass();
5018
+ this.analyser = this.audioContext.createAnalyser();
5019
+ this.analyser.fftSize = this.fftSize;
5020
+ this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
5021
+ const bufferLength = this.analyser.frequencyBinCount;
5022
+ this.dataArray = new Uint8Array(bufferLength);
5023
+ this.analyser.connect(this.audioContext.destination);
5024
+ this.isInitialized = true;
5025
+ this.useFallbackMode = false;
5026
+ this.zeroDataFrameCount = 0;
5027
+ }
5028
+ if (this.videoElement1 === videoElement) {
5029
+ this.switchToSource(this.sourceVideo1);
5030
+ return;
5031
+ }
5032
+ if (this.videoElement2 === videoElement) {
5033
+ this.switchToSource(this.sourceVideo2);
5034
+ return;
5035
+ }
5036
+ const newSource = this.audioContext.createMediaElementSource(videoElement);
5037
+ if (!this.videoElement1) {
5038
+ this.videoElement1 = videoElement;
5039
+ this.sourceVideo1 = newSource;
5040
+ this.switchToSource(newSource);
5041
+ } else if (!this.videoElement2) {
5042
+ this.videoElement2 = videoElement;
5043
+ this.sourceVideo2 = newSource;
5044
+ this.switchToSource(newSource);
5045
+ } else {
5046
+ console.warn("AudioVisualizationManager: More than 2 video elements detected - this should not happen with dual video system");
5047
+ }
5048
+ this.zeroDataFrameCount = 0;
5049
+ } catch (error2) {
5050
+ console.warn("AudioVisualizationManager: Web Audio API failed, using fallback animation", error2);
5051
+ this.isInitialized = true;
5052
+ this.useFallbackMode = true;
5053
+ }
5054
+ }
5055
+ /**
5056
+ * Switches active source and connects it to the analyser
5057
+ * Disconnects previous source to avoid audio doubling
5058
+ */
5059
+ switchToSource(source) {
5060
+ if (this.activeSource && this.activeSource !== source) {
5061
+ this.activeSource.disconnect();
5062
+ }
5063
+ source.connect(this.analyser);
5064
+ this.activeSource = source;
5065
+ }
5066
+ /**
5067
+ * Gets frequency data for visualization bars
5068
+ * Maps lower frequencies to left bars (bass) and higher frequencies to right bars (treble)
5069
+ * Falls back to animated visualization if Web Audio API fails or CORS blocks analysis
5070
+ * Returns flat bars when paused
5071
+ * @returns Array of normalized bar heights (0-1) for each bar
5072
+ */
5073
+ getBarHeights() {
5074
+ if (this.isPaused) {
5075
+ return this.getFlatBarHeights();
5076
+ }
5077
+ if (this.useFallbackMode || !this.analyser || !this.dataArray) {
5078
+ return this.getFallbackBarHeights();
5079
+ }
5080
+ this.analyser.getByteFrequencyData(this.dataArray);
5081
+ const hasNonZeroData = this.dataArray.some((value) => value > 0);
5082
+ if (!hasNonZeroData) {
5083
+ this.zeroDataFrameCount++;
5084
+ if (this.zeroDataFrameCount >= this.maxZeroFramesBeforeFallback) {
5085
+ if (!this.useFallbackMode) {
5086
+ console.warn(
5087
+ "AudioVisualizationManager: Detected CORS blocking audio analysis after",
5088
+ this.zeroDataFrameCount,
5089
+ "frames, switching to fallback animation"
5090
+ );
5091
+ this.useFallbackMode = true;
5092
+ }
5093
+ }
5094
+ return this.getFallbackBarHeights();
5095
+ }
5096
+ this.zeroDataFrameCount = 0;
5097
+ if (this.useFallbackMode && hasNonZeroData) {
5098
+ this.useFallbackMode = false;
5099
+ }
5100
+ const halfBarCount = Math.floor(this.barCount / 2);
5101
+ const barHeights = [];
5102
+ const bufferLength = this.dataArray.length;
5103
+ const samplesPerBar = Math.floor(bufferLength / this.barCount);
5104
+ for (let i = 0; i < halfBarCount; i++) {
5105
+ let sum = 0;
5106
+ const startIndex = i * samplesPerBar;
5107
+ const endIndex = startIndex + samplesPerBar;
5108
+ for (let j = startIndex; j < endIndex && j < bufferLength; j++) {
5109
+ sum += this.dataArray[j];
5110
+ }
5111
+ const average = sum / samplesPerBar;
5112
+ const normalized = average / 255;
5113
+ barHeights.push(normalized);
5114
+ }
5115
+ const reversedBars = [...barHeights].reverse();
5116
+ reversedBars.pop();
5117
+ const mirroredBars = reversedBars.concat(barHeights);
5118
+ return mirroredBars;
5119
+ }
5120
+ /**
5121
+ * Generates animated bar heights as fallback when Web Audio API is unavailable
5122
+ * Creates a wave-like pattern that looks like audio visualization
5123
+ * Mirrors the first 50% to match real audio visualization
5124
+ * @returns Array of normalized bar heights (0-1) for animated effect
5125
+ */
5126
+ getFallbackBarHeights() {
5127
+ const halfBarCount = Math.floor(this.barCount / 2);
5128
+ const barHeights = [];
5129
+ for (let i = 0; i < halfBarCount; i++) {
5130
+ const wave1 = Math.sin(this.fallbackTime * 3e-3 + i * 0.5) * 0.3;
5131
+ const wave2 = Math.sin(this.fallbackTime * 5e-3 + i * 0.3) * 0.2;
5132
+ const wave3 = Math.sin(this.fallbackTime * 2e-3 + i * 0.7) * 0.15;
5133
+ let height = 0.3 + (wave1 + wave2 + wave3);
5134
+ height = Math.max(0, Math.min(1, height));
5135
+ barHeights.push(height);
5136
+ }
5137
+ const reversedBars = [...barHeights].reverse();
5138
+ reversedBars.pop();
5139
+ const mirroredBars = reversedBars.concat(barHeights);
5140
+ return mirroredBars;
5141
+ }
5142
+ /**
5143
+ * Returns flat bar heights for paused state
5144
+ * All bars at minimal height for visual presence
5145
+ * @returns Array of normalized bar heights (0-1) all at minimal value
5146
+ */
5147
+ getFlatBarHeights() {
5148
+ const halfBarCount = Math.floor(this.barCount / 2);
5149
+ const barHeights = [];
5150
+ for (let i = 0; i < halfBarCount; i++) {
5151
+ barHeights.push(0.1);
5152
+ }
5153
+ const reversedBars = [...barHeights].reverse();
5154
+ reversedBars.pop();
5155
+ const mirroredBars = reversedBars.concat(barHeights);
5156
+ return mirroredBars;
5157
+ }
5158
+ /**
5159
+ * Gets the average amplitude across all frequencies
5160
+ * Used for dynamic color intensity
5161
+ * Falls back to animated value if Web Audio API fails
5162
+ * @returns Normalized amplitude value (0-1)
5163
+ */
5164
+ getAverageAmplitude() {
5165
+ if (this.useFallbackMode || !this.analyser || !this.dataArray) {
5166
+ return 0.5 + Math.sin(this.fallbackTime * 2e-3) * 0.3;
5167
+ }
5168
+ this.analyser.getByteFrequencyData(this.dataArray);
5169
+ let sum = 0;
5170
+ for (let i = 0; i < this.dataArray.length; i++) {
5171
+ sum += this.dataArray[i];
5172
+ }
5173
+ const average = sum / this.dataArray.length;
5174
+ const normalized = average / 255;
5175
+ if (normalized === 0) {
5176
+ return 0.5 + Math.sin(this.fallbackTime * 2e-3) * 0.3;
5177
+ }
5178
+ return normalized;
5179
+ }
5180
+ /**
5181
+ * Starts the animation loop for continuous visualization updates
5182
+ * @param callback - Function called on each animation frame with bar heights and amplitude
5183
+ */
5184
+ startVisualization(callback) {
5185
+ if (!this.isInitialized) {
5186
+ console.warn("AudioVisualizationManager: Cannot start visualization - not initialized");
5187
+ return;
5188
+ }
5189
+ if (this.animationId !== null) {
5190
+ this.stopVisualization();
5191
+ }
5192
+ if (this.audioContext && this.audioContext.state === "suspended") {
5193
+ this.audioContext.resume();
5194
+ }
5195
+ this.fallbackTime = 0;
5196
+ this.zeroDataFrameCount = 0;
5197
+ const animate = () => {
5198
+ this.fallbackTime += 16;
5199
+ const barHeights = this.getBarHeights();
5200
+ const amplitude = this.getAverageAmplitude();
5201
+ callback(barHeights, amplitude);
5202
+ this.animationId = requestAnimationFrame(animate);
5203
+ };
5204
+ animate();
5205
+ }
5206
+ /**
5207
+ * Stops the visualization animation loop
5208
+ */
5209
+ stopVisualization() {
5210
+ if (this.animationId !== null) {
5211
+ cancelAnimationFrame(this.animationId);
5212
+ this.animationId = null;
5213
+ }
5214
+ }
5215
+ /**
5216
+ * Pauses the visualization - bars will stay at minimal height
5217
+ * Animation loop continues but returns flat bars
5218
+ */
5219
+ pause() {
5220
+ this.isPaused = true;
5221
+ }
5222
+ /**
5223
+ * Resumes the visualization - bars will react to audio again
5224
+ */
5225
+ resume() {
5226
+ this.isPaused = false;
5227
+ }
5228
+ /**
5229
+ * Checks if visualization is currently initialized
5230
+ */
5231
+ getIsInitialized() {
5232
+ return this.isInitialized;
5233
+ }
5234
+ /**
5235
+ * Resets video element references for playlist restart
5236
+ * Keeps AudioContext and analyser intact but clears video element tracking
5237
+ * This allows new video elements to be assigned when playlist restarts
5238
+ */
5239
+ reset() {
5240
+ this.stopVisualization();
5241
+ if (this.sourceVideo1) {
5242
+ try {
5243
+ this.sourceVideo1.disconnect();
5244
+ } catch (e) {
5245
+ }
5246
+ }
5247
+ if (this.sourceVideo2) {
5248
+ try {
5249
+ this.sourceVideo2.disconnect();
5250
+ } catch (e) {
5251
+ }
5252
+ }
5253
+ this.videoElement1 = null;
5254
+ this.videoElement2 = null;
5255
+ this.sourceVideo1 = null;
5256
+ this.sourceVideo2 = null;
5257
+ this.activeSource = null;
5258
+ this.useFallbackMode = false;
5259
+ this.isPaused = false;
5260
+ this.fallbackTime = 0;
5261
+ this.zeroDataFrameCount = 0;
5262
+ }
5263
+ /**
5264
+ * Cleans up Web Audio resources
5265
+ * Important to call this to prevent memory leaks
5266
+ * WARNING: Only call this when truly destroying - not when switching video elements!
5267
+ */
5268
+ cleanup() {
5269
+ this.stopVisualization();
5270
+ if (this.sourceVideo1) {
5271
+ this.sourceVideo1.disconnect();
5272
+ }
5273
+ if (this.sourceVideo2) {
5274
+ this.sourceVideo2.disconnect();
5275
+ }
5276
+ if (this.analyser) {
5277
+ this.analyser.disconnect();
5278
+ }
5279
+ if (this.audioContext && this.audioContext.state !== "closed") {
5280
+ this.audioContext.close();
5281
+ }
5282
+ this.audioContext = null;
5283
+ this.analyser = null;
5284
+ this.sourceVideo1 = null;
5285
+ this.sourceVideo2 = null;
5286
+ this.activeSource = null;
5287
+ this.videoElement1 = null;
5288
+ this.videoElement2 = null;
5289
+ this.dataArray = null;
5290
+ this.isInitialized = false;
5291
+ this.useFallbackMode = false;
5292
+ this.isPaused = false;
5293
+ this.fallbackTime = 0;
5294
+ this.zeroDataFrameCount = 0;
5295
+ }
5296
+ /**
5297
+ * Destroys the manager and releases all resources
5298
+ */
5299
+ destroy() {
5300
+ this.cleanup();
5301
+ }
5302
+ }
5015
5303
  class VideoManager {
5016
5304
  constructor() {
5017
5305
  // Dual video elements for seamless transitions
@@ -5024,6 +5312,8 @@ class VideoManager {
5024
5312
  __publicField(this, "transcriptManager");
5025
5313
  __publicField(this, "preloadedVideos", /* @__PURE__ */ new Map());
5026
5314
  __publicField(this, "audioFallbackOverlay", null);
5315
+ __publicField(this, "audioVisualizationManager");
5316
+ __publicField(this, "soundbarElement", null);
5027
5317
  // Track the current video URL and position
5028
5318
  __publicField(this, "currentVideoUrl", "");
5029
5319
  __publicField(this, "nextVideoUrl", "");
@@ -5097,6 +5387,7 @@ class VideoManager {
5097
5387
  const deviceInfo = DeviceDetector.getDeviceInfo();
5098
5388
  this.deviceHandler = createDevicePlaybackHandler(deviceInfo);
5099
5389
  this.transcriptManager = new TranscriptManager();
5390
+ this.audioVisualizationManager = new AudioVisualizationManager();
5100
5391
  this.deviceChangeCleanup = DeviceDetector.onDeviceChange((newDeviceInfo) => {
5101
5392
  this.deviceHandler.updateDeviceInfo(newDeviceInfo);
5102
5393
  });
@@ -5125,10 +5416,12 @@ class VideoManager {
5125
5416
  container.appendChild(this.container);
5126
5417
  this.currentVideo = document.createElement("video");
5127
5418
  this.currentVideo.className = "sf-video-container__video sf-video-container__video--current";
5419
+ this.currentVideo.crossOrigin = "anonymous";
5128
5420
  this.deviceHandler.configureVideoElement(this.currentVideo);
5129
5421
  this.container.appendChild(this.currentVideo);
5130
5422
  this.nextVideo = document.createElement("video");
5131
5423
  this.nextVideo.className = "sf-video-container__video sf-video-container__video--next sf-hidden";
5424
+ this.nextVideo.crossOrigin = "anonymous";
5132
5425
  this.deviceHandler.configureVideoElement(this.nextVideo);
5133
5426
  this.container.appendChild(this.nextVideo);
5134
5427
  const store = useSaltfishStore.getState();
@@ -5397,6 +5690,9 @@ class VideoManager {
5397
5690
  if (this.controls) {
5398
5691
  this.controls.startProgressTracking();
5399
5692
  }
5693
+ if (this.audioVisualizationManager.getIsInitialized()) {
5694
+ this.audioVisualizationManager.resume();
5695
+ }
5400
5696
  return;
5401
5697
  }
5402
5698
  this.deviceHandler.handlePlayAttempt(activeVideo, this.hasUserInteracted).then((playSucceeded) => {
@@ -5407,6 +5703,9 @@ class VideoManager {
5407
5703
  if (this.controls) {
5408
5704
  this.controls.startProgressTracking();
5409
5705
  }
5706
+ if (this.audioVisualizationManager.getIsInitialized()) {
5707
+ this.audioVisualizationManager.resume();
5708
+ }
5410
5709
  } else {
5411
5710
  store.setAutoplayFallback();
5412
5711
  if (this.isMobileDevice()) {
@@ -5442,6 +5741,9 @@ class VideoManager {
5442
5741
  this.controls.updateProgressImmediate(activeVideo.currentTime, activeVideo.duration);
5443
5742
  }
5444
5743
  activeVideo.pause();
5744
+ if (this.audioVisualizationManager.getIsInitialized()) {
5745
+ this.audioVisualizationManager.pause();
5746
+ }
5445
5747
  if (this.controls) {
5446
5748
  this.controls.stopProgressTracking();
5447
5749
  }
@@ -5508,13 +5810,21 @@ class VideoManager {
5508
5810
  this.videoEndedCallback = null;
5509
5811
  this.transcriptManager.reset();
5510
5812
  this.hideAudioFallbackOverlay();
5813
+ const isWebAudioActive = this.audioVisualizationManager.getIsInitialized();
5814
+ if (isWebAudioActive) {
5815
+ this.audioVisualizationManager.reset();
5816
+ }
5511
5817
  if (this.currentVideo) {
5512
- this.currentVideo.src = "";
5818
+ if (!isWebAudioActive) {
5819
+ this.currentVideo.src = "";
5820
+ }
5513
5821
  this.currentVideo.currentTime = 0;
5514
5822
  this.currentVideo.pause();
5515
5823
  }
5516
5824
  if (this.nextVideo) {
5517
- this.nextVideo.src = "";
5825
+ if (!isWebAudioActive) {
5826
+ this.nextVideo.src = "";
5827
+ }
5518
5828
  this.nextVideo.currentTime = 0;
5519
5829
  this.nextVideo.pause();
5520
5830
  }
@@ -5701,8 +6011,24 @@ class VideoManager {
5701
6011
  loadTranscript(transcript, initiallyVisible = true) {
5702
6012
  this.transcriptManager.loadTranscript(transcript, initiallyVisible);
5703
6013
  }
6014
+ /**
6015
+ * Creates a soundbar visualization element with 11 frequency bars (mirrored with single center)
6016
+ * @returns HTMLElement containing the soundbar visualization
6017
+ */
6018
+ createSoundbar() {
6019
+ const soundbar = document.createElement("div");
6020
+ soundbar.className = "sf-audio-soundbar";
6021
+ for (let i = 0; i < 11; i++) {
6022
+ const bar = document.createElement("div");
6023
+ bar.className = "sf-audio-soundbar__bar";
6024
+ bar.dataset.barIndex = i.toString();
6025
+ soundbar.appendChild(bar);
6026
+ }
6027
+ return soundbar;
6028
+ }
5704
6029
  /**
5705
6030
  * Shows audio fallback overlay with optional poster image or avatar thumbnail
6031
+ * Includes real-time audio visualization soundbar
5706
6032
  * @param posterUrl - Optional URL for poster image (e.g., GIF)
5707
6033
  * @param avatarThumbnailUrl - Optional URL for avatar thumbnail to display instead of icon/text
5708
6034
  */
@@ -5728,36 +6054,79 @@ class VideoManager {
5728
6054
  this.audioFallbackOverlay.appendChild(avatar);
5729
6055
  this.audioFallbackOverlay.appendChild(overlay);
5730
6056
  } else {
5731
- this.audioFallbackOverlay.className = "sf-audio-fallback-overlay";
5732
- const icon = document.createElement("div");
5733
- icon.className = "sf-audio-fallback-overlay__icon";
5734
- icon.innerHTML = `
5735
- <svg viewBox="0 0 24 24" fill="currentColor">
5736
- <path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
5737
- </svg>
5738
- `;
5739
- const text = document.createElement("div");
5740
- text.className = "sf-audio-fallback-overlay__text";
5741
- text.textContent = "Video is being generated";
5742
- this.audioFallbackOverlay.appendChild(icon);
5743
- this.audioFallbackOverlay.appendChild(text);
6057
+ this.audioFallbackOverlay.className = "sf-audio-fallback-overlay sf-audio-fallback-overlay--soundbar-only";
5744
6058
  }
6059
+ this.soundbarElement = this.createSoundbar();
6060
+ this.audioFallbackOverlay.appendChild(this.soundbarElement);
5745
6061
  this.container.appendChild(this.audioFallbackOverlay);
5746
6062
  }
5747
6063
  }
6064
+ /**
6065
+ * Starts audio visualization for the active video element
6066
+ * Should be called AFTER video is loaded and ready to play
6067
+ */
6068
+ startAudioVisualization() {
6069
+ if (!this.soundbarElement) {
6070
+ return;
6071
+ }
6072
+ const activeVideo = this.getActiveVideo();
6073
+ if (!activeVideo) {
6074
+ return;
6075
+ }
6076
+ try {
6077
+ this.audioVisualizationManager.initialize(activeVideo);
6078
+ this.audioVisualizationManager.startVisualization((barHeights, _amplitude) => {
6079
+ if (!this.soundbarElement) return;
6080
+ const bars = this.soundbarElement.querySelectorAll(".sf-audio-soundbar__bar");
6081
+ bars.forEach((bar, index) => {
6082
+ const height = barHeights[index] || 0;
6083
+ const htmlBar = bar;
6084
+ const heightPercent = Math.max(10, height * 100);
6085
+ htmlBar.style.height = `${heightPercent}%`;
6086
+ const center = 5;
6087
+ const distanceFromCenter = Math.abs(index - center);
6088
+ const maxDistance = 5;
6089
+ const hue = 180 + distanceFromCenter / maxDistance * 120;
6090
+ const opacity = 1;
6091
+ htmlBar.style.backgroundColor = `hsla(${hue}, 70%, 60%, ${opacity})`;
6092
+ });
6093
+ });
6094
+ } catch (error2) {
6095
+ console.error("VideoManager: Failed to initialize audio visualization", error2);
6096
+ }
6097
+ }
6098
+ /**
6099
+ * Initializes/switches Web Audio source for regular video nodes
6100
+ * Does NOT start visualization - only ensures audio routing
6101
+ * Must be called after video loads for proper audio routing once Web Audio is initialized
6102
+ */
6103
+ initializeAudioForVideo() {
6104
+ const activeVideo = this.getActiveVideo();
6105
+ if (!activeVideo) {
6106
+ return;
6107
+ }
6108
+ try {
6109
+ this.audioVisualizationManager.initialize(activeVideo);
6110
+ } catch (error2) {
6111
+ console.error("VideoManager: Failed to initialize audio for video", error2);
6112
+ }
6113
+ }
5748
6114
  /**
5749
6115
  * Hides audio fallback overlay and removes poster image
6116
+ * Note: Does not cleanup Web Audio connections - they must persist for audio playback
5750
6117
  */
5751
6118
  hideAudioFallbackOverlay() {
5752
6119
  if (!this.container) {
5753
6120
  return;
5754
6121
  }
6122
+ this.audioVisualizationManager.stopVisualization();
5755
6123
  this.container.style.removeProperty("--sf-audio-poster-url");
5756
6124
  this.container.classList.remove("sf-video-container--audio-fallback");
5757
6125
  if (this.audioFallbackOverlay && this.audioFallbackOverlay.parentNode) {
5758
6126
  this.audioFallbackOverlay.parentNode.removeChild(this.audioFallbackOverlay);
5759
6127
  this.audioFallbackOverlay = null;
5760
6128
  }
6129
+ this.soundbarElement = null;
5761
6130
  }
5762
6131
  }
5763
6132
  class CursorManager {
@@ -10828,7 +11197,7 @@ const SaltfishPlayer$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de
10828
11197
  __proto__: null,
10829
11198
  SaltfishPlayer
10830
11199
  }, Symbol.toStringTag, { value: "Module" }));
10831
- const version = "0.2.77";
11200
+ const version = "0.2.79";
10832
11201
  const packageJson = {
10833
11202
  version
10834
11203
  };