UNPKG

saltfish

Version:

An interactive video-guided tour system for web applications

12,791 lines 522 kB
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
const __vite_import_meta_env__ = {};
const createStoreImpl = (createState) => {
  let state;
  const listeners = /* @__PURE__ */ new Set();
  const setState = (partial, replace) => {
    const nextState = typeof partial === "function" ? partial(state) : partial;
    if (!Object.is(nextState, state)) {
      const previousState = state;
      state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
      listeners.forEach((listener) => listener(state, previousState));
    }
  };
  const getState = () => state;
  const getInitialState = () => initialState;
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  const destroy = () => {
    if ((__vite_import_meta_env__ ? "production" : void 0) !== "production") {
      console.warn(
        "[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."
      );
    }
    listeners.clear();
  };
  const api = { setState, getState, getInitialState, subscribe, destroy };
  const initialState = state = createState(setState, getState, api);
  return api;
};
const createStore = (createState) => createStoreImpl;
var NOTHING = Symbol.for("immer-nothing");
var DRAFTABLE = Symbol.for("immer-draftable");
var DRAFT_STATE = Symbol.for("immer-state");
function die(error2, ...args) {
  throw new Error(
    `[Immer] minified error nr: ${error2}. Full error at: https://bit.ly/3cXEKWf`
  );
}
var getPrototypeOf = Object.getPrototypeOf;
function isDraft(value) {
  return !!value && !!value[DRAFT_STATE];
}
function isDraftable(value) {
  var _a;
  if (!value)
    return false;
  return isPlainObject(value) || Array.isArray(value) || !!value[DRAFTABLE] || !!((_a = value.constructor) == null ? void 0 : _a[DRAFTABLE]) || isMap(value) || isSet(value);
}
var objectCtorString = Object.prototype.constructor.toString();
function isPlainObject(value) {
  if (!value || typeof value !== "object")
    return false;
  const proto = getPrototypeOf(value);
  if (proto === null) {
    return true;
  }
  const Ctor = Object.hasOwnProperty.call(proto, "constructor") && proto.constructor;
  if (Ctor === Object)
    return true;
  return typeof Ctor == "function" && Function.toString.call(Ctor) === objectCtorString;
}
function each(obj, iter) {
  if (getArchtype(obj) === 0) {
    Reflect.ownKeys(obj).forEach((key) => {
      iter(key, obj[key], obj);
    });
  } else {
    obj.forEach((entry, index) => iter(index, entry, obj));
  }
}
function getArchtype(thing) {
  const state = thing[DRAFT_STATE];
  return state ? state.type_ : Array.isArray(thing) ? 1 : isMap(thing) ? 2 : isSet(thing) ? 3 : 0;
}
function has(thing, prop) {
  return getArchtype(thing) === 2 ? thing.has(prop) : Object.prototype.hasOwnProperty.call(thing, prop);
}
function set(thing, propOrOldValue, value) {
  const t = getArchtype(thing);
  if (t === 2)
    thing.set(propOrOldValue, value);
  else if (t === 3) {
    thing.add(value);
  } else
    thing[propOrOldValue] = value;
}
function is(x, y) {
  if (x === y) {
    return x !== 0 || 1 / x === 1 / y;
  } else {
    return x !== x && y !== y;
  }
}
function isMap(target) {
  return target instanceof Map;
}
function isSet(target) {
  return target instanceof Set;
}
function latest(state) {
  return state.copy_ || state.base_;
}
function shallowCopy(base, strict) {
  if (isMap(base)) {
    return new Map(base);
  }
  if (isSet(base)) {
    return new Set(base);
  }
  if (Array.isArray(base))
    return Array.prototype.slice.call(base);
  const isPlain = isPlainObject(base);
  if (strict === true || strict === "class_only" && !isPlain) {
    const descriptors = Object.getOwnPropertyDescriptors(base);
    delete descriptors[DRAFT_STATE];
    let keys = Reflect.ownKeys(descriptors);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const desc = descriptors[key];
      if (desc.writable === false) {
        desc.writable = true;
        desc.configurable = true;
      }
      if (desc.get || desc.set)
        descriptors[key] = {
          configurable: true,
          writable: true,
          // could live with !!desc.set as well here...
          enumerable: desc.enumerable,
          value: base[key]
        };
    }
    return Object.create(getPrototypeOf(base), descriptors);
  } else {
    const proto = getPrototypeOf(base);
    if (proto !== null && isPlain) {
      return { ...base };
    }
    const obj = Object.create(proto);
    return Object.assign(obj, base);
  }
}
function freeze(obj, deep = false) {
  if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj))
    return obj;
  if (getArchtype(obj) > 1) {
    obj.set = obj.add = obj.clear = obj.delete = dontMutateFrozenCollections;
  }
  Object.freeze(obj);
  if (deep)
    Object.entries(obj).forEach(([key, value]) => freeze(value, true));
  return obj;
}
function dontMutateFrozenCollections() {
  die(2);
}
function isFrozen(obj) {
  return Object.isFrozen(obj);
}
var plugins = {};
function getPlugin(pluginKey) {
  const plugin = plugins[pluginKey];
  if (!plugin) {
    die(0, pluginKey);
  }
  return plugin;
}
var currentScope;
function getCurrentScope() {
  return currentScope;
}
function createScope(parent_, immer_) {
  return {
    drafts_: [],
    parent_,
    immer_,
    // Whenever the modified draft contains a draft from another scope, we
    // need to prevent auto-freezing so the unowned draft can be finalized.
    canAutoFreeze_: true,
    unfinalizedDrafts_: 0
  };
}
function usePatchesInScope(scope, patchListener) {
  if (patchListener) {
    getPlugin("Patches");
    scope.patches_ = [];
    scope.inversePatches_ = [];
    scope.patchListener_ = patchListener;
  }
}
function revokeScope(scope) {
  leaveScope(scope);
  scope.drafts_.forEach(revokeDraft);
  scope.drafts_ = null;
}
function leaveScope(scope) {
  if (scope === currentScope) {
    currentScope = scope.parent_;
  }
}
function enterScope(immer2) {
  return currentScope = createScope(currentScope, immer2);
}
function revokeDraft(draft) {
  const state = draft[DRAFT_STATE];
  if (state.type_ === 0 || state.type_ === 1)
    state.revoke_();
  else
    state.revoked_ = true;
}
function processResult(result, scope) {
  scope.unfinalizedDrafts_ = scope.drafts_.length;
  const baseDraft = scope.drafts_[0];
  const isReplaced = result !== void 0 && result !== baseDraft;
  if (isReplaced) {
    if (baseDraft[DRAFT_STATE].modified_) {
      revokeScope(scope);
      die(4);
    }
    if (isDraftable(result)) {
      result = finalize(scope, result);
      if (!scope.parent_)
        maybeFreeze(scope, result);
    }
    if (scope.patches_) {
      getPlugin("Patches").generateReplacementPatches_(
        baseDraft[DRAFT_STATE].base_,
        result,
        scope.patches_,
        scope.inversePatches_
      );
    }
  } else {
    result = finalize(scope, baseDraft, []);
  }
  revokeScope(scope);
  if (scope.patches_) {
    scope.patchListener_(scope.patches_, scope.inversePatches_);
  }
  return result !== NOTHING ? result : void 0;
}
function finalize(rootScope, value, path) {
  if (isFrozen(value))
    return value;
  const state = value[DRAFT_STATE];
  if (!state) {
    each(
      value,
      (key, childValue) => finalizeProperty(rootScope, state, value, key, childValue, path)
    );
    return value;
  }
  if (state.scope_ !== rootScope)
    return value;
  if (!state.modified_) {
    maybeFreeze(rootScope, state.base_, true);
    return state.base_;
  }
  if (!state.finalized_) {
    state.finalized_ = true;
    state.scope_.unfinalizedDrafts_--;
    const result = state.copy_;
    let resultEach = result;
    let isSet2 = false;
    if (state.type_ === 3) {
      resultEach = new Set(result);
      result.clear();
      isSet2 = true;
    }
    each(
      resultEach,
      (key, childValue) => finalizeProperty(rootScope, state, result, key, childValue, path, isSet2)
    );
    maybeFreeze(rootScope, result, false);
    if (path && rootScope.patches_) {
      getPlugin("Patches").generatePatches_(
        state,
        path,
        rootScope.patches_,
        rootScope.inversePatches_
      );
    }
  }
  return state.copy_;
}
function finalizeProperty(rootScope, parentState, targetObject, prop, childValue, rootPath, targetIsSet) {
  if (isDraft(childValue)) {
    const path = rootPath && parentState && parentState.type_ !== 3 && // Set objects are atomic since they have no keys.
    !has(parentState.assigned_, prop) ? rootPath.concat(prop) : void 0;
    const res = finalize(rootScope, childValue, path);
    set(targetObject, prop, res);
    if (isDraft(res)) {
      rootScope.canAutoFreeze_ = false;
    } else
      return;
  } else if (targetIsSet) {
    targetObject.add(childValue);
  }
  if (isDraftable(childValue) && !isFrozen(childValue)) {
    if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) {
      return;
    }
    finalize(rootScope, childValue);
    if ((!parentState || !parentState.scope_.parent_) && typeof prop !== "symbol" && Object.prototype.propertyIsEnumerable.call(targetObject, prop))
      maybeFreeze(rootScope, childValue);
  }
}
function maybeFreeze(scope, value, deep = false) {
  if (!scope.parent_ && scope.immer_.autoFreeze_ && scope.canAutoFreeze_) {
    freeze(value, deep);
  }
}
function createProxyProxy(base, parent) {
  const isArray = Array.isArray(base);
  const state = {
    type_: isArray ? 1 : 0,
    // Track which produce call this is associated with.
    scope_: parent ? parent.scope_ : getCurrentScope(),
    // True for both shallow and deep changes.
    modified_: false,
    // Used during finalization.
    finalized_: false,
    // Track which properties have been assigned (true) or deleted (false).
    assigned_: {},
    // The parent draft state.
    parent_: parent,
    // The base state.
    base_: base,
    // The base proxy.
    draft_: null,
    // set below
    // The base copy with any updated values.
    copy_: null,
    // Called by the `produce` function.
    revoke_: null,
    isManual_: false
  };
  let target = state;
  let traps = objectTraps;
  if (isArray) {
    target = [state];
    traps = arrayTraps;
  }
  const { revoke, proxy } = Proxy.revocable(target, traps);
  state.draft_ = proxy;
  state.revoke_ = revoke;
  return proxy;
}
var objectTraps = {
  get(state, prop) {
    if (prop === DRAFT_STATE)
      return state;
    const source = latest(state);
    if (!has(source, prop)) {
      return readPropFromProto(state, source, prop);
    }
    const value = source[prop];
    if (state.finalized_ || !isDraftable(value)) {
      return value;
    }
    if (value === peek(state.base_, prop)) {
      prepareCopy(state);
      return state.copy_[prop] = createProxy(value, state);
    }
    return value;
  },
  has(state, prop) {
    return prop in latest(state);
  },
  ownKeys(state) {
    return Reflect.ownKeys(latest(state));
  },
  set(state, prop, value) {
    const desc = getDescriptorFromProto(latest(state), prop);
    if (desc == null ? void 0 : desc.set) {
      desc.set.call(state.draft_, value);
      return true;
    }
    if (!state.modified_) {
      const current2 = peek(latest(state), prop);
      const currentState = current2 == null ? void 0 : current2[DRAFT_STATE];
      if (currentState && currentState.base_ === value) {
        state.copy_[prop] = value;
        state.assigned_[prop] = false;
        return true;
      }
      if (is(value, current2) && (value !== void 0 || has(state.base_, prop)))
        return true;
      prepareCopy(state);
      markChanged(state);
    }
    if (state.copy_[prop] === value && // special case: handle new props with value 'undefined'
    (value !== void 0 || prop in state.copy_) || // special case: NaN
    Number.isNaN(value) && Number.isNaN(state.copy_[prop]))
      return true;
    state.copy_[prop] = value;
    state.assigned_[prop] = true;
    return true;
  },
  deleteProperty(state, prop) {
    if (peek(state.base_, prop) !== void 0 || prop in state.base_) {
      state.assigned_[prop] = false;
      prepareCopy(state);
      markChanged(state);
    } else {
      delete state.assigned_[prop];
    }
    if (state.copy_) {
      delete state.copy_[prop];
    }
    return true;
  },
  // Note: We never coerce `desc.value` into an Immer draft, because we can't make
  // the same guarantee in ES5 mode.
  getOwnPropertyDescriptor(state, prop) {
    const owner = latest(state);
    const desc = Reflect.getOwnPropertyDescriptor(owner, prop);
    if (!desc)
      return desc;
    return {
      writable: true,
      configurable: state.type_ !== 1 || prop !== "length",
      enumerable: desc.enumerable,
      value: owner[prop]
    };
  },
  defineProperty() {
    die(11);
  },
  getPrototypeOf(state) {
    return getPrototypeOf(state.base_);
  },
  setPrototypeOf() {
    die(12);
  }
};
var arrayTraps = {};
each(objectTraps, (key, fn) => {
  arrayTraps[key] = function() {
    arguments[0] = arguments[0][0];
    return fn.apply(this, arguments);
  };
});
arrayTraps.deleteProperty = function(state, prop) {
  return arrayTraps.set.call(this, state, prop, void 0);
};
arrayTraps.set = function(state, prop, value) {
  return objectTraps.set.call(this, state[0], prop, value, state[0]);
};
function peek(draft, prop) {
  const state = draft[DRAFT_STATE];
  const source = state ? latest(state) : draft;
  return source[prop];
}
function readPropFromProto(state, source, prop) {
  var _a;
  const desc = getDescriptorFromProto(source, prop);
  return desc ? `value` in desc ? desc.value : (
    // This is a very special case, if the prop is a getter defined by the
    // prototype, we should invoke it with the draft as context!
    (_a = desc.get) == null ? void 0 : _a.call(state.draft_)
  ) : void 0;
}
function getDescriptorFromProto(source, prop) {
  if (!(prop in source))
    return void 0;
  let proto = getPrototypeOf(source);
  while (proto) {
    const desc = Object.getOwnPropertyDescriptor(proto, prop);
    if (desc)
      return desc;
    proto = getPrototypeOf(proto);
  }
  return void 0;
}
function markChanged(state) {
  if (!state.modified_) {
    state.modified_ = true;
    if (state.parent_) {
      markChanged(state.parent_);
    }
  }
}
function prepareCopy(state) {
  if (!state.copy_) {
    state.copy_ = shallowCopy(
      state.base_,
      state.scope_.immer_.useStrictShallowCopy_
    );
  }
}
var Immer2 = class {
  constructor(config) {
    this.autoFreeze_ = true;
    this.useStrictShallowCopy_ = false;
    this.produce = (base, recipe, patchListener) => {
      if (typeof base === "function" && typeof recipe !== "function") {
        const defaultBase = recipe;
        recipe = base;
        const self = this;
        return function curriedProduce(base2 = defaultBase, ...args) {
          return self.produce(base2, (draft) => recipe.call(this, draft, ...args));
        };
      }
      if (typeof recipe !== "function")
        die(6);
      if (patchListener !== void 0 && typeof patchListener !== "function")
        die(7);
      let result;
      if (isDraftable(base)) {
        const scope = enterScope(this);
        const proxy = createProxy(base, void 0);
        let hasError = true;
        try {
          result = recipe(proxy);
          hasError = false;
        } finally {
          if (hasError)
            revokeScope(scope);
          else
            leaveScope(scope);
        }
        usePatchesInScope(scope, patchListener);
        return processResult(result, scope);
      } else if (!base || typeof base !== "object") {
        result = recipe(base);
        if (result === void 0)
          result = base;
        if (result === NOTHING)
          result = void 0;
        if (this.autoFreeze_)
          freeze(result, true);
        if (patchListener) {
          const p = [];
          const ip = [];
          getPlugin("Patches").generateReplacementPatches_(base, result, p, ip);
          patchListener(p, ip);
        }
        return result;
      } else
        die(1, base);
    };
    this.produceWithPatches = (base, recipe) => {
      if (typeof base === "function") {
        return (state, ...args) => this.produceWithPatches(state, (draft) => base(draft, ...args));
      }
      let patches, inversePatches;
      const result = this.produce(base, recipe, (p, ip) => {
        patches = p;
        inversePatches = ip;
      });
      return [result, patches, inversePatches];
    };
    if (typeof (config == null ? void 0 : config.autoFreeze) === "boolean")
      this.setAutoFreeze(config.autoFreeze);
    if (typeof (config == null ? void 0 : config.useStrictShallowCopy) === "boolean")
      this.setUseStrictShallowCopy(config.useStrictShallowCopy);
  }
  createDraft(base) {
    if (!isDraftable(base))
      die(8);
    if (isDraft(base))
      base = current(base);
    const scope = enterScope(this);
    const proxy = createProxy(base, void 0);
    proxy[DRAFT_STATE].isManual_ = true;
    leaveScope(scope);
    return proxy;
  }
  finishDraft(draft, patchListener) {
    const state = draft && draft[DRAFT_STATE];
    if (!state || !state.isManual_)
      die(9);
    const { scope_: scope } = state;
    usePatchesInScope(scope, patchListener);
    return processResult(void 0, scope);
  }
  /**
   * Pass true to automatically freeze all copies created by Immer.
   *
   * By default, auto-freezing is enabled.
   */
  setAutoFreeze(value) {
    this.autoFreeze_ = value;
  }
  /**
   * Pass true to enable strict shallow copy.
   *
   * By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
   */
  setUseStrictShallowCopy(value) {
    this.useStrictShallowCopy_ = value;
  }
  applyPatches(base, patches) {
    let i;
    for (i = patches.length - 1; i >= 0; i--) {
      const patch = patches[i];
      if (patch.path.length === 0 && patch.op === "replace") {
        base = patch.value;
        break;
      }
    }
    if (i > -1) {
      patches = patches.slice(i + 1);
    }
    const applyPatchesImpl = getPlugin("Patches").applyPatches_;
    if (isDraft(base)) {
      return applyPatchesImpl(base, patches);
    }
    return this.produce(
      base,
      (draft) => applyPatchesImpl(draft, patches)
    );
  }
};
function createProxy(value, parent) {
  const draft = isMap(value) ? getPlugin("MapSet").proxyMap_(value, parent) : isSet(value) ? getPlugin("MapSet").proxySet_(value, parent) : createProxyProxy(value, parent);
  const scope = parent ? parent.scope_ : getCurrentScope();
  scope.drafts_.push(draft);
  return draft;
}
function current(value) {
  if (!isDraft(value))
    die(10, value);
  return currentImpl(value);
}
function currentImpl(value) {
  if (!isDraftable(value) || isFrozen(value))
    return value;
  const state = value[DRAFT_STATE];
  let copy;
  if (state) {
    if (!state.modified_)
      return state.base_;
    state.finalized_ = true;
    copy = shallowCopy(value, state.scope_.immer_.useStrictShallowCopy_);
  } else {
    copy = shallowCopy(value, true);
  }
  each(copy, (key, childValue) => {
    set(copy, key, currentImpl(childValue));
  });
  if (state) {
    state.finalized_ = false;
  }
  return copy;
}
var immer$1 = new Immer2();
var produce = immer$1.produce;
immer$1.produceWithPatches.bind(
  immer$1
);
immer$1.setAutoFreeze.bind(immer$1);
immer$1.setUseStrictShallowCopy.bind(immer$1);
immer$1.applyPatches.bind(immer$1);
immer$1.createDraft.bind(immer$1);
immer$1.finishDraft.bind(immer$1);
const immerImpl = (initializer) => (set2, get, store) => {
  store.setState = (updater, replace, ...a) => {
    const nextState = typeof updater === "function" ? produce(updater) : updater;
    return set2(nextState, replace, ...a);
  };
  return initializer(store.setState, get, store);
};
const immer = immerImpl;
function log(message, data) {
}
function info(message, data) {
  if (data !== void 0) {
    console.info(message, data);
  } else {
    console.info(message);
  }
}
function warn(message, data) {
  {
    console.warn(message);
  }
}
function error(message, data) {
  if (data !== void 0) {
    console.error(message, data);
  } else {
    console.error(message);
  }
}
function debug(message, data) {
}
class PlayerStateMachine {
  constructor(config, initialContext) {
    __publicField(this, "currentState");
    __publicField(this, "config");
    __publicField(this, "context");
    __publicField(this, "actionHandlers", {});
    this.config = config;
    this.currentState = config.initial;
    this.context = initialContext;
    this.setupDefaultActions();
    this.runEntryActions(this.currentState);
  }
  /**
   * Set up default action handlers for common operations
   */
  setupDefaultActions() {
    this.actionHandlers = {
      ...this.actionHandlers,
      logStateEntry: (_context) => {
        log(`State Machine: Entered ${this.currentState} state`);
      },
      logErrorEvent: (_context, event) => {
        if ((event == null ? void 0 : event.type) === "ERROR") {
          log(`State Machine: ERROR event received with message: ${event.error.message}`);
        }
      },
      logStepTransition: (_context, event) => {
        if ((event == null ? void 0 : event.type) === "TRANSITION_TO_STEP") {
          log(`State Machine: Transitioning to new step: ${event.step.id}`);
        }
      },
      logErrorRecovery: () => {
      }
    };
  }
  /**
   * Register custom action handlers
   * @param actions - Object mapping action names to handler functions
   */
  registerActions(actions) {
    this.actionHandlers = { ...this.actionHandlers, ...actions };
    log(`PlayerStateMachine: Registered action handlers: ${Object.keys(actions).join(", ")}`);
  }
  /**
   * Execute an action (either named or inline function)
   * @param action - The action to execute
   * @param event - Optional event that triggered the action
   */
  executeAction(action, event) {
    if (typeof action === "string") {
      const handler = this.actionHandlers[action];
      if (handler) {
        handler(this.context, event);
      }
    } else {
      action(this.context, event);
    }
  }
  /**
   * Send an event to the state machine to trigger a transition
   * @param event - The event to send
   * @returns The new state after the transition
   */
  send(event) {
    const stateConfig = this.config.states[this.currentState];
    const transition = stateConfig.on[event.type];
    if (!transition) {
      log(`No transition defined for event ${event.type} in state ${this.currentState}`);
      return this.currentState;
    }
    log(`Processing transition: ${this.currentState} -> ${transition.target} via ${event.type}`);
    this.runExitActions(this.currentState);
    this.updateContextFromEvent(event);
    if (transition.actions) {
      transition.actions.forEach((action) => this.executeAction(action, event));
    }
    const prevState = this.currentState;
    this.currentState = transition.target;
    this.runEntryActions(this.currentState);
    log(`State transition complete: ${prevState} -> ${this.currentState}`);
    return this.currentState;
  }
  /**
   * Update context based on the event
   * @param event - The event to process
   */
  updateContextFromEvent(event) {
    if (event.type === "TRANSITION_TO_STEP" || event.type === "MANIFEST_LOADED" || event.type === "VIDEO_FINISHED_WAIT") {
      this.context.currentStep = event.step;
    } else if (event.type === "ERROR") {
      this.context.error = event.error;
    }
  }
  /**
   * Get the current state of the state machine
   * @returns The current state
   */
  getState() {
    return this.currentState;
  }
  /**
   * Get the current context of the state machine
   * @returns The current context
   */
  getContext() {
    return this.context;
  }
  /**
   * Update the context directly (use with caution)
   * @param updater Function that takes the current context and returns an updated one
   */
  updateContext(updater) {
    const updates = updater(this.context);
    this.context = { ...this.context, ...updates };
  }
  /**
   * Run entry actions for a state
   * @param state - The state to run entry actions for
   */
  runEntryActions(state) {
    const stateConfig = this.config.states[state];
    if (stateConfig.entry) {
      stateConfig.entry.forEach((action) => this.executeAction(action));
    }
  }
  /**
   * Run exit actions for a state
   * @param state - The state to run exit actions for
   */
  runExitActions(state) {
    const stateConfig = this.config.states[state];
    if (stateConfig.exit) {
      stateConfig.exit.forEach((action) => this.executeAction(action));
    }
  }
}
const playerStateMachineConfig = {
  initial: "idle",
  states: {
    "idle": {
      on: {
        "INITIALIZE": { target: "idle" },
        "LOAD_MANIFEST": { target: "loading" }
      },
      entry: ["logStateEntry"]
    },
    "loading": {
      on: {
        "MANIFEST_LOADED": { target: "paused" },
        "ERROR": {
          target: "error",
          actions: ["logErrorEvent"]
        }
      },
      entry: ["logStateEntry", "showLoadingState"],
      exit: ["hideLoadingState"]
    },
    "playing": {
      on: {
        "PAUSE": { target: "paused" },
        "MINIMIZE": { target: "minimized" },
        "VIDEO_FINISHED_WAIT": { target: "waitingForInteraction" },
        "AUTOPLAY_FALLBACK": { target: "autoplayBlocked" },
        "TRANSITION_TO_STEP": {
          target: "playing",
          actions: ["logStepTransition"]
        },
        "ERROR": { target: "error" },
        "COMPLETE_PLAYLIST": { target: "completed" },
        "COMPLETE_PLAYLIST_WAITING_FOR_INTERACTION": { target: "completedWaitingForInteraction" }
      },
      entry: ["logStateEntry", "startVideoPlayback", "showVideoControls", "hidePlayButton"],
      exit: ["pauseVideoPlayback"]
    },
    "paused": {
      on: {
        "PLAY": { target: "playing" },
        "START_IDLE_MODE": { target: "idleMode" },
        "MINIMIZE": { target: "minimized" },
        "TRANSITION_TO_STEP": {
          target: "playing",
          actions: ["logStepTransition"]
        }
      },
      entry: ["logStateEntry", "pauseVideoPlayback", "showPlayButton"]
    },
    "minimized": {
      on: {
        "MAXIMIZE": { target: "playing" },
        "EXIT": { target: "closing" }
      },
      entry: ["logStateEntry", "pauseVideoPlayback"]
    },
    "waitingForInteraction": {
      on: {
        "PLAY": { target: "playing" },
        "PAUSE": { target: "paused" },
        "MINIMIZE": { target: "minimized" },
        "COMPLETE_PLAYLIST": { target: "completed" },
        "COMPLETE_PLAYLIST_WAITING_FOR_INTERACTION": { target: "completedWaitingForInteraction" },
        "TRANSITION_TO_STEP": {
          target: "playing",
          actions: ["logStepTransition"]
        }
      },
      entry: ["logStateEntry", "showPlayButton"]
    },
    "autoplayBlocked": {
      on: {
        "PLAY": { target: "playing" },
        "TRANSITION_TO_STEP": { target: "playing" },
        "MINIMIZE": { target: "minimized" }
      },
      entry: ["logStateEntry", "enterCompactMode", "startMutedLoopedVideo", "hideVideoControls", "showPlayButton", "enablePlayButtonProminent"],
      exit: ["disablePlayButtonProminent", "showVideoControls", "exitCompactMode"]
    },
    "idleMode": {
      on: {
        "PLAY": { target: "playing" },
        "TRANSITION_TO_STEP": { target: "playing" },
        "MINIMIZE": { target: "minimized" }
      },
      entry: ["logStateEntry", "startIdleModeVideo", "hideVideoControls", "showPlayButton", "enablePlayButtonProminent"],
      exit: ["disablePlayButtonProminent", "showVideoControls", "exitCompactMode"]
    },
    "error": {
      on: {
        "INITIALIZE": { target: "idle" },
        "PLAY": {
          target: "playing",
          actions: ["logErrorRecovery"]
        },
        "AUTOPLAY_FALLBACK": { target: "autoplayBlocked" }
      },
      entry: ["logStateEntry", "handleError", "showPlayButton"],
      exit: ["hideError"]
    },
    "completedWaitingForInteraction": {
      on: {
        "COMPLETE_PLAYLIST": { target: "completed" },
        "INITIALIZE": { target: "idle" },
        "TRANSITION_TO_STEP": {
          target: "playing",
          actions: ["logStepTransition"]
        },
        "PLAY": { target: "playing" },
        "MINIMIZE": { target: "minimized" }
      },
      entry: ["logStateEntry", "showPlayButton"]
    },
    "completed": {
      on: {
        "INITIALIZE": { target: "idle" }
      },
      entry: ["logStateEntry", "trackPlaylistComplete"]
    },
    "closing": {
      on: {},
      entry: ["logStateEntry", "triggerPlaylistDismissed"]
    }
  }
};
const STORAGE_KEYS = {
  PROGRESS: "saltfish_progress",
  SESSION: "saltfish_session",
  ANONYMOUS_USER: "saltfish_anonymous_user_data",
  PENDING_NAVIGATION: "saltfish_pending_navigation"
};
const API = {
  BASE_URL: "https://player.saltfish.ai",
  SHARE_BASE_URL: "https://studio-api.saltfish.ai/studio/flows2/share",
  ENDPOINTS: {
    VALIDATE_TOKEN: "/validate-token",
    USERS: "/clients/{token}/users/{userId}"
  }
};
const CSS_CLASSES = {
  PLAYER: "sf-player",
  PLAYER_MINIMIZED: "sf-player--minimized",
  CONTROLS_CONTAINER: "sf-controls-container",
  LOGO: "sf-player__logo",
  // Legacy error class
  ERROR_DISPLAY: "sf-error-display",
  ERROR_DISPLAY_VISIBLE: "sf-error-display--visible",
  ERROR_DISPLAY_CONTENT: "sf-error-display__content",
  ERROR_DISPLAY_MESSAGE: "sf-error-display__message",
  LOADING_SPINNER: "sf-loading-spinner"
};
const _StorageManager = class _StorageManager {
  constructor() {
    __publicField(this, "isLocalStorageAvailable");
    this.isLocalStorageAvailable = this.checkLocalStorageAvailability();
    if (!this.isLocalStorageAvailable) ;
  }
  /**
   * Get the singleton instance of StorageManager
   * @returns The StorageManager instance
   */
  static getInstance() {
    if (!_StorageManager.instance) {
      _StorageManager.instance = new _StorageManager();
    }
    return _StorageManager.instance;
  }
  /**
   * Reset the singleton instance (useful for testing)
   * @internal
   */
  static resetInstance() {
    _StorageManager.instance = null;
  }
  /**
   * Check if localStorage is available and working
   */
  checkLocalStorageAvailability() {
    if (typeof window === "undefined") {
      return false;
    }
    try {
      const testKey = "__storage_test__";
      localStorage.setItem(testKey, "test");
      localStorage.removeItem(testKey);
      return true;
    } catch (error2) {
      return false;
    }
  }
  /**
   * Safely get an item from localStorage with JSON parsing
   */
  safeGetItem(key) {
    if (!this.isLocalStorageAvailable) {
      return null;
    }
    try {
      const item = localStorage.getItem(key);
      if (item === null) {
        return null;
      }
      return JSON.parse(item);
    } catch (error2) {
      this.safeClearItem(key);
      return null;
    }
  }
  /**
   * Safely set an item in localStorage with JSON stringification
   */
  safeSetItem(key, value) {
    if (!this.isLocalStorageAvailable) {
      return false;
    }
    try {
      localStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch (error2) {
      if (error2 instanceof DOMException && error2.code === 22) {
        this.clearOldData();
        try {
          localStorage.setItem(key, JSON.stringify(value));
          return true;
        } catch (retryError) {
        }
      }
      return false;
    }
  }
  /**
   * Safely clear an item from localStorage
   */
  safeClearItem(key) {
    if (!this.isLocalStorageAvailable) {
      return;
    }
    try {
      localStorage.removeItem(key);
    } catch (error2) {
    }
  }
  /**
   * Clear old data to free up storage space
   */
  clearOldData() {
    this.safeClearItem(STORAGE_KEYS.SESSION);
  }
  // =============================================================================
  // Progress Data Methods
  // =============================================================================
  /**
   * Get playlist progress data for a specific user
   * If userId doesn't match stored userId, returns null (stale data)
   * @param userId - The user ID to validate against stored data
   */
  getProgress(userId) {
    const stored = this.safeGetItem(STORAGE_KEYS.PROGRESS);
    if (!stored) {
      return null;
    }
    if (userId && stored.userId && stored.userId !== userId) {
      log(`[StorageManager] Progress belongs to different user (${stored.userId}), ignoring`);
      return null;
    }
    return stored.playlists || null;
  }
  /**
   * Set playlist progress data for a specific user
   * Clears existing data if userId changes
   * @param progress - The progress data to save
   * @param userId - The user ID to associate with this progress
   */
  setProgress(progress, userId) {
    const stored = {
      userId,
      playlists: progress
    };
    return this.safeSetItem(STORAGE_KEYS.PROGRESS, stored);
  }
  /**
   * Clear all progress data
   */
  clearProgress() {
    this.safeClearItem(STORAGE_KEYS.PROGRESS);
  }
  // =============================================================================
  // Session Data Methods  
  // =============================================================================
  /**
   * Get session data
   */
  getSession() {
    return this.safeGetItem(STORAGE_KEYS.SESSION);
  }
  /**
   * Set session data
   */
  setSession(session) {
    return this.safeSetItem(STORAGE_KEYS.SESSION, session);
  }
  /**
   * Clear session data
   */
  clearSession() {
    this.safeClearItem(STORAGE_KEYS.SESSION);
  }
  // =============================================================================
  // Anonymous User Data Methods
  // =============================================================================
  /**
   * Get anonymous user data
   */
  getAnonymousUserData() {
    return this.safeGetItem(STORAGE_KEYS.ANONYMOUS_USER);
  }
  /**
   * Set anonymous user data
   */
  setAnonymousUserData(data) {
    return this.safeSetItem(STORAGE_KEYS.ANONYMOUS_USER, data);
  }
  /**
   * Clear anonymous user data
   */
  clearAnonymousUserData() {
    this.safeClearItem(STORAGE_KEYS.ANONYMOUS_USER);
  }
  // =============================================================================
  // Pending Navigation Methods (Cross-page URL transitions)
  // =============================================================================
  /**
   * Get pending navigation data for cross-page URL transitions
   * Used when a step has a url-path transition and user navigates causing hard refresh
   */
  getPendingNavigation() {
    return this.safeGetItem(STORAGE_KEYS.PENDING_NAVIGATION);
  }
  /**
   * Set pending navigation data
   * Called when setting up a url-path transition to enable resuming after hard refresh
   * @param data - The pending navigation data to save
   */
  setPendingNavigation(data) {
    log(`[StorageManager] Saving pending navigation to step ${data.nextStepId} for pattern ${data.urlPattern}`);
    return this.safeSetItem(STORAGE_KEYS.PENDING_NAVIGATION, data);
  }
  /**
   * Clear pending navigation data
   * Called after successful transition or when navigation is no longer valid
   */
  clearPendingNavigation() {
    this.safeClearItem(STORAGE_KEYS.PENDING_NAVIGATION);
  }
  // =============================================================================
  // Utility Methods
  // =============================================================================
  /**
   * Clear all storage data
   */
  clearAll() {
    this.clearProgress();
    this.clearSession();
    this.clearAnonymousUserData();
    this.clearPendingNavigation();
  }
};
__publicField(_StorageManager, "instance", null);
let StorageManager = _StorageManager;
const storageManager = StorageManager.getInstance();
const createInitialContext = () => ({
  currentStep: null,
  error: null
});
const saltfishStore = createStore()(
  immer((set2, get) => {
    let _stateMachine = new PlayerStateMachine(
      playerStateMachineConfig,
      createInitialContext()
    );
    const transitionState = (event) => {
      const newState = _stateMachine.send(event);
      return newState;
    };
    return {
      // State
      config: null,
      user: null,
      userData: null,
      // User data from backend
      currentState: _stateMachine.getState(),
      // Get initial state from machine
      manifest: null,
      currentStepId: null,
      isMinimized: false,
      position: null,
      progress: {},
      error: null,
      playlistOptions: null,
      backendPlaylists: [],
      // Renamed state for playlists from backend
      isAdmin: false,
      // Admin privileges flag from backend
      isMuted: false,
      // Global mute state that persists across video transitions
      abTests: [],
      // A/B test configurations from backend
      abTestAssignments: {},
      // User's A/B test assignments
      // Actions
      initialize: async (config) => {
        set2((state) => {
          state.config = config;
          state.currentState = transitionState({ type: "INITIALIZE" });
        });
      },
      // Add setPlaylistOptions action
      setPlaylistOptions: (options) => {
        set2((state) => {
          state.playlistOptions = options;
          if (options.position) {
            const validPosition = options.position === "bottom-left" || options.position === "bottom-right" ? options.position : "bottom-right";
            state.position = { x: 0, y: 0 };
            state.playlistOptions = { ...options, position: validPosition };
          }
        });
      },
      identifyUser: (userId, userData) => {
        const user = {
          id: userId,
          ...userData
        };
        set2((state) => {
          state.user = user;
        });
      },
      setUserData: (userData) => {
        set2((state) => {
          state.userData = userData;
        });
      },
      setManifest: (manifest, startStepId) => {
        set2((state) => {
          state.manifest = manifest;
          state.currentStepId = startStepId;
          const startStep = manifest.steps.find((s) => s.id === startStepId);
          if (startStep) {
            state.currentState = transitionState({ type: "MANIFEST_LOADED", step: startStep });
          } else {
            state.currentState = transitionState({ type: "ERROR", error: new Error(`Start step '${startStepId}' not found in manifest`) });
          }
        });
      },
      play: () => {
        const { currentState } = get();
        set2((state) => {
          state.currentState = transitionState({ type: "PLAY" });
        });
      },
      pause: () => {
        const { currentState } = get();
        set2((state) => {
          state.currentState = transitionState({ type: "PAUSE" });
        });
      },
      minimize: () => {
        const { currentState } = get();
        set2((state) => {
          if (state.currentState === "playing") {
            state.currentState = transitionState({ type: "PAUSE" });
          }
          state.currentState = transitionState({ type: "MINIMIZE" });
          state.isMinimized = true;
        });
      },
      maximize: () => {
        set2((state) => {
          state.currentState = transitionState({ type: "MAXIMIZE" });
          state.isMinimized = false;
        });
      },
      goToStep: (stepId) => {
        const { manifest } = get();
        if (stepId === "completed") {
          set2((state) => {
            state.currentState = transitionState({ type: "COMPLETE_PLAYLIST" });
          });
          return;
        }
        if (manifest && manifest.steps.some((step) => step.id === stepId)) {
          const targetStep = manifest.steps.find((step) => step.id === stepId);
          set2((state) => {
            var _a, _b, _c;
            state.currentStepId = stepId;
            if (targetStep) {
              state.currentState = transitionState({
                type: "TRANSITION_TO_STEP",
                step: targetStep
              });
              if (state.position || ((_a = state.playlistOptions) == null ? void 0 : _a.position)) {
                state.position = { x: 0, y: 0 };
              }
            }
            state.progress[manifest.id] = {
              ...state.progress[manifest.id],
              lastStepId: stepId,
              lastVisited: (/* @__PURE__ */ new Date()).toISOString()
            };
            const playlistPersistence = ((_b = state.playlistOptions) == null ? void 0 : _b.persistence) ?? true;
            if (playlistPersistence) {
              const userId = (_c = state.user) == null ? void 0 : _c.id;
              storageManager.setProgress(state.progress, userId);
            }
          });
        }
      },
      reset: () => {
        set2((state) => {
          state.config = null;
          state.user = null;
          state.userData = null;
          state.currentState = _stateMachine.getState();
          state.manifest = null;
          state.currentStepId = null;
          state.isMinimized = false;
          state.position = null;
          state.progress = {};
          state.error = null;
          _stateMachine = new PlayerStateMachine(
            playerStateMachineConfig,
            createInitialContext()
          );
        });
      },
      setError: (error2) => {
        set2((state) => {
          state.currentState = transitionState({
            type: "ERROR",
            error: error2
          });
          state.error = error2;
        });
      },
      setAutoplayFallback: () => {
        set2((state) => {
          state.currentState = transitionState({ type: "AUTOPLAY_FALLBACK" });
        });
      },
      setIdleMode: () => {
        set2((state) => {
          state.currentState = transitionState({ type: "START_IDLE_MODE" });
        });
      },
      setMuted: (muted) => {
        set2((state) => {
          state.isMuted = muted;
        });
      },
      // Correct action for playlists from backend
      setBackendPlaylists: (playlists) => {
        set2((state) => {
          state.backendPlaylists = playlists;
        });
      },
      // Set admin flag from backend
      setIsAdmin: (isAdmin) => {
        set2((state) => {
          state.isAdmin = isAdmin;
        });
      },
      completePlaylist: () => {
        set2((state) => {
          state.currentState = transitionState({ type: "COMPLETE_PLAYLIST" });
        });
      },
      // Add new method to reset playlist state while preserving config and user data
      resetForNewPlaylist: () => {
        set2((state) => {
          const preservedConfig = state.config;
          const preservedUser = state.user;
          const preservedUserData = state.userData;
          const preservedProgress = state.progress;
          state.manifest = null;
          state.currentStepId = null;
          state.isMinimized = false;
          state.position = null;
          state.error = null;
          state.playlistOptions = null;
          _stateMachine = new PlayerStateMachine(
            playerStateMachineConfig,
            createInitialContext()
          );
          state.currentState = _stateMachine.getState();
          state.config = preservedConfig;
          state.user = preservedUser;
          state.userData = preservedUserData;
          state.progress = preservedProgress;
        });
      },
      loadPlaylistProgress: (playlistId, progress) => {
        set2((state) => {
          state.progress[playlistId] = progress;
        });
      },
      // Method to register actions with the state machine without exposing it
      registerStateMachineActions: (actions) => {
        _stateMachine.registerActions(actions);
      },
      // Method to send events to the state machine without exposing it
      sendStateMachineEvent: (event) => {
        set2((state) => {
          state.currentState = transitionState(event);
        });
      },
      // New action to update progress when transitioning to completion waiting state
      updateProgressWithCompletion: (playlistId, currentStepId) => {
        set2((state) => {
          var _a, _b;
          state.currentState = transitionState({ type: "COMPLETE_PLAYLIST_WAITING_FOR_INTERACTION" });
          state.progress[playlistId] = {
            ...state.progress[playlistId],
            lastStepId: currentStepId,
            lastVisited: (/* @__PURE__ */ new Date()).toISOString(),
            completedWaitingForInteraction: true
          };
          const playlistPersistence = ((_a = state.playlistOptions) == null ? void 0 : _a.persistence) ?? true;
          if (playlistPersistence) {
            const userId = (_b = state.user) == null ? void 0 : _b.id;
            storageManager.setProgress(state.progress, userId);
          }
        });
      },
      setABTests: (abTests) => {
        set2((state) => {
          state.abTests = abTests;
        });
      },
      setABTestAssignments: (assignments) => {
        set2((state) => {
          state.abTestAssignments = assignments;
        });
      },
      getFilteredPlaylists: () => {
        const state = get();
        const allPlaylists = state.backendPlaylists || [];
        const abTests = state.abTests || [];
        const assignments = state.abTestAssignments || {};
        if (abTests.length === 0) {
          return allPlaylists;
        }
        const excludedPlaylistIds = /* @__PURE__ */ new Set();
        for (const test of abTests) {
          const assignment = assignments[test.id];
          if (!assignment || !assignment.assigned) {
            excludedPlaylistIds.add(test.playlistId);
          }
        }
        return allPlaylists.filter(
          (playlist) => !excludedPlaylistIds.has(playlist.id)
        );
      }
    };
  })
);
const useSaltfishStore = {
  getState: () => saltfishStore.getState(),
  setState: saltfishStore.setState,
  subscribe: saltfishStore.subscribe,
  destroy: saltfishStore.destroy
};
function getSaltfishStore() {
  try {
    return useSaltfishStore.getState();
  } catch (error2) {
    const errorMessage = `[storeUtils] Failed to access Saltfish store: ${error2}`;
    throw new Error(errorMessage);
  }
}
const MAX_PROGRESS_AGE_MS = 6e3;
function parseProgressTimestamp(progressData) {
  if (!progressData) {
    return null;
  }
  if (progressData.lastProgressAt && typeof progressData.lastProgressAt === "object" && "_seconds" in progressData.lastProgressAt && "_nanoseconds" in progressData.lastProgressAt) {
    const firestoreTimestamp = progressData.lastProgressAt;
    return firestoreTimestamp._seconds * 1e3 + Math.floor(firestoreTimestamp._nanoseconds / 1e6);
  }
  if (progressData.timestamp && typeof progressData.timestamp === "number") {
    return progressData.timestamp;
  }
  if (progressData.lastProgressAt && typeof progressData.lastProgressAt === "number") {
    return progressData.lastProgressAt;
  }
  return null;
}
function isProgressRecent(progressData, maxAgeMs = MAX_PROGRESS_AGE_MS) {
  const timestampMs = parseProgressTimestamp(progressData);
  if (!timestampMs) {
    return { isValid: false, ageMs: null, timestampMs: null };
  }
  const currentTime = Date.now();
  const ageMs = currentTime - timestampMs;
  return {
    isValid: ageMs <= maxAgeMs,
    ageMs,
    timestampMs
  };
}
class ErrorHandler {
  /**
   * Handles an error with consistent logging, reporting, and response
   * @param error The error to handle (Error object, string, or unknown)
   * @param context Context information about where the error occurred
   * @param options Options for how to handle the error
   */
  static handle(error2, context = {}, options = {}) {
    var _a;
    const finalOptions = { ...this.DEFAULT_OPTIONS, ...options };
    const normalizedError = this.normalizeError(error2, context);
    if (finalOptions.shouldLog) {
      this.logError(normalizedError, context, finalOptions.severity);
    }
    if (finalOptions.shouldUpdateStore) {
      this.updateStore(normalizedError);
    }
    if (finalOptions.shouldTriggerEvent) {
      this.triggerErrorEvent(normalizedError, context, (_a = context.component) == null ? void 0 : _a.toLowerCase());
    }
    if (finalOptions.shouldDestroy) {
      this.destroyPlayer();
    }
    if (finalOptions.shouldThrow) {
      throw normalizedError;
    }
    return normalizedError;
  }
  /**
   * Handles initialization errors
   */
  static handleInitializationError(error2, context = {}) {
    return this.handle(error2, { ...context, component: "Initialization" }, {
      severity: "critical",
      shouldLog: true,
      shouldUpdateStore: true,
      shouldDestroy: true,
      shouldThrow: true
    });
  }
  /**
   * Handles playlist loading errors
   */
  static handlePlaylistError(error2, context = {}) {
    return this.handle(error2, { ...context, component: "Playlist" }, {
      severity: "error",
      shouldLog: true,
      shouldUpdateStore: true,
      shouldTriggerEvent: true
    });
  }
  /**
   * Handles video loading/playback errors
   */
  static handleVideoError(error2, context = {}) {
    return this.handle(error2, { ...context, component: "Video" }, {
      severity: "error",
      shouldLog: true,
      shouldUpdateStore: true,
      shouldTriggerEvent: true
    });
  }
  /**
   * Handles network/API errors
   */
  static handleNetworkError(error2, context = {}) {
    return this.handle(error2, { ...context, component: "Network" }, {
      severity: "warning",
      shouldLog: true,
      shouldTriggerEvent: true
    });
  }
  /**
   * Handles non-critical errors (warnings)
   */
  static handleWarning(error2, context = {}) {
    return this.handle(error2, context, {
      severity: "warning",
      shouldLog: true,
      shouldThrow: false
    });
  }
  /**
   * Handles cleanup/destroy errors
   */
  static handleCleanupError(error2, context = {}) {
    return this.handle(error2, { ...context, component: "Cleanup" }, {
      severity: "warning",
      shouldLog: true,
      shouldThrow: false
    });
  }
  /**
   * Normalizes different error types to Error objects
   */
  static normalizeError(error2, context) {
    if (error2 instanceof Error) {
      return error2;
    }
    if (typeof error2 === "string") {
      return new Error(error2);
    }
    const errorString = error2 && typeof error2 === "object" && "message" in error2 ? String(error2.message) : String(error2);
    return new Error(`Unknown error in ${context.component || "application"}: ${errorString}`);
  }
  /**
   * Formats error message with context information
   */
  static formatErrorMessage(error2, context) {
    const parts = [];
    if (context.component) {
      parts.push(`[${context.component}]`);
    }
    if (context.method) {
      parts.push(`${context.method}:`);
    }
    parts.push(error2.message);
    return parts.join(" ");
  }
  /**
   * Logs error with appropriate severity level
   */
  static logError(error2, context, severity) {
    const message = this.formatErrorMessage(error2, context);
    const logData = {
      error: {
        name: error2.name,
        message: error2.message,
        stack: error2.stack
      },
      context,
      severity,
      timestamp: (/* @__PURE__ */ new Date()).toISOString()
    };
    switch (severity) {
      case "info":
        break;
      case "warning":
        console.warn(message, logData);
        break;
      case "error":
      case "critical":
        console.error(message, logData);
        break;
    }
  }
  /**
   * Updates store with error state
   */
  static updateStore(error2) {
    try {
      const store = getSaltfishStore();
      store.setError(error2);
    } catch (storeError) {
      console.error("Failed to update store with error:", storeError);
    }
  }
  /**
   * Triggers error event through EventManager
   */
  static triggerErrorEvent(error2, context, errorType) {
    var _a, _b;
    try {
      const store = getSaltfishStore();
      if (typeof window !== "undefined" && window._saltfishPlayer) {
        const player = window._saltfishPlayer;
        if (player && player.eventManager) {
          player.eventManager.trigger("error", {
            timestamp: Date.now(),
            playlistId: context.playlistId || ((_a = store.manifest) == null ? void 0 : _a.id),
            stepId: context.stepId || store.currentStepId,
            error: error2,
            errorType: context.errorType || errorType || ((_b = context.component) == null ? void 0 : _b.toLowerCase()) || "unknown"
          });
        }
      }
    } catch (eventError) {
      console.error("Failed to trigger error event:", eventError);
    }
  }
  /**
   * Destroys player for critical errors
   */
  static destroyPlayer() {
    try {
      if (typeof window !== "undefined" && window._saltfishPlayer) {
        const player = window._saltfishPlayer;
        if (player && typeof player.destroy === "function") {
          player.destroy();
        }
      }
    } catch (destroyError) {
      console.error("Failed to destroy player during error handling:", destroyError);
    }
  }
  /**
   * Creates a standardized error with context
   */
  static createError(message, context = {}) {
    const error2 = new Error(this.formatErrorMessage(new Error(message), context));
    if (context.component) {
      error2.component = context.component;
    }
    if (context.method) {
      error2.method = context.method;
    }
    if (context.playlistId) {
      error2.playlistId = context.playlistId;
    }
    if (context.stepId) {
      error2.stepId = context.stepId;
    }
    return error2;
  }
  /**
   * Checks if an error is recoverable based on its type and context
   */
  static isRecoverable(error2) {
    if (error2.message.includes("fetch") || error2.message.includes("network") || error2.message.includes("timeout")) {
      return true;
    }
    const recoverablePatterns = [
      /autoplay.*blocked/i,
      /video.*failed.*load/i,
      /manifest.*not.*found/i
    ];
    return recoverablePatterns.some((pattern) => pattern.test(error2.message));
  }
  /**
   * Safely executes a function with error handling
   */
  static async safeExecute(fn, context = {}, options = {}) {
    try {
      return await fn();
    } catch (error2) {
      this.handle(error2, context, options);
      return null;
    }
  }
}
__publicField(ErrorHandler, "DEFAULT_OPTIONS", {
  severity: "error",
  shouldLog: true,
  shouldThrow: false,
  shouldUpdateStore: false,
  shouldTriggerEvent: false,
  shouldDestroy: false
});
const VERSION = "0.2.73";
class ShareLinkService {
  /**
   * Detects if the current URL contains a saltfish-share-id parameter
   * @returns The shareId if found, null otherwise
   */
  detectShareIdFromUrl() {
    try {
      const url = window.location.href;
      const regex = /saltfish-share-id=([^&#]*)/;
      const match = url.match(regex);
      if (match && match[1]) {
        const shareId = match[1];
        log("[ShareLinkService] Detected share ID in URL:", shareId);
        return shareId;
      }
      return null;
    } catch (error2) {
      return null;
    }
  }
  /**
   * Fetches share data from the share API
   * @param shareId The share ID to fetch
   * @returns Share data including flowId
   * @throws Error if API call fails or returns invalid data
   */
  async fetchShareData(shareId) {
    try {
      log("[ShareLinkService] Fetching share data for shareId:", shareId);
      const response = await fetch(`${API.SHARE_BASE_URL}/${shareId}`, {
        method: "GET",
        headers: {
          "Content-Type": "application/json"
        }
      });
      if (!response.ok) {
        throw new Error(`Share API returned status ${response.status}`);
      }
      const data = await response.json();
      if (!data.flowId) {
        throw new Error("Share API response missing flowId");
      }
      log("[ShareLinkService] Successfully fetched share data:", {
        shareId: data.shareId,
        flowId: data.flowId,
        createdBy: data.createdBy
      });
      return data;
    } catch (error2) {
      throw ErrorHandler.handleInitializationError(
        error2,
        {
          component: "ShareLinkService",
          method: "fetchShareData",
          additionalData: { shareId }
        }
      );
    }
  }
  /**
   * Main orchestration method: detects share link and fetches flow data
   * @returns The share data to auto-start, or null if no valid share link found
   */
  async shouldAutoStartSharePlaylist() {
    try {
      const shareId = this.detectShareIdFromUrl();
      if (!shareId) {
        return null;
      }
      const shareData = await this.fetchShareData(shareId);
      log("[ShareLinkService] Share link will auto-start playlist:", {
        flowId: shareData.flowId,
        isGlobal: shareData.isGlobal
      });
      return shareData;
    } catch (error2) {
      return null;
    }
  }
}
const ANALYTICS = {
  /** Interval for flushing analytics events to the backend (30 seconds) */
  FLUSH_INTERVAL_MS: 3e4
};
const TIMING = {
  // Polling and updates
  /** Video progress polling interval (50ms) */
  VIDEO_PROGRESS_POLL_INTERVAL: 50,
  /** Cursor position update throttle interval (100ms) */
  CURSOR_UPDATE_THROTTLE: 100,
  /** Delay for state processing operations (100ms) */
  STATE_PROCESSING_DELAY_MS: 100,
  // Analytics
  /** Analytics event flush interval (30 seconds) */
  ANALYTICS_FLUSH_INTERVAL: 3e4,
  // Timeouts
  /** User data loading timeout (5 seconds) */
  USER_DATA_TIMEOUT: 5e3,
  /** Step timeout - player will be destroyed if user stays on same step (120 seconds) */
  STEP_TIMEOUT: 12e4,
  /** Retry delay for failed operations (0.5 seconds) */
  RETRY_DELAY_MS: 500,
  // DOM and cursor operations
  /** Delay for DOM stabilization before cursor operations (0.5 seconds) */
  DOM_STABILIZATION_DELAY_MS: 500,
  /** Default cursor animation distance in pixels */
  CURSOR_DEFAULT_DISTANCE: 100,
  // URL monitoring
  /** Interval for checking URL path changes (5 seconds) */
  URL_PATH_CHECK_INTERVAL_MS: 5e3,
  // Session persistence
  /** Session expiry time (30 minutes) */
  SESSION_EXPIRY: 30 * 60 * 1e3,
  // Cross-page navigation
  /** Pending navigation expiry time (60 seconds) - longer than normal 6s rule for URL transitions */
  PENDING_NAVIGATION_EXPIRY: 60 * 1e3
};
const THRESHOLDS = {
  /** Minimum scroll distance in pixels to trigger scroll events */
  SCROLL_THRESHOLD_PX: 10
};
function stripShareIdFromUrl(url) {
  try {
    let cleanedUrl = url.replace(/[?&]saltfish-share-id=[^&#]*/g, (match, offset) => {
      if (match.startsWith("?")) {
        const afterMatch = url.substring(offset + match.length);
        if (afterMatch.startsWith("&")) {
          return "?";
        }
        return "";
      }
      return "";
    });
    cleanedUrl = cleanedUrl.replace(/\?&/g, "?");
    cleanedUrl = cleanedUrl.replace(/&&/g, "&");
    cleanedUrl = cleanedUrl.replace(/\?$/g, "");
    cleanedUrl = cleanedUrl.replace(/&$/g, "");
    return cleanedUrl;
  } catch (error2) {
    return url;
  }
}
function validateUrlRequirement(urlRequirement) {
  const { pattern, matchType } = urlRequirement;
  if (!pattern) {
    return true;
  }
  const currentUrl = stripShareIdFromUrl(window.location.href);
  const currentPath = window.location.pathname;
  if (matchType === "regex") {
    try {
      const regex = new RegExp(pattern);
      const fullUrlMatch = regex.test(currentUrl);
      const pathMatch = regex.test(currentPath);
      const matches2 = fullUrlMatch || pathMatch;
      log(`urlValidation: Result (regex) - matches: ${matches2}`);
      return matches2;
    } catch (error2) {
      return false;
    }
  }
  if (matchType === "contains") {
    const matches2 = currentUrl.includes(pattern) || currentPath.includes(pattern);
    return matches2;
  }
  const matches = currentUrl === pattern || currentPath === pattern;
  return matches;
}
async function validateUrlRequirementWithRetry(urlRequirement, maxRetries = 20, retryDelay = 100) {
  if (!urlRequirement) {
    return true;
  }
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    if (validateUrlRequirement(urlRequirement)) {
      return true;
    }
    if (attempt < maxRetries - 1) {
      await new Promise((resolve) => setTimeout(resolve, retryDelay));
      const saltfishPlayer = window._saltfishPlayer;
      if (!saltfishPlayer) {
        return false;
      }
    }
  }
  log(`urlValidation: Expected pattern: '${urlRequirement.pattern}' (matchType: ${urlRequirement.matchType})`);
  return false;
}
class PlayerInitializationService {
  constructor(managers) {
    __publicField(this, "managers");
    __publicField(this, "userManagementService");
    __publicField(this, "playlistOrchestrator");
    __publicField(this, "shareLinkService");
    // Store the last config for potential reinitialization
    __publicField(this, "lastConfig", null);
    this.managers = managers;
    this.shareLinkService = new ShareLinkService();
  }
  /**
   * Set the user management service for dependency
   */
  setUserManagementService(service) {
    this.userManagementService = service;
  }
  /**
   * Set the playlist orchestrator for dependency
   */
  setPlaylistOrchestrator(orchestrator) {
    this.playlistOrchestrator = orchestrator;
  }
  /**
   * Get the last config for restoration
   */
  getLastConfig() {
    return this.lastConfig;
  }
  /**
   * Initialize the player with configuration
   */
  async initialize(config) {
    var _a;
    try {
      const response = await fetch(`${API.BASE_URL}/validate-token`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          token: config.token,
          playerVersion: VERSION
        })
      });
      const data = await response.json();
      log("[PlayerInitializationService.initialize] Token validation response data:", data);
      if (!data.isValid) {
        throw ErrorHandler.handleInitializationError(
          data.error || "Token validation failed",
          {
            component: "PlayerInitializationService",
            method: "initialize",
            additionalData: { token: ((_a = config.token) == null ? void 0 : _a.substring(0, 10)) + "..." }
          }
        );
      }
      const updatedConfig = {
        ...config,
        showLogo: data.showLogo !== false
        // Default to true if not specified
      };
      this.lastConfig = updatedConfig;
      const store = getSaltfishStore();
      store.initialize(updatedConfig);
      this.managers.analyticsManager.initialize(config, this.managers.sessionManager.getSessionId());
      if (data.isAdmin && store.setIsAdmin) {
        store.setIsAdmin(true);
        log("[PlayerInitializationService.initialize] Admin token detected");
      }
      if (data.playlists && Array.isArray(data.playlists) && store.setBackendPlaylists) {
        log("[PlayerInitializationService.initialize] Found data.playlists, attempting to store:", data.playlists);
        store.setBackendPlaylists(data.playlists);
        log("[PlayerInitializationService.initialize] Successfully called setBackendPlaylists with data.playlists.");
        if (data.playlists.length > 0) {
          this.managers.triggerManager.registerTriggers(data.playlists);
          log("[PlayerInitializationService.initialize] Registered playlist triggers");
        } else if (data.isAdmin) {
          log("[PlayerInitializationService.initialize] Admin token with empty playlists array - skipping trigger registration");
        }
        if (data.abTests && Array.isArray(data.abTests)) {
          log("[PlayerInitializationService.initialize] Initializing A/B tests:", data.abTests);
          this.managers.abTestManager.initializeTests(data.abTests);
        }
      } else if (!data.isAdmin) {
        throw ErrorHandler.handleInitializationError(
          "Backend validation successful, but no playlists array provided in the response. Cannot initialize player.",
          {
            component: "PlayerInitializationService",
            method: "initialize",
            additionalData: { responseData: data }
          }
        );
      }
      this.managers.eventManager.trigger("initialized", {
        timestamp: Date.now()
      });
    } catch (error2) {
      throw ErrorHandler.handleInitializationError(
        error2,
        {
          component: "PlayerInitializationService",
          method: "initialize"
        }
      );
    }
  }
  /**
   * Fetch user data from backend
   */
  async fetchUserData(userId, userData) {
    var _a;
    try {
      const store = getSaltfishStore();
      if (!((_a = store.config) == null ? void 0 : _a.token)) {
        ErrorHandler.handleWarning(
          "Cannot fetch user data: Token not available",
          {
            component: "PlayerInitializationService",
            method: "fetchUserData",
            userId
          }
        );
        return;
      }
      log("[PlayerInitializationService.fetchUserData] Fetching user data for userId:", userId);
      log("[PlayerInitializationService.fetchUserData] userData being sent to backend:", userData);
      const response = await fetch(`https://player.saltfish.ai/clients/${store.config.token}/users/${userId}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ userData })
      });
      if (!response.ok) {
        const errorText = await response.text();
        log("[PlayerInitializationService.fetchUserData] Failed to fetch user data:", {
          status: response.status,
          statusText: response.statusText,
          error: errorText
        });
        return;
      }
      const data = await response.json();
      log("[PlayerInitializationService.fetchUserData] User data fetched successfully:", data);
      if (data.success) {
        store.setUserData({
          watchedPlaylists: data.watchedPlaylists || {},
          language: data.language
          // Store language from backend if available
        });
        const userListAssignments = data.userListAssignments || {};
        const existingAssignments = data.abTestAssignments || {};
        this.managers.abTestManager.assignUserToTests(userId, existingAssignments, userListAssignments);
        this.managers.eventManager.trigger("userDataLoaded", {
          timestamp: Date.now(),
          userId,
          userData: {
            watchedPlaylists: data.watchedPlaylists || {}
          }
        });
        if (this.userManagementService) {
          this.userManagementService.resolveUserDataLoaded();
        }
        const resumedFromPendingNav = await this.checkAndResumeFromPendingNavigation();
        if (resumedFromPendingNav) {
          log("[PlayerInitializationService.fetchUserData] Resumed from pending URL navigation, skipping other checks");
        }
        const resumedPlaylist = resumedFromPendingNav || await this.checkAndResumeInProgressPlaylist(data.watchedPlaylists || {});
        if (!resumedPlaylist) {
          const shareData = await this.shareLinkService.shouldAutoStartSharePlaylist();
          if (shareData && this.playlistOrchestrator) {
            log("[PlayerInitializationService.fetchUserData] Share link detected, auto-starting playlist:", shareData.flowId);
            await this.playlistOrchestrator.startPlaylist(shareData.flowId, {
              once: false,
              // Always allow playback via share link
              position: "bottom-right",
              _startedFromShareLink: true,
              // Bypass backend validation
              _isGlobalShare: shareData.isGlobal
              // Pass global flag for validation
            });
            log("[PlayerInitializationService.fetchUserData] Skipped trigger monitoring due to share link auto-start");
          } else {
            this.managers.triggerManager.startMonitoring();
            log("[PlayerInitializationService.fetchUserData] Started playlist trigger monitoring");
          }
        } else {
          log("[PlayerInitializationService.fetchUserData] Skipped trigger monitoring due to resumed in-progress playlist");
        }
      } else {
        log("[PlayerInitializationService.fetchUserData] Backend returned unsuccessful response:", data);
        if (this.userManagementService) {
          this.userManagementService.resolveUserDataLoaded();
        }
      }
    } catch (error2) {
      ErrorHandler.handleNetworkError(
        error2,
        {
          component: "PlayerInitializationService",
          method: "fetchUserData",
          userId
        }
      );
      if (this.userManagementService) {
        this.userManagementService.resolveUserDataLoaded();
      }
    }
  }
  /**
   * Load anonymous user data from localStorage
   */
  async loadAnonymousUserData(userId, userData) {
    try {
      if (typeof window === "undefined") {
        return;
      }
      log("[PlayerInitializationService.loadAnonymousUserData] Loading anonymous user data for userId:", userId);
      const abTestAssignments = this.managers.abTestManager.assignUserToTests(userId, void 0, {});
      const existingData = this.managers.storageManager.getAnonymousUserData();
      let anonymousUserData = {
        userId,
        userData: userData || {},
        watchedPlaylists: {},
        abTestAssignments,
        timestamp: Date.now()
      };
      if (existingData) {
        const existingAssignments = existingData.abTestAssignments || {};
        const mergedAssignments = this.managers.abTestManager.assignUserToTests(userId, existingAssignments);
        anonymousUserData = {
          userId,
          userData: { ...existingData.userData, ...userData },
          watchedPlaylists: existingData.watchedPlaylists || {},
          abTestAssignments: mergedAssignments,
          timestamp: Date.now()
        };
        log("[PlayerInitializationService.loadAnonymousUserData] Loaded existing anonymous user data:", anonymousUserData);
      }
      this.managers.storageManager.setAnonymousUserData(anonymousUserData);
      const store = getSaltfishStore();
      store.setUserData({
        watchedPlaylists: anonymousUserData.watchedPlaylists || {},
        language: userData == null ? void 0 : userData.language
        // Store language from userData
      });
      this.managers.eventManager.trigger("userDataLoaded", {
        timestamp: Date.now(),
        userId,
        userData: {
          watchedPlaylists: anonymousUserData.watchedPlaylists || {}
        }
      });
      const resumedFromPendingNav = await this.checkAndResumeFromPendingNavigation();
      if (resumedFromPendingNav) {
        log("[PlayerInitializationService.loadAnonymousUserData] Resumed from pending URL navigation, skipping other checks");
      }
      const watchedPlaylists = anonymousUserData.watchedPlaylists || {};
      const resumedPlaylist = resumedFromPendingNav || await this.checkAndResumeInProgressPlaylist(watchedPlaylists);
      if (!resumedPlaylist) {
        const shareData = await this.shareLinkService.shouldAutoStartSharePlaylist();
        if (shareData && this.playlistOrchestrator) {
          log("[PlayerInitializationService.loadAnonymousUserData] Share link detected, auto-starting playlist:", shareData.flowId);
          await this.playlistOrchestrator.startPlaylist(shareData.flowId, {
            once: false,
            // Always allow playback via share link
            position: "bottom-right",
            _startedFromShareLink: true,
            // Bypass backend validation
            _isGlobalShare: shareData.isGlobal
            // Pass global flag for validation
          });
          log("[PlayerInitializationService.loadAnonymousUserData] Skipped trigger monitoring due to share link auto-start");
        } else {
          this.managers.triggerManager.startMonitoring();
          log("[PlayerInitializationService.loadAnonymousUserData] Started playlist trigger monitoring with localStorage data");
        }
      } else {
        log("[PlayerInitializationService.loadAnonymousUserData] Skipped trigger monitoring due to resumed in-progress playlist");
      }
    } catch (error2) {
      ErrorHandler.handleNetworkError(
        error2,
        {
          component: "PlayerInitializationService",
          method: "loadAnonymousUserData",
          userId
        }
      );
    }
  }
  /**
   * Check for in-progress playlists and resume them
   */
  async checkAndResumeInProgressPlaylist(watchedPlaylists) {
    var _a;
    if (!watchedPlaylists) {
      return false;
    }
    const store = getSaltfishStore();
    const currentManifestId = (_a = store.manifest) == null ? void 0 : _a.id;
    const currentState = store.currentState;
    if (currentManifestId && currentState !== "idle" && currentState !== "error") {
      return false;
    }
    const inProgressEntry = Object.entries(watchedPlaylists).find(([playlistId, status]) => {
      if ((status == null ? void 0 : status.status) !== "in_progress") {
        return false;
      }
      const { isValid, ageMs } = isProgressRecent(status);
      if (!isValid) {
        return false;
      }
      return true;
    });
    if (inProgressEntry) {
      const [playlistId, status] = inProgressEntry;
      parseProgressTimestamp(status);
      try {
        if (this.playlistOrchestrator) {
          await this.playlistOrchestrator.startPlaylist(playlistId);
          log(`[PlayerInitializationService.checkAndResumeInProgressPlaylist] Successfully resumed playlist ${playlistId}`);
          return true;
        }
      } catch (error2) {
        return false;
      }
    }
    return false;
  }
  /**
   * Check for pending navigation from cross-page URL transitions and auto-start the playlist
   * This handles the case where user navigated to a new page (hard refresh)
   * and we need to resume from the step that was waiting for that URL
   * @returns true if a playlist was started from pending navigation
   */
  async checkAndResumeFromPendingNavigation() {
    const pending = this.managers.storageManager.getPendingNavigation();
    if (!pending) {
      return false;
    }
    const ageMs = Date.now() - pending.timestamp;
    if (ageMs > TIMING.PENDING_NAVIGATION_EXPIRY) {
      this.managers.storageManager.clearPendingNavigation();
      return false;
    }
    if (!this.isURLPathMatch(pending.urlPattern)) {
      log(`[PlayerInitializationService.checkAndResumeFromPendingNavigation] URL doesn't match pending pattern '${pending.urlPattern}'`);
      this.managers.storageManager.clearPendingNavigation();
      return false;
    }
    log(`[PlayerInitializationService.checkAndResumeFromPendingNavigation] URL matches! Starting playlist ${pending.playlistId} at step ${pending.nextStepId}`);
    this.managers.storageManager.clearPendingNavigation();
    try {
      if (this.playlistOrchestrator) {
        await this.playlistOrchestrator.startPlaylist(pending.playlistId, {
          startNodeId: pending.nextStepId
        });
        log(`[PlayerInitializationService.checkAndResumeFromPendingNavigation] Successfully started playlist from pending navigation`);
        return true;
      }
    } catch (error2) {
      return false;
    }
    return false;
  }
  /**
   * Checks if the current URL path matches a pattern
   * Uses the same logic as TransitionManager for consistency
   * @param pattern - The URL pattern to match (supports wildcards)
   * @returns true if the current URL matches the pattern
   */
  isURLPathMatch(pattern) {
    if (!pattern || typeof window === "undefined") {
      return false;
    }
    const currentUrl = stripShareIdFromUrl(window.location.href);
    const currentPath = window.location.pathname;
    const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const regexPattern = escapedPattern.replace(/\\\*/g, ".*");
    const regex = new RegExp(regexPattern);
    const match = regex.test(currentUrl) || regex.test(currentPath);
    return match;
  }
  /**
   * Get or create persistent anonymous user ID
   */
  getOrCreateAnonymousUserId() {
    if (typeof window === "undefined") {
      return `anonymous_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
    }
    const existingData = this.managers.storageManager.getAnonymousUserData();
    if (existingData == null ? void 0 : existingData.userId) {
      log(`[PlayerInitializationService.getOrCreateAnonymousUserId] Using existing anonymous user ID: ${existingData.userId}`);
      return existingData.userId;
    }
    const anonymousUserId = `anonymous_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
    this.managers.storageManager.setAnonymousUserData({
      userId: anonymousUserId,
      userData: {},
      watchedPlaylists: {},
      timestamp: Date.now()
    });
    return anonymousUserId;
  }
  destroy() {
    this.lastConfig = null;
  }
}
function detectUserLanguage() {
  try {
    const browserLang = navigator.language || navigator.userLanguage;
    if (!browserLang) {
      return void 0;
    }
    const normalizedLang = browserLang.split("-")[0].toLowerCase();
    debug(`LanguageDetector: Detected browser language: ${browserLang}, normalized to: ${normalizedLang}`);
    return normalizedLang;
  } catch (error2) {
    return void 0;
  }
}
function normalizeLanguageCode(language) {
  return language.split("-")[0].toLowerCase();
}
function processLanguageConfig(configLanguage) {
  if (!configLanguage) {
    return void 0;
  }
  if (configLanguage === "auto") {
    const detectedLang = detectUserLanguage();
    return detectedLang;
  }
  const normalizedLang = normalizeLanguageCode(configLanguage);
  return normalizedLang;
}
class UserManagementService {
  constructor(managers) {
    __publicField(this, "managers");
    __publicField(this, "playerInitializationService");
    // State for managing user data loading promise
    __publicField(this, "userDataLoadedPromise", null);
    __publicField(this, "userDataLoadedResolve", null);
    // Store the last user identification for restoration after reinitialization
    __publicField(this, "lastUserIdentification", null);
    this.managers = managers;
  }
  /**
   * Set the player initialization service for dependency
   */
  setPlayerInitializationService(service) {
    this.playerInitializationService = service;
  }
  /**
   * Get the last user identification for restoration
   */
  getLastUserIdentification() {
    return this.lastUserIdentification;
  }
  /**
   * Get the user data loaded promise for waiting on A/B test assignments
   */
  getUserDataLoadedPromise() {
    return this.userDataLoadedPromise;
  }
  /**
   * Identify a user with ID and optional data
   */
  identifyUser(userId, userData) {
    let processedUserData = userData;
    if (userData && typeof userData.language === "string") {
      const processedLanguage = processLanguageConfig(userData.language);
      processedUserData = {
        ...userData,
        language: processedLanguage
      };
      log(`UserManagementService: Processed language from userData: ${userData.language} -> ${processedLanguage}`);
    }
    this.lastUserIdentification = { userId, userData: processedUserData };
    const store = getSaltfishStore();
    store.identifyUser(userId, processedUserData);
    this.managers.analyticsManager.setUser({
      id: userId,
      ...processedUserData
    });
    const abTests = store.abTests || [];
    const hasABTests = abTests.length > 0;
    if (hasABTests) {
      this.userDataLoadedPromise = new Promise((resolve) => {
        this.userDataLoadedResolve = resolve;
      });
    }
    if (this.playerInitializationService) {
      this.playerInitializationService.fetchUserData(userId, processedUserData);
    }
  }
  /**
   * Identify user anonymously with optional data
   */
  async identifyAnonymous(userData) {
    if (!this.playerInitializationService) {
      throw new Error("PlayerInitializationService not set");
    }
    let processedUserData = userData;
    if (userData && typeof userData.language === "string") {
      const processedLanguage = processLanguageConfig(userData.language);
      processedUserData = {
        ...userData,
        language: processedLanguage
      };
      log(`UserManagementService: Processed language from userData (anonymous): ${userData.language} -> ${processedLanguage}`);
    }
    const userId = this.playerInitializationService.getOrCreateAnonymousUserId();
    this.lastUserIdentification = { userId, userData: processedUserData };
    const store = getSaltfishStore();
    store.identifyUser(userId, { ...processedUserData, __isAnonymous: true });
    this.managers.analyticsManager.setUser({
      id: userId,
      ...processedUserData
    });
    await this.playerInitializationService.loadAnonymousUserData(userId, processedUserData);
  }
  /**
   * Record A/B test attempt for analytics
   */
  async recordABTestAttempt(playlistId) {
    var _a;
    try {
      const store = getSaltfishStore();
      const user = store.user;
      if (!user || !((_a = store.config) == null ? void 0 : _a.token)) {
        return;
      }
      const abTests = store.abTests || [];
      const relevantTest = abTests.find((test) => test.playlistId === playlistId);
      if (!relevantTest) {
        return;
      }
      const assignments = store.abTestAssignments || {};
      const assignment = assignments[relevantTest.id];
      if (!assignment) {
        log(`[UserManagementService.recordABTestAttempt] No assignment found for test ${relevantTest.id}`);
        return;
      }
      log(`[UserManagementService.recordABTestAttempt] Recording A/B test attempt for playlist ${playlistId}, test ${relevantTest.name}, assigned: ${assignment.assigned}`);
      const response = await fetch(`https://player.saltfish.ai/clients/${store.config.token}/users/${user.id}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          abTestAssignments: {
            [relevantTest.id]: assignment
          }
        })
      });
      if (!response.ok) {
        log("[UserManagementService.recordABTestAttempt] Failed to record A/B test attempt:", {
          status: response.status,
          statusText: response.statusText
        });
      } else {
        log(`[UserManagementService.recordABTestAttempt] Successfully recorded A/B test attempt for ${relevantTest.name}`);
      }
    } catch (error2) {
    }
  }
  /**
   * Resolve the user data loaded promise (called by PlayerInitializationService)
   */
  resolveUserDataLoaded() {
    if (this.userDataLoadedResolve) {
      this.userDataLoadedResolve();
      this.userDataLoadedResolve = null;
      this.userDataLoadedPromise = null;
    }
  }
  destroy() {
    this.userDataLoadedPromise = null;
    this.userDataLoadedResolve = null;
    this.lastUserIdentification = null;
  }
}
class PlaylistValidator {
  /**
   * Validates all conditions for starting a playlist
   */
  async validatePlaylistStart(context) {
    const { playlistId, options, eventManager } = context;
    try {
      const store = getSaltfishStore();
      if (!store.config) {
        return {
          isValid: false,
          error: "Saltfish Player must be initialized before starting a playlist"
        };
      }
      if (store.isAdmin === true) {
        log(`[PlaylistValidator] Admin token detected. Allowing direct access to playlist '${playlistId}'`);
        const manifestPath = `https://storage.saltfish.ai/flows2/drafts/${playlistId}.json`;
        return {
          isValid: true,
          manifestPath,
          updatedOptions: options
        };
      }
      if ((options == null ? void 0 : options._startedFromShareLink) === true && (options == null ? void 0 : options._isGlobalShare) === true) {
        log(`[PlaylistValidator] Global share link detected. Allowing direct access to playlist '${playlistId}'`);
        const manifestPath = `https://storage.saltfish.ai/flows2/drafts/${playlistId}.json`;
        return {
          isValid: true,
          manifestPath,
          updatedOptions: options
        };
      }
      if ((options == null ? void 0 : options._startedFromShareLink) === true && (options == null ? void 0 : options._isGlobalShare) === false) {
        log(`[PlaylistValidator] Non-global share link detected. Validating against backend playlists for '${playlistId}'`);
      }
      if (false) ;
      const backendPlaylists = store.backendPlaylists;
      if (!backendPlaylists || backendPlaylists.length === 0) {
        ErrorHandler.handlePlaylistError(
          "No playlist list available from backend validation",
          {
            component: "PlaylistValidator",
            method: "validatePlaylistStart",
            playlistId,
            errorType: "playlist_backend_unavailable"
          }
        );
        return { isValid: false, error: "No backend playlists available" };
      }
      const foundPlaylist = backendPlaylists.find((p) => p.id === playlistId);
      if (!foundPlaylist) {
        ErrorHandler.handlePlaylistError(
          `Playlist ID '${playlistId}' not found in the list provided by backend validation`,
          {
            component: "PlaylistValidator",
            method: "validatePlaylistStart",
            playlistId,
            errorType: "playlist_not_found",
            additionalData: { backendPlaylists }
          }
        );
        return { isValid: false, error: "Playlist not found in backend list" };
      }
      const hasTriggers = (foundPlaylist == null ? void 0 : foundPlaylist.hasTriggers) ?? (foundPlaylist == null ? void 0 : foundPlaylist.autoStart) ?? false;
      if (hasTriggers) {
        const triggerValidation = await this.validateTriggerConditions(
          playlistId,
          foundPlaylist,
          eventManager
        );
        if (!triggerValidation.isValid) {
          return triggerValidation;
        }
      }
      let updatedOptions = options;
      if (options == null ? void 0 : options.once) {
        const onceValidation = await this.validateOnceOptionConditions(
          playlistId,
          options,
          eventManager
        );
        if (!onceValidation.isValid) {
          return onceValidation;
        }
        updatedOptions = onceValidation.updatedOptions || options;
      }
      const compatibilityValidation = await this.validateDeviceCompatibility(
        foundPlaylist,
        playlistId
      );
      if (!compatibilityValidation.isValid) {
        return compatibilityValidation;
      }
      log(`[PlaylistValidator] Playlist '${playlistId}' passed all validation checks`);
      return {
        isValid: true,
        manifestPath: foundPlaylist.path,
        updatedOptions
      };
    } catch (error2) {
      const errorMessage = error2 instanceof Error ? error2.message : "Unknown validation error";
      ErrorHandler.handlePlaylistError(
        `Playlist validation failed: ${errorMessage}`,
        {
          component: "PlaylistValidator",
          method: "validatePlaylistStart",
          playlistId,
          additionalData: { error: error2 }
        }
      );
      return { isValid: false, error: errorMessage };
    }
  }
  /**
   * Validates trigger conditions for playlists with hasTriggers enabled
   */
  async validateTriggerConditions(playlistId, foundPlaylist, eventManager) {
    var _a, _b;
    const store = getSaltfishStore();
    if (!store.user) {
      ErrorHandler.handlePlaylistError(
        "User must be identified before starting auto-start playlist",
        {
          component: "PlaylistValidator",
          method: "validateAutoStartConditions",
          playlistId,
          errorType: "playlist_user_required"
        }
      );
      return { isValid: false, error: "User identification required for triggered playlist" };
    }
    if (!store.userData) {
      await this.waitForUserData(eventManager);
    }
    const currentStore = getSaltfishStore();
    const watchedPlaylists = ((_a = currentStore.userData) == null ? void 0 : _a.watchedPlaylists) || {};
    const hasTriggers = foundPlaylist.hasTriggers ?? foundPlaylist.autoStart ?? false;
    const playlistData = watchedPlaylists[playlistId];
    const maxVisits = (_b = foundPlaylist.triggers) == null ? void 0 : _b.maxVisits;
    const visitCount = (playlistData == null ? void 0 : playlistData.visitCount) ?? ((playlistData == null ? void 0 : playlistData.status) === "completed" || (playlistData == null ? void 0 : playlistData.status) === "dismissed" ? 1 : 0);
    if (hasTriggers && maxVisits !== null && maxVisits !== void 0 && visitCount >= maxVisits) {
      info(`Playlist ${playlistId} has hasTriggers enabled with maxVisits:${maxVisits} and user has ${visitCount} visits. Skipping playlist start.`, {
        watchedPlaylists,
        triggers: foundPlaylist.triggers
      });
      return { isValid: false, error: `Playlist visit limit reached (${visitCount}/${maxVisits})` };
    }
    return { isValid: true };
  }
  /**
   * Validates "once" option conditions
   */
  async validateOnceOptionConditions(playlistId, options, eventManager) {
    var _a;
    const store = getSaltfishStore();
    if (!store.user) {
      ErrorHandler.handlePlaylistError(
        "User must be identified before starting playlist with once option",
        {
          component: "PlaylistValidator",
          method: "validateOnceOptionConditions",
          playlistId,
          errorType: "playlist_auth_required"
        }
      );
      return { isValid: false, error: "User identification required for once option" };
    }
    if (!store.userData) {
      await this.waitForUserData(eventManager);
    }
    const currentStore = getSaltfishStore();
    const watchedPlaylists = ((_a = currentStore.userData) == null ? void 0 : _a.watchedPlaylists) || {};
    const playlistData = watchedPlaylists[playlistId];
    if (playlistData && (playlistData.status === "completed" || playlistData.status === "dismissed")) {
      info(`Playlist ${playlistId} has once option enabled and has already been ${playlistData.status}. Skipping playlist start.`, {
        watchedPlaylists,
        playlistStatus: playlistData.status
      });
      return { isValid: false, error: `Playlist already ${playlistData.status} with once option` };
    } else if (playlistData && playlistData.status === "in_progress") {
      if (!(options == null ? void 0 : options.startNodeId) && playlistData.currentStepId) {
        const updatedOptions = {
          ...options,
          startNodeId: playlistData.currentStepId
        };
        return { isValid: true, updatedOptions };
      }
    }
    return { isValid: true };
  }
  /**
   * Validates device compatibility with playlist requirements
   * Uses deviceType from the playlist data (from validate-token response) instead of fetching manifest
   */
  async validateDeviceCompatibility(playlist, playlistId) {
    try {
      const deviceType = playlist.deviceType || "both";
      const { isDeviceCompatible: isDeviceCompatible2 } = await Promise.resolve().then(() => deviceDetection);
      if (!isDeviceCompatible2(deviceType)) {
        ErrorHandler.handlePlaylistError(
          `Playlist '${playlistId}' is not compatible with this device. Required: ${deviceType}`,
          {
            component: "PlaylistValidator",
            method: "validateDeviceCompatibility",
            playlistId,
            errorType: "playlist_device_incompatible",
            additionalData: {
              requiredDeviceType: deviceType,
              manifestPath: playlist.path
            }
          }
        );
        return { isValid: false, error: `Device incompatible. Required: ${deviceType}` };
      }
      log(`[PlaylistValidator] Device compatibility check passed for playlist '${playlistId}' with deviceType: ${deviceType}`);
      return { isValid: true };
    } catch (error2) {
      const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
      ErrorHandler.handlePlaylistError(
        `Device compatibility check failed: ${errorMessage}`,
        {
          component: "PlaylistValidator",
          method: "validateDeviceCompatibility",
          playlistId,
          additionalData: { error: error2, manifestPath: playlist.path }
        }
      );
      return { isValid: false, error: `Device compatibility check failed: ${errorMessage}` };
    }
  }
  /**
   * Waits for user data to be loaded with timeout
   */
  async waitForUserData(eventManager) {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        eventManager.off("userDataLoaded", handler);
        reject(new Error("Timeout waiting for user data"));
      }, 5e3);
      const handler = () => {
        clearTimeout(timeout);
        getSaltfishStore();
        resolve();
      };
      eventManager.on("userDataLoaded", handler);
    });
  }
}
class PlaylistOrchestrator {
  constructor(managers) {
    __publicField(this, "managers");
    __publicField(this, "userManagementService");
    __publicField(this, "managerOrchestrator");
    __publicField(this, "playerInitializationService");
    __publicField(this, "stateMachineActionHandler");
    this.managers = managers;
  }
  /**
   * Set the user management service for dependency
   */
  setUserManagementService(service) {
    this.userManagementService = service;
  }
  /**
   * Set the manager orchestrator for dependency
   */
  setManagerOrchestrator(orchestrator) {
    this.managerOrchestrator = orchestrator;
  }
  /**
   * Set the player initialization service for dependency
   */
  setPlayerInitializationService(service) {
    this.playerInitializationService = service;
  }
  /**
   * Set the state machine action handler for dependency
   */
  setStateMachineActionHandler(handler) {
    this.stateMachineActionHandler = handler;
  }
  /**
   * Start a playlist with given options
   */
  async startPlaylist(playlistId, options) {
    var _a, _b, _c, _d, _e;
    try {
      const needsManagerRecreation = this.isInitialized() && !this.managers.uiManager.getPlayerElement();
      if (!this.isInitialized() && this.playerInitializationService) {
        const lastConfig = this.playerInitializationService.getLastConfig();
        if (lastConfig) {
          try {
            await this.playerInitializationService.initialize(lastConfig);
            const lastUserIdentification = (_a = this.userManagementService) == null ? void 0 : _a.getLastUserIdentification();
            if (lastUserIdentification && this.userManagementService) {
              this.userManagementService.identifyUser(
                lastUserIdentification.userId,
                lastUserIdentification.userData
              );
            }
            this.resetManagers();
            if (this.stateMachineActionHandler) {
              this.stateMachineActionHandler.registerStateMachineActions();
            }
          } catch (reinitError) {
            throw ErrorHandler.handleInitializationError(
              `Failed to reinitialize player: ${reinitError instanceof Error ? reinitError.message : "Unknown error"}`,
              {
                component: "PlaylistOrchestrator",
                method: "startPlaylist",
                playlistId,
                additionalData: { reinitError }
              }
            );
          }
        }
      }
      if (needsManagerRecreation) {
        this.resetManagers();
        this.managers.transitionManager.setTriggerManager(this.managers.triggerManager);
        if (this.stateMachineActionHandler) {
          this.stateMachineActionHandler.registerStateMachineActions();
        }
      }
      const store = getSaltfishStore();
      if (!store.config) {
        const lastConfig = (_b = this.playerInitializationService) == null ? void 0 : _b.getLastConfig();
        if (!lastConfig) {
          throw ErrorHandler.createError(
            "Saltfish Player must be initialized at least once before starting a playlist",
            { component: "PlaylistOrchestrator", method: "startPlaylist", playlistId }
          );
        }
        throw ErrorHandler.createError(
          "Saltfish Player must be initialized before starting a playlist",
          { component: "PlaylistOrchestrator", method: "startPlaylist", playlistId }
        );
      }
      const isPlaylistRunning = store.manifest && (store.currentState === "playing" || store.currentState === "paused" || store.currentState === "loading" || store.currentState === "waitingForInteraction" || store.currentState === "autoplayBlocked" || store.currentState === "minimized" || store.currentState === "idleMode");
      if (isPlaylistRunning) {
        log("PlaylistOrchestrator: Starting new playlist while another is running, resetting state");
        if (this.managerOrchestrator) {
          this.managerOrchestrator.cleanupCurrentPlaylist();
        }
        store.resetForNewPlaylist();
        if (this.stateMachineActionHandler) {
          this.stateMachineActionHandler.registerStateMachineActions();
        }
      }
      if (store.currentState === "completed" || store.currentState === "closing") {
        log(`PlaylistOrchestrator: Resetting from ${store.currentState} state to start new playlist`);
        store.resetForNewPlaylist();
        if (this.stateMachineActionHandler) {
          this.stateMachineActionHandler.registerStateMachineActions();
        }
      }
      const runId = this.managers.sessionManager.startNewRun();
      log(`PlaylistOrchestrator: Starting playlist ${playlistId} with runId: ${runId}`);
      this.managers.triggerManager.markPlaylistAsTriggered(playlistId);
      const userDataLoadedPromise = (_c = this.userManagementService) == null ? void 0 : _c.getUserDataLoadedPromise();
      if (userDataLoadedPromise) {
        log("[PlaylistOrchestrator.startPlaylist] Waiting for user data to load before checking A/B test assignments");
        await userDataLoadedPromise;
      }
      if (this.userManagementService) {
        await this.userManagementService.recordABTestAttempt(playlistId);
      }
      if ((options == null ? void 0 : options._startedFromShareLink) !== true) {
        if (!this.managers.abTestManager.isPlaylistAvailable(playlistId)) {
          log(`[PlaylistOrchestrator.startPlaylist] Playlist ${playlistId} not available due to A/B test assignment`);
          return;
        }
      } else if (false) ;
      else if ((options == null ? void 0 : options._startedFromShareLink) === true) {
        log(`[PlaylistOrchestrator.startPlaylist] Share link - skipping A/B test check for playlist ${playlistId}`);
      }
      const validator = new PlaylistValidator();
      const validationResult = await validator.validatePlaylistStart({
        playlistId,
        options,
        eventManager: this.managers.eventManager
      });
      if (!validationResult.isValid) {
        return;
      }
      options = validationResult.updatedOptions || options;
      const manifestPathToLoad = validationResult.manifestPath;
      const finalOptions = options || {};
      if (finalOptions) {
        store.setPlaylistOptions(finalOptions);
        if (finalOptions.position) {
        } else {
          store.setPlaylistOptions({
            ...finalOptions,
            position: "bottom-right"
          });
        }
      } else {
        store.setPlaylistOptions({
          position: "bottom-right"
        });
      }
      log("[PlaylistOrchestrator.startPlaylist] Creating UI immediately for fast loading experience");
      this.managers.uiManager.createPlayerUI(
        this.managers.videoManager,
        this.managers.cursorManager,
        this.managers.interactionManager
      );
      if (this.managerOrchestrator) {
        this.managerOrchestrator.setupUpdaters();
      }
      this.managers.uiManager.updatePosition();
      store.sendStateMachineEvent({ type: "LOAD_MANIFEST" });
      const playlistPersistence = options == null ? void 0 : options.persistence;
      if (typeof window !== "undefined") {
        const userId = (_d = store.user) == null ? void 0 : _d.id;
        const progressFromStorage = this.managers.storageManager.getProgress(userId);
        if (progressFromStorage && progressFromStorage[playlistId]) {
          store.loadPlaylistProgress(playlistId, progressFromStorage[playlistId]);
        } else if (store.progress[playlistId]) {
          log(`PlaylistOrchestrator: Clearing stale progress for ${playlistId} from store.progress`);
          const cleanedProgress = { ...store.progress };
          delete cleanedProgress[playlistId];
          useSaltfishStore.setState((state) => {
            state.progress = cleanedProgress;
          });
        }
      }
      this.managers.cursorManager.resetFirstAnimation();
      log(`[PlaylistOrchestrator.startPlaylist] Using validated manifest path: ${manifestPathToLoad}`);
      await this.managers.playlistManager.load(manifestPathToLoad, { ...finalOptions, persistence: playlistPersistence });
      const updatedStore = getSaltfishStore();
      if (updatedStore.manifest) {
        const manifestPersistence = finalOptions.persistence ?? updatedStore.manifest.isPersistent ?? true;
        const currentOptions = updatedStore.playlistOptions || {};
        updatedStore.setPlaylistOptions({
          ...currentOptions,
          ...finalOptions,
          persistence: manifestPersistence
        });
      }
      if (finalOptions.startNodeId && updatedStore.manifest) {
        const targetStep = updatedStore.manifest.steps.find((step) => step.id === finalOptions.startNodeId);
        if (targetStep) {
          updatedStore.goToStep(finalOptions.startNodeId);
        } else {
          console.warn(`[PlaylistOrchestrator] startNodeId '${finalOptions.startNodeId}' not found in manifest steps. Starting from default step.`);
        }
      }
      if (updatedStore.manifest) {
        if (updatedStore.manifest.cursorColor) {
          this.managers.cursorManager.setColor(updatedStore.manifest.cursorColor);
        }
        if (updatedStore.manifest.cursorLabel) {
          this.managers.cursorManager.setLabel(updatedStore.manifest.cursorLabel);
        }
        const isTriggeredAutomatically = finalOptions._triggeredByTriggerManager === true;
        const isFirstStep = updatedStore.currentStepId === ((_e = updatedStore.manifest.steps[0]) == null ? void 0 : _e.id);
        if (updatedStore.manifest.idleMode && isFirstStep) {
          if (updatedStore.manifest.compactFirstStep) {
            const playerElement = this.managers.uiManager.getPlayerElement();
            playerElement == null ? void 0 : playerElement.classList.add("sf-player--compact");
            if (updatedStore.manifest.compactLabel) {
              this.managers.uiManager.showCompactLabel(updatedStore.manifest.compactLabel);
            }
          }
          store.setIdleMode();
        } else {
          store.play();
        }
      } else {
        store.play();
      }
      info(`Playlist started: ${playlistId}${updatedStore.manifest ? ` (${updatedStore.manifest.name})` : ""}`);
    } catch (error2) {
      this.managers.uiManager.hideLoading();
      const errorStore = getSaltfishStore();
      errorStore.sendStateMachineEvent({
        type: "ERROR",
        error: error2 instanceof Error ? error2 : new Error(String(error2))
      });
      ErrorHandler.handlePlaylistError(
        error2,
        {
          component: "PlaylistOrchestrator",
          method: "startPlaylist",
          playlistId,
          errorType: "playlist_load_failed"
        }
      );
    }
  }
  /**
   * Reset current playlist to initial state
   */
  resetPlaylist() {
    const store = getSaltfishStore();
    if (store.manifest) {
      store.goToStep(store.manifest.startStep);
    }
  }
  /**
   * Check if the player is initialized
   */
  isInitialized() {
    const store = getSaltfishStore();
    return !!store.config;
  }
  /**
   * Reset managers that have reset capability
   */
  resetManagers() {
    this.managers.videoManager.reset();
    this.managers.cursorManager.reset();
    this.managers.interactionManager.reset();
    this.managers.transitionManager.reset();
    this.managers.uiManager.reset();
    this.managers.stepTimeoutManager.reset();
  }
  destroy() {
  }
}
class StateMachineActionHandler {
  constructor(managers) {
    __publicField(this, "managers");
    __publicField(this, "destroyCallback", null);
    __publicField(this, "cursorAnimationListener", null);
    __publicField(this, "cursorAnimationVideoElement", null);
    __publicField(this, "cursorAnimationStepId", null);
    this.managers = managers;
  }
  /**
   * Set the destroy callback for player destruction
   */
  setDestroyCallback(callback) {
    this.destroyCallback = callback;
  }
  /**
   * Register all state machine actions with the store
   */
  registerStateMachineActions() {
    const store = getSaltfishStore();
    store.registerStateMachineActions({
      startVideoPlayback: (context) => {
        this.handleStartVideoPlayback(context);
      },
      pauseVideoPlayback: () => {
        this.handlePauseVideoPlayback();
      },
      startMutedLoopedVideo: () => {
        this.handleStartMutedLoopedVideo();
      },
      startIdleModeVideo: (context) => {
        this.handleStartIdleModeVideo(context);
      },
      trackPlaylistComplete: () => {
        this.handleTrackPlaylistComplete();
      },
      handleError: (context) => {
        this.handleError(context);
      },
      hideError: () => {
        this.handleHideError();
      },
      showLoadingState: () => {
        this.handleShowLoadingState();
      },
      hideLoadingState: () => {
        this.handleHideLoadingState();
      },
      hideVideoControls: () => {
        this.handleHideVideoControls();
      },
      showVideoControls: () => {
        this.handleShowVideoControls();
      },
      showPlayButton: () => {
        this.handleShowPlayButton();
      },
      hidePlayButton: () => {
        this.handleHidePlayButton();
      },
      enablePlayButtonProminent: () => {
        this.handleEnablePlayButtonProminent();
      },
      disablePlayButtonProminent: () => {
        this.handleDisablePlayButtonProminent();
      },
      enterCompactMode: () => {
        this.handleEnterCompactMode();
      },
      exitCompactMode: () => {
        this.handleExitCompactMode();
      },
      triggerPlaylistDismissed: () => {
        this.handleTriggerPlaylistDismissed();
      },
      scheduleDestroy: () => {
        this.handleScheduleDestroy();
      }
    });
  }
  /**
   * Gets the appropriate video URL for a step, preferring compressedUrl if available
   * Also checks for translations if a language is configured
   * Falls back to audioUrl if video URL is empty/missing
   */
  getVideoUrl(step) {
    var _a;
    const store = getSaltfishStore();
    const language = (_a = store.userData) == null ? void 0 : _a.language;
    let videoUrl;
    if (language && step.translations && step.translations[language]) {
      const translation = step.translations[language];
      videoUrl = translation.videoUrl;
    } else {
      videoUrl = step.compressedUrl || step.videoUrl;
    }
    if (!videoUrl || videoUrl.trim() === "") {
      if (step.audioUrl && step.audioUrl.trim() !== "") {
        log(`StateMachineActionHandler: Video URL missing for step ${step.id}, using audio URL as fallback`);
        return step.audioUrl;
      }
    }
    return videoUrl;
  }
  /**
   * Checks if a step is using audio fallback (when video URL is missing)
   */
  isUsingAudioFallback(step) {
    var _a;
    const store = getSaltfishStore();
    const language = (_a = store.userData) == null ? void 0 : _a.language;
    let videoUrl;
    if (language && step.translations && step.translations[language]) {
      videoUrl = step.translations[language].videoUrl;
    } else {
      videoUrl = step.compressedUrl || step.videoUrl;
    }
    return (!videoUrl || videoUrl.trim() === "") && !!(step.audioUrl && step.audioUrl.trim() !== "");
  }
  /**
   * Finds the URL of the next video in the playlist for preloading
   */
  findNextVideoUrl(currentStep) {
    const store = getSaltfishStore();
    if (!store.manifest || !currentStep) {
      return null;
    }
    if (currentStep.transitions.length > 0) {
      const defaultTransition = currentStep.transitions[0];
      if (defaultTransition.type === "url-path" || defaultTransition.type === "dom-click") {
        return null;
      }
      const nextStepId = defaultTransition.nextStep;
      const nextStep = store.manifest.steps.find((step) => step.id === nextStepId);
      if (nextStep) {
        return this.getVideoUrl(nextStep);
      }
    }
    const currentIndex = store.manifest.steps.findIndex((step) => step.id === currentStep.id);
    if (currentIndex >= 0 && currentIndex < store.manifest.steps.length - 1) {
      return this.getVideoUrl(store.manifest.steps[currentIndex + 1]);
    }
    return null;
  }
  destroy() {
    this.cleanupCursorAnimationListener();
  }
  /**
   * Cleans up the active cursor animation time listener
   * @param stepId - Optional step ID to verify we're cleaning up the right listener
   */
  cleanupCursorAnimationListener(stepId) {
    if (stepId && this.cursorAnimationStepId && stepId !== this.cursorAnimationStepId) {
      log(`StateMachineActionHandler: Skipping cleanup - stepId mismatch (requested: ${stepId}, current: ${this.cursorAnimationStepId})`);
      return;
    }
    if (this.cursorAnimationListener && this.cursorAnimationVideoElement) {
      this.cursorAnimationVideoElement.removeEventListener("timeupdate", this.cursorAnimationListener);
      this.cursorAnimationListener = null;
      this.cursorAnimationVideoElement = null;
      this.cursorAnimationStepId = null;
    }
  }
  /**
   * Schedules a cursor animation to run either immediately or at a specific video time
   * Note: Works for both video and audio-only steps (audio files use the same video element)
   */
  scheduleCursorAnimation(animation, stepId) {
    this.cleanupCursorAnimationListener();
    let showAtSeconds = animation.showAtSeconds ?? 0;
    if (typeof showAtSeconds !== "number" || !isFinite(showAtSeconds)) {
      showAtSeconds = 0;
    }
    if (showAtSeconds < 0) {
      showAtSeconds = 0;
    }
    if (showAtSeconds <= 0) {
      this.managers.cursorManager.animate(animation);
    } else {
      const videoElement = this.managers.videoManager.getVideoElement();
      if (!videoElement) {
        return;
      }
      let animationTriggered = false;
      let warningLogged = false;
      const timeUpdateHandler = () => {
        if (animationTriggered) {
          return;
        }
        const currentTime = videoElement.currentTime;
        const duration = videoElement.duration;
        if (duration && !isNaN(duration) && showAtSeconds > duration && !warningLogged) {
          warningLogged = true;
          animationTriggered = true;
          this.cleanupCursorAnimationListener();
          this.managers.cursorManager.animate(animation);
          return;
        }
        if (currentTime >= showAtSeconds) {
          animationTriggered = true;
          this.cleanupCursorAnimationListener();
          this.managers.cursorManager.animate(animation);
        }
      };
      const endedHandler = () => {
        if (!animationTriggered) {
          const store = getSaltfishStore();
          if (store.currentStepId !== stepId) {
            log(`StateMachineActionHandler: Video ended but step changed (was ${stepId}, now ${store.currentStepId}). Not triggering cursor animation.`);
            this.cleanupCursorAnimationListener(stepId);
            return;
          }
          videoElement.duration;
          animationTriggered = true;
          this.managers.cursorManager.animate(animation);
          this.cleanupCursorAnimationListener();
        }
      };
      this.cursorAnimationListener = () => {
        timeUpdateHandler();
      };
      this.cursorAnimationVideoElement = videoElement;
      this.cursorAnimationStepId = stepId;
      videoElement.addEventListener("timeupdate", this.cursorAnimationListener);
      videoElement.addEventListener("ended", endedHandler, { once: true });
    }
  }
  /**
   * Validates URL requirement for a specific step with retry logic
   * @param step - The step to validate
   * @returns Promise<boolean> - true if validation passes, false otherwise
   */
  async validateStepUrlRequirement(step) {
    if (!step.urlRequirement) {
      return true;
    }
    log(`StateMachineActionHandler: Validating URL requirement for step ${step.id} with retry logic`);
    this.managers.transitionManager.startStateMachineValidation();
    try {
      const isValid = await validateUrlRequirementWithRetry(step.urlRequirement);
      if (!isValid) {
        log(`StateMachineActionHandler: URL requirement validation failed for step '${step.id}'`);
        log(`StateMachineActionHandler: Exiting playlist and closing player`);
        const saltfishPlayer = window._saltfishPlayer;
        if (saltfishPlayer && typeof saltfishPlayer.destroy === "function") {
          saltfishPlayer.destroy();
        } else {
          log("StateMachineActionHandler: Warning - Could not find SaltfishPlayer instance to destroy");
        }
      }
      return isValid;
    } finally {
      this.managers.transitionManager.endStateMachineValidation();
    }
  }
  // Private action handler methods
  async handleStartVideoPlayback(context) {
    if (!context.currentStep) {
      return;
    }
    const currentStep = context.currentStep;
    if (currentStep.urlRequirement) {
      log(`StateMachineActionHandler: Validating URL requirement for step ${currentStep.id}`);
      const isValid = await this.validateStepUrlRequirement(currentStep);
      if (!isValid) {
        return;
      }
    }
    this.managers.uiManager.updatePosition();
    this.managers.uiManager.showPlayer();
    const videoUrl = this.getVideoUrl(currentStep);
    const isAudioFallback = this.isUsingAudioFallback(currentStep);
    if (isAudioFallback) {
      const store = getSaltfishStore();
      const manifest = store.manifest;
      const posterUrl = currentStep.gifUrl;
      const avatarThumbnailUrl = manifest == null ? void 0 : manifest.avatarThumbnailUrl;
      this.managers.videoManager.showAudioFallbackOverlay(posterUrl, avatarThumbnailUrl);
    } else {
      this.managers.videoManager.hideAudioFallbackOverlay();
    }
    try {
      this.managers.interactionManager.clearButtons();
      if (currentStep.buttons) {
        this.managers.interactionManager.createButtons(currentStep.buttons);
      }
      log(`StateMachineActionHandler: Step has cursor animations: ${!!(currentStep.cursorAnimations && currentStep.cursorAnimations.length > 0)}`);
      const hasSpecialTransitions = currentStep.buttons && currentStep.buttons.length > 0 || currentStep.transitions.some(
        (t) => t.type === "dom-click" || t.type === "url-path" || t.type === "dom-element-visible"
      );
      const completionPolicy = hasSpecialTransitions ? "manual" : "auto";
      if (hasSpecialTransitions) {
        log("StateMachineActionHandler: Setting up transitions immediately for step with special transitions");
        this.managers.transitionManager.setupTransitions(currentStep, false, true);
      }
      this.managers.videoManager.setCompletionPolicy(completionPolicy, () => {
        var _a;
        const store = getSaltfishStore();
        if (!hasSpecialTransitions) {
          const timeoutTransition = currentStep.transitions.find((t) => t.type === "timeout");
          const hasNonZeroTimeout = timeoutTransition && timeoutTransition.timeout && timeoutTransition.timeout > 0;
          if (hasNonZeroTimeout) {
            log(`StateMachineActionHandler: Setting up transitions after video ended with ${timeoutTransition.timeout}ms delay`);
            this.managers.transitionManager.setupTransitions(currentStep, false);
          } else {
            log("StateMachineActionHandler: Setting up transitions after video ended (immediate)");
            this.managers.transitionManager.setupTransitions(currentStep, true);
          }
          const hasValidNextSteps = currentStep.transitions.some((transition) => {
            return store.manifest.steps.some((s) => s.id === transition.nextStep);
          });
          if (!hasValidNextSteps) {
            log("StateMachineActionHandler: No valid next steps found, completing playlist");
            store.sendStateMachineEvent({
              type: "COMPLETE_PLAYLIST"
            });
          }
        } else {
          const timeoutTransition = currentStep.transitions.find((t) => t.type === "timeout");
          const hasStepTransitionButtons = (_a = currentStep.buttons) == null ? void 0 : _a.some(
            (button) => button.action.type === "goto" || button.action.type === "next"
          );
          if (timeoutTransition && !hasStepTransitionButtons) {
            const timeout = timeoutTransition.timeout || 0;
            log(`StateMachineActionHandler: Video ended, setting up timeout transition to ${timeoutTransition.nextStep} with ${timeout}ms delay`);
            setTimeout(() => {
              var _a2;
              const currentStore = getSaltfishStore();
              if (currentStore.currentStepId === currentStep.id && (currentStore.currentState === "waitingForInteraction" || currentStore.currentState === "playing")) {
                log(`StateMachineActionHandler: Timeout expired (${timeout}ms), transitioning to ${timeoutTransition.nextStep}`);
                (_a2 = currentStore.goToStep) == null ? void 0 : _a2.call(currentStore, timeoutTransition.nextStep);
              } else {
                log(`StateMachineActionHandler: Timeout cancelled - step changed or state is ${currentStore.currentState}`);
              }
            }, timeout);
            store.sendStateMachineEvent({
              type: "VIDEO_FINISHED_WAIT",
              step: currentStep
            });
          } else {
            if (hasStepTransitionButtons) {
              log("StateMachineActionHandler: Video ended, has step-transition button (goto/next) - waiting for user to click button");
            } else {
              log("StateMachineActionHandler: Video ended, waiting for user interaction");
            }
            store.sendStateMachineEvent({
              type: "VIDEO_FINISHED_WAIT",
              step: currentStep
            });
          }
        }
      });
      log("StateMachineActionHandler: Starting async video load");
      const loadTranscriptForStep = () => {
        var _a, _b;
        const store = getSaltfishStore();
        const captionsEnabled = ((_a = store.manifest) == null ? void 0 : _a.captions) ?? true;
        const language = (_b = store.userData) == null ? void 0 : _b.language;
        let transcript = currentStep.transcript;
        if (language && currentStep.translations && currentStep.translations[language]) {
          const translatedTranscript = currentStep.translations[language].transcript;
          if (translatedTranscript) {
            transcript = translatedTranscript;
            log(`StateMachineActionHandler: Loading translated transcript for step ${currentStep.id} in language ${language}`);
          } else {
            log(`StateMachineActionHandler: No translated transcript found for language ${language}, using default`);
          }
        } else if (language) {
          log(`StateMachineActionHandler: No translation available for language ${language}, using default transcript`);
        }
        if (transcript) {
          log(`StateMachineActionHandler: Loading transcript for step ${currentStep.id}, initially visible: ${captionsEnabled}`);
          this.managers.videoManager.loadTranscript(transcript, captionsEnabled);
        } else {
          log(`StateMachineActionHandler: No transcript available for step ${currentStep.id}`);
          this.managers.videoManager.loadTranscript(null, true);
        }
      };
      this.managers.videoManager.loadVideo(videoUrl).then(() => {
        log("StateMachineActionHandler: Video loaded successfully, playing");
        this.managers.uiManager.hideError();
        loadTranscriptForStep();
        if (currentStep.cursorAnimations && currentStep.cursorAnimations.length > 0) {
          log(`StateMachineActionHandler: Setting cursor visibility and scheduling animation for step ${currentStep.id}`);
          this.managers.cursorManager.setShouldShowCursor(true);
          this.scheduleCursorAnimation(currentStep.cursorAnimations[0], currentStep.id);
        } else {
          log(`StateMachineActionHandler: Setting cursor visibility to false for step ${currentStep.id} - step has no cursor animations`);
          this.managers.cursorManager.setShouldShowCursor(false);
        }
        if (isAudioFallback) {
          this.managers.videoManager.startAudioVisualization();
        } else {
          this.managers.videoManager.initializeAudioForVideo();
        }
        this.managers.videoManager.play();
        const nextVideoUrl = this.findNextVideoUrl(currentStep);
        if (nextVideoUrl) {
          log(`StateMachineActionHandler: Preloading next video: ${nextVideoUrl}`);
          this.managers.videoManager.preloadNextVideo(nextVideoUrl);
        }
      }).catch((error2) => {
        var _a;
        log(`StateMachineActionHandler: Error loading video: ${error2}`);
        this.managers.uiManager.showError(
          error2 instanceof Error ? error2 : new Error(`Failed to load video: ${error2}`),
          "video"
        );
        const store = getSaltfishStore();
        const errorObj = error2 instanceof Error ? error2 : new Error(`Failed to load video: ${error2}`);
        const enrichedError = error2;
        this.managers.eventManager.trigger("error", {
          timestamp: Date.now(),
          playlistId: ((_a = store.manifest) == null ? void 0 : _a.id) || void 0,
          stepId: currentStep.id,
          error: errorObj,
          errorType: "video",
          videoUrl: enrichedError == null ? void 0 : enrichedError.videoUrl,
          mediaErrorCode: enrichedError == null ? void 0 : enrichedError.mediaErrorCode,
          mediaErrorMessage: enrichedError == null ? void 0 : enrichedError.mediaErrorMessage,
          failureReason: enrichedError == null ? void 0 : enrichedError.failureReason
        });
      });
    } catch (error2) {
    }
  }
  handlePauseVideoPlayback() {
    this.managers.videoManager.pause();
  }
  handleStartMutedLoopedVideo() {
    const videoElement = this.managers.videoManager.getVideoElement();
    if (videoElement) {
      videoElement.muted = true;
      videoElement.loop = true;
      videoElement.play().catch(() => {
      });
    }
  }
  handleStartIdleModeVideo(context) {
    if (!context.currentStep) {
      return;
    }
    const currentStep = context.currentStep;
    const videoUrl = this.getVideoUrl(currentStep);
    const isAudioFallback = this.isUsingAudioFallback(currentStep);
    this.managers.uiManager.showPlayer();
    if (isAudioFallback) {
      const store = getSaltfishStore();
      const manifest = store.manifest;
      const posterUrl = currentStep.gifUrl;
      const avatarThumbnailUrl = manifest == null ? void 0 : manifest.avatarThumbnailUrl;
      this.managers.videoManager.showAudioFallbackOverlay(posterUrl, avatarThumbnailUrl);
    } else {
      this.managers.videoManager.hideAudioFallbackOverlay();
    }
    this.managers.videoManager.loadVideo(videoUrl).then(() => {
      this.managers.uiManager.hideError();
      if (isAudioFallback) {
        this.managers.videoManager.startAudioVisualization();
      }
      const videoElement = this.managers.videoManager.getVideoElement();
      if (videoElement) {
        videoElement.muted = true;
        videoElement.loop = true;
        videoElement.play().catch(() => {
        });
      }
    }).catch((error2) => {
    });
  }
  handleTrackPlaylistComplete() {
    var _a, _b;
    const store = getSaltfishStore();
    if (store.manifest && this.managers.eventManager) {
      const playlistId = store.manifest.id;
      this.managers.eventManager.trigger("playlistEnded", {
        timestamp: Date.now(),
        playlist: {
          id: playlistId,
          title: store.manifest.name
        }
      });
      const playlistPersistence = ((_a = store.playlistOptions) == null ? void 0 : _a.persistence) ?? true;
      if (playlistPersistence && store.progress[playlistId]) {
        const updatedProgress = { ...store.progress };
        delete updatedProgress[playlistId];
        const userId = (_b = store.user) == null ? void 0 : _b.id;
        this.managers.storageManager.setProgress(updatedProgress, userId);
      }
    }
  }
  handleError(context) {
    var _a, _b;
    log(`StateMachineActionHandler: Handling error: ${(_a = context.error) == null ? void 0 : _a.message}`);
    if (context.error && this.managers.uiManager.getPlayerElement()) {
      this.managers.uiManager.showError(context.error, "player");
      const store = getSaltfishStore();
      this.managers.eventManager.trigger("error", {
        timestamp: Date.now(),
        playlistId: ((_b = store.manifest) == null ? void 0 : _b.id) || void 0,
        stepId: store.currentStepId || void 0,
        error: context.error,
        errorType: "player"
      });
    }
  }
  handleHideError() {
    this.managers.uiManager.hideError();
  }
  handleShowLoadingState() {
    this.managers.uiManager.showLoading("Loading...");
  }
  handleHideLoadingState() {
    this.managers.uiManager.hideLoading();
  }
  handleHideVideoControls() {
    this.managers.videoManager.hideProgressBar();
    this.managers.videoManager.hideMuteButton();
  }
  handleShowVideoControls() {
    this.managers.videoManager.showProgressBar();
    this.managers.videoManager.showMuteButton();
  }
  handleShowPlayButton() {
    this.managers.uiManager.updatePlayPauseButton("paused");
  }
  handleHidePlayButton() {
    this.managers.uiManager.updatePlayPauseButton("playing");
  }
  handleEnablePlayButtonProminent() {
    this.managers.uiManager.enablePlayButtonProminent();
  }
  handleDisablePlayButtonProminent() {
    this.managers.uiManager.disablePlayButtonProminent();
  }
  handleEnterCompactMode() {
    var _a, _b, _c;
    const store = getSaltfishStore();
    const isFirstStep = store.manifest && store.currentStepId === ((_a = store.manifest.steps[0]) == null ? void 0 : _a.id);
    const shouldBeCompact = isFirstStep && ((_b = store.manifest) == null ? void 0 : _b.compactFirstStep);
    if (shouldBeCompact) {
      const playerElement = this.managers.uiManager.getPlayerElement();
      playerElement == null ? void 0 : playerElement.classList.add("sf-player--compact");
      if ((_c = store.manifest) == null ? void 0 : _c.compactLabel) {
        this.managers.uiManager.showCompactLabel(store.manifest.compactLabel);
      }
    }
  }
  handleExitCompactMode() {
    const playerElement = this.managers.uiManager.getPlayerElement();
    playerElement == null ? void 0 : playerElement.classList.remove("sf-player--compact");
    this.managers.uiManager.hideCompactLabel();
  }
  handleTriggerPlaylistDismissed() {
    const store = getSaltfishStore();
    if (store.manifest && this.managers.eventManager) {
      this.managers.eventManager.trigger("playlistDismissed", {
        timestamp: Date.now(),
        playlist: {
          id: store.manifest.id,
          title: store.manifest.name
        }
      });
    }
  }
  handleScheduleDestroy() {
    setTimeout(() => {
      if (this.destroyCallback) {
        this.destroyCallback();
      }
    }, 0);
  }
}
function setupUIUpdater(playerElement, cursorManager) {
  let minimizeButton = null;
  const findMinimizeButton = () => {
    minimizeButton = playerElement.querySelector(".sf-player__minimize-button");
  };
  findMinimizeButton();
  let prevState = {
    currentState: "",
    isMinimized: false,
    currentStepId: null
  };
  const unsubscribe = useSaltfishStore.subscribe(
    (state) => {
      if (!minimizeButton) {
        findMinimizeButton();
      }
      if (state.currentState !== prevState.currentState) {
        updateStateClass(state.currentState);
      }
      if (state.isMinimized !== prevState.isMinimized) {
        updateMinimizeState(state.isMinimized, state.currentStepId, state.manifest);
        updateMinimizeButtonIcon(state.isMinimized);
      }
      if (state.currentStepId !== prevState.currentStepId) {
        if (!state.isMinimized) {
          updateCursorForStep(state.currentStepId, state.manifest);
        }
      }
      prevState = {
        currentState: state.currentState,
        isMinimized: state.isMinimized,
        currentStepId: state.currentStepId
      };
    }
  );
  function updateStateClass(currentState) {
    const stateClasses = [
      "sf-player--idle",
      "sf-player--loading",
      "sf-player--playing",
      "sf-player--paused",
      "sf-player--waitingForInteraction",
      "sf-player--autoplayBlocked",
      "sf-player--idleMode",
      "sf-player--error",
      "sf-player--completed",
      "sf-player--completedWaitingForInteraction"
    ];
    stateClasses.forEach((cls) => {
      playerElement.classList.remove(cls);
    });
    playerElement.classList.add(`sf-player--${currentState}`);
  }
  function updateMinimizeState(isMinimized, currentStepId, manifest) {
    if (isMinimized) {
      playerElement.classList.add("sf-player--minimized");
      if (cursorManager) {
        cursorManager.setShouldShowCursor(false);
      }
    } else {
      playerElement.classList.remove("sf-player--minimized");
      updateCursorForStep(currentStepId, manifest);
    }
  }
  function updateCursorForStep(currentStepId, manifest) {
    var _a;
    if (!cursorManager) {
      return;
    }
    const currentStep = (_a = manifest == null ? void 0 : manifest.steps) == null ? void 0 : _a.find((step) => step.id === currentStepId);
    if (currentStep && currentStep.cursorAnimations && currentStep.cursorAnimations.length > 0) {
      cursorManager.setShouldShowCursor(true);
    }
  }
  function updateMinimizeButtonIcon(isMinimized) {
    if (!minimizeButton) {
      return;
    }
    if (isMinimized) {
      minimizeButton.innerHTML = `
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
          <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>
      `;
    } else {
      minimizeButton.innerHTML = `
        <svg width="20" height="20" viewBox="0 0 256 256" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
          <path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path>
        </svg>
      `;
    }
  }
  return unsubscribe;
}
function setupEventUpdater(eventManager) {
  let prevCurrentState = null;
  let prevPreviousState = null;
  let prevIsMinimized = null;
  let prevStepId = null;
  let pendingStartEvents = { playlistData: null, stepData: null };
  let autoplayConfirmationTimer = null;
  const firePendingEvents = () => {
    if (autoplayConfirmationTimer) {
      clearTimeout(autoplayConfirmationTimer);
      autoplayConfirmationTimer = null;
    }
    if (pendingStartEvents.playlistData) {
      eventManager.trigger("playlistStarted", {
        timestamp: Date.now(),
        playlist: pendingStartEvents.playlistData
      });
    }
    if (pendingStartEvents.stepData) {
      eventManager.trigger("stepStarted", {
        timestamp: Date.now(),
        step: { id: pendingStartEvents.stepData.id, title: pendingStartEvents.stepData.title },
        playlist: { id: pendingStartEvents.stepData.playlistId, title: pendingStartEvents.stepData.playlistTitle }
      });
    }
    pendingStartEvents = { playlistData: null, stepData: null };
  };
  const startAutoplayConfirmationTimer = () => {
    if (autoplayConfirmationTimer) {
      clearTimeout(autoplayConfirmationTimer);
    }
    autoplayConfirmationTimer = setTimeout(() => {
      if (pendingStartEvents.playlistData || pendingStartEvents.stepData) {
        firePendingEvents();
      }
      autoplayConfirmationTimer = null;
    }, 500);
  };
  const unsubscribe = useSaltfishStore.subscribe(
    (state) => {
      var _a;
      if (prevCurrentState === state.currentState && prevIsMinimized === state.isMinimized && prevStepId === state.currentStepId) {
        return;
      }
      const actualPrevState = prevCurrentState;
      const actualPrevPreviousState = prevPreviousState;
      const actualPrevMinimized = prevIsMinimized;
      const actualPrevStepId = prevStepId;
      prevPreviousState = prevCurrentState;
      prevCurrentState = state.currentState;
      prevIsMinimized = state.isMinimized;
      prevStepId = state.currentStepId;
      const eventData = {
        prevPreviousState: actualPrevPreviousState,
        previousState: actualPrevState,
        currentState: state.currentState,
        currentStepId: state.currentStepId,
        isMinimized: state.isMinimized
      };
      log(`EventUpdater: Processing state change from '${eventData.previousState}' to '${eventData.currentState}'`, {
        manifestId: (_a = state.manifest) == null ? void 0 : _a.id
      });
      if (eventData.previousState === "playing") {
        if (eventData.currentState === "autoplayBlocked" || eventData.currentState === "idleMode") {
          if (autoplayConfirmationTimer) {
            clearTimeout(autoplayConfirmationTimer);
            autoplayConfirmationTimer = null;
          }
          if (pendingStartEvents.playlistData || pendingStartEvents.stepData) {
            pendingStartEvents = { playlistData: null, stepData: null };
          }
        } else if (pendingStartEvents.playlistData || pendingStartEvents.stepData) {
          firePendingEvents();
        }
      }
      handleStateTransitionEvents(eventData, state, eventManager, pendingStartEvents);
      handleMinimizeEvents(eventData, state, eventManager, actualPrevMinimized);
      handleStepEvents(eventData, state, eventManager, actualPrevStepId, pendingStartEvents);
      handleErrorEvents(eventData, state, eventManager);
      if ((pendingStartEvents.playlistData || pendingStartEvents.stepData) && !autoplayConfirmationTimer) {
        startAutoplayConfirmationTimer();
      }
    }
  );
  return unsubscribe;
}
function handleStateTransitionEvents(eventData, store, eventManager, pendingStartEvents) {
  const { prevPreviousState, previousState, currentState, currentStepId } = eventData;
  const isNormalPlaylistStart = currentState === "playing" && previousState === "paused" && prevPreviousState === "loading";
  const isUserInitiatedPlaylistStart = currentState === "playing" && (previousState === "autoplayBlocked" || previousState === "idleMode");
  if (store.manifest && currentStepId) {
    const isStartingNode = currentStepId === store.manifest.startStep;
    if (isStartingNode) {
      if (isUserInitiatedPlaylistStart) {
        log(`EventUpdater: Triggering playlistStarted event for ${store.manifest.id} (user-initiated from ${previousState})`);
        eventManager.trigger("playlistStarted", {
          timestamp: Date.now(),
          playlist: {
            id: store.manifest.id,
            title: store.manifest.name
          }
        });
        return;
      } else if (isNormalPlaylistStart) {
        log(`EventUpdater: Deferring playlistStarted event for ${store.manifest.id} (pending autoplay confirmation)`);
        pendingStartEvents.playlistData = {
          id: store.manifest.id,
          title: store.manifest.name
        };
        return;
      }
    }
  }
  if (previousState === "paused" && currentState === "playing") {
    eventManager.trigger("playerResumed", {
      timestamp: Date.now(),
      previousState,
      currentState
    });
  } else if (previousState === "playing" && currentState === "paused") {
    eventManager.trigger("playerPaused", {
      timestamp: Date.now(),
      previousState,
      currentState
    });
  }
}
function handleMinimizeEvents(eventData, _store, eventManager, prevIsMinimized) {
  const { previousState, currentState, isMinimized } = eventData;
  const wasPreviouslyMinimized = prevIsMinimized || false;
  if (!isMinimized && wasPreviouslyMinimized) {
    eventManager.trigger("playerMaximized", {
      timestamp: Date.now(),
      previousState,
      currentState
    });
  } else if (isMinimized && !wasPreviouslyMinimized) {
    eventManager.trigger("playerMinimized", {
      timestamp: Date.now(),
      previousState,
      currentState
    });
  }
}
function handleStepEvents(eventData, store, eventManager, prevStepId, pendingStartEvents) {
  var _a, _b, _c;
  const { prevPreviousState, previousState, currentStepId, currentState } = eventData;
  const currentStep = store.currentStepId ? (((_a = store.manifest) == null ? void 0 : _a.steps) || []).find((s) => s.id === store.currentStepId) : null;
  log("EventUpdater.handleStepEvents: Processing step events", {
    currentStep: currentStep == null ? void 0 : currentStep.id,
    manifestId: (_b = store.manifest) == null ? void 0 : _b.id,
    hasManifest: !!store.manifest
  });
  const shouldTriggerStepEnded = prevStepId && store.manifest && (prevStepId !== currentStepId || currentState === "waitingForInteraction" || currentState === "completed");
  log("EventUpdater.handleStepEvents: Step ended check", {
    hasManifest: !!store.manifest
  });
  if (shouldTriggerStepEnded) {
    const prevStep = (((_c = store.manifest) == null ? void 0 : _c.steps) || []).find((s) => s.id === prevStepId);
    log("EventUpdater.handleStepEvents: Found previous step", {
      prevStep: prevStep == null ? void 0 : prevStep.id
    });
    if (prevStep) {
      log(`EventUpdater.handleStepEvents: Triggering stepEnded for ${prevStep.id}`);
      eventManager.trigger("stepEnded", {
        timestamp: Date.now(),
        step: {
          id: prevStep.id,
          title: prevStep.title || prevStep.id
        },
        playlist: {
          id: store.manifest.id,
          title: store.manifest.name
        }
      });
    }
  }
  const isStepChange = currentStepId !== prevStepId && currentState === "playing";
  const isUserInitiatedStepStart = (previousState === "autoplayBlocked" || previousState === "idleMode") && currentState === "playing" && prevStepId === currentStepId;
  const isFirstPlayOfPlaylist = prevPreviousState === "loading" && previousState === "paused" && currentState === "playing" && currentStepId;
  const isStartingNode = store.manifest && currentStepId === store.manifest.startStep;
  const isFirstStepAutoplayAttempt = isStartingNode && isFirstPlayOfPlaylist;
  const isNonStartingNodeFirstPlay = !isStartingNode && isFirstPlayOfPlaylist;
  log("EventUpdater.handleStepEvents: Step started check", {
    currentStep: currentStep == null ? void 0 : currentStep.id,
    hasManifest: !!store.manifest
  });
  if (currentStep && store.manifest) {
    if (isStepChange || isUserInitiatedStepStart || isNonStartingNodeFirstPlay) {
      log(`EventUpdater: Triggering stepStarted for ${currentStep.id}`);
      eventManager.trigger("stepStarted", {
        timestamp: Date.now(),
        step: {
          id: currentStep.id,
          title: currentStep.title || currentStep.id
        },
        playlist: {
          id: store.manifest.id,
          title: store.manifest.name
        }
      });
    } else if (isFirstStepAutoplayAttempt) {
      log(`EventUpdater: Deferring stepStarted for ${currentStep.id} (pending autoplay confirmation)`);
      pendingStartEvents.stepData = {
        id: currentStep.id,
        title: currentStep.title || currentStep.id,
        playlistId: store.manifest.id,
        playlistTitle: store.manifest.name
      };
    }
  }
}
function handleErrorEvents(eventData, store, eventManager) {
  var _a;
  const { currentState, previousState } = eventData;
  if (currentState === "error" && previousState !== "error" && store.error) {
    eventManager.trigger("error", {
      timestamp: Date.now(),
      playlistId: (_a = store.manifest) == null ? void 0 : _a.id,
      stepId: store.currentStepId,
      error: store.error,
      errorType: "state"
    });
  }
}
function resetEventUpdater() {
}
const _ManagerOrchestrator = class _ManagerOrchestrator {
  constructor(managers) {
    __publicField(this, "managers");
    // Store updater unsubscribe functions
    __publicField(this, "uiUpdaterUnsubscribe", null);
    __publicField(this, "eventUpdaterUnsubscribe", null);
    __publicField(this, "cursorAnimationListener", null);
    __publicField(this, "cursorAnimationVideoElement", null);
    __publicField(this, "cursorAnimationStepId", null);
    // Initialization state
    __publicField(this, "isInitialized", false);
    this.managers = managers;
  }
  /**
   * Set initialization state
   */
  setInitialized(initialized) {
    this.isInitialized = initialized;
  }
  /**
   * Set up UI and event updaters
   */
  setupUpdaters() {
    if (this.uiUpdaterUnsubscribe) {
      this.uiUpdaterUnsubscribe();
    }
    if (this.eventUpdaterUnsubscribe) {
      this.eventUpdaterUnsubscribe();
    }
    const playerElement = this.managers.uiManager.getPlayerElement();
    if (playerElement) {
      this.uiUpdaterUnsubscribe = setupUIUpdater(playerElement, this.managers.cursorManager);
    }
    this.eventUpdaterUnsubscribe = setupEventUpdater(this.managers.eventManager);
    const stepTimeoutUnsubscribe = useSaltfishStore.subscribe(
      (state) => {
        this.managers.stepTimeoutManager.update({
          currentState: state.currentState,
          currentStepId: state.currentStepId,
          isMinimized: state.isMinimized,
          previousState: void 0
          // StepTimeoutManager doesn't need previous state
        });
      }
    );
    const originalEventUnsubscribe = this.eventUpdaterUnsubscribe;
    this.eventUpdaterUnsubscribe = () => {
      if (originalEventUnsubscribe) {
        originalEventUnsubscribe();
      }
      stepTimeoutUnsubscribe();
    };
  }
  /**
   * Cleans up the active cursor animation time listener
   * @param stepId - Optional step ID to verify we're cleaning up the right listener
   */
  cleanupCursorAnimationListener(stepId) {
    if (stepId && this.cursorAnimationStepId && stepId !== this.cursorAnimationStepId) {
      log(`ManagerOrchestrator: Skipping cleanup - stepId mismatch (requested: ${stepId}, current: ${this.cursorAnimationStepId})`);
      return;
    }
    if (this.cursorAnimationListener && this.cursorAnimationVideoElement) {
      this.cursorAnimationVideoElement.removeEventListener("timeupdate", this.cursorAnimationListener);
      this.cursorAnimationListener = null;
      this.cursorAnimationVideoElement = null;
      this.cursorAnimationStepId = null;
    }
  }
  /**
   * Schedules a cursor animation to run either immediately or at a specific video time
   * Note: Works for both video and audio-only steps (audio files use the same video element)
   */
  scheduleCursorAnimation(animation, stepId) {
    this.cleanupCursorAnimationListener();
    let showAtSeconds = animation.showAtSeconds ?? 0;
    if (typeof showAtSeconds !== "number" || !isFinite(showAtSeconds)) {
      showAtSeconds = 0;
    }
    if (showAtSeconds < 0) {
      showAtSeconds = 0;
    }
    if (showAtSeconds <= 0) {
      this.managers.cursorManager.animate(animation);
    } else {
      const videoElement = this.managers.videoManager.getVideoElement();
      if (!videoElement) {
        return;
      }
      let animationTriggered = false;
      let warningLogged = false;
      const timeUpdateHandler = () => {
        if (animationTriggered) {
          return;
        }
        const currentTime = videoElement.currentTime;
        const duration = videoElement.duration;
        if (duration && !isNaN(duration) && showAtSeconds > duration && !warningLogged) {
          warningLogged = true;
          animationTriggered = true;
          this.cleanupCursorAnimationListener();
          this.managers.cursorManager.animate(animation);
          return;
        }
        if (currentTime >= showAtSeconds) {
          animationTriggered = true;
          this.cleanupCursorAnimationListener();
          this.managers.cursorManager.animate(animation);
        }
      };
      const endedHandler = () => {
        if (!animationTriggered) {
          const store = getSaltfishStore();
          if (store.currentStepId !== stepId) {
            log(`ManagerOrchestrator: Video ended but step changed (was ${stepId}, now ${store.currentStepId}). Not triggering cursor animation.`);
            this.cleanupCursorAnimationListener(stepId);
            return;
          }
          videoElement.duration;
          animationTriggered = true;
          this.managers.cursorManager.animate(animation);
          this.cleanupCursorAnimationListener();
        }
      };
      this.cursorAnimationListener = () => {
        timeUpdateHandler();
      };
      this.cursorAnimationVideoElement = videoElement;
      this.cursorAnimationStepId = stepId;
      videoElement.addEventListener("timeupdate", this.cursorAnimationListener);
      videoElement.addEventListener("ended", endedHandler, { once: true });
    }
  }
  /**
   * Handle store state changes
   */
  handleStoreChanges() {
    if (!this.isInitialized) {
      return;
    }
    const store = getSaltfishStore();
    if (store.currentState === _ManagerOrchestrator.prevState.currentState && store.currentStepId === _ManagerOrchestrator.prevState.currentStepId && store.isMinimized === _ManagerOrchestrator.prevState.isMinimized) {
      return;
    }
    log("ManagerOrchestrator: Store state changed", {
      prevState: _ManagerOrchestrator.prevState.currentState,
      newState: store.currentState,
      prevStepId: _ManagerOrchestrator.prevState.currentStepId,
      newStepId: store.currentStepId,
      prevMinimized: _ManagerOrchestrator.prevState.isMinimized,
      newMinimized: store.isMinimized
    });
    if (this.managers.uiManager.getPlayerRoot() && this.managers.uiManager.getPlayerElement()) {
      this.managers.uiManager.updatePosition();
    }
    this.managers.videoManager.transcriptManager.handleStateChange(store.currentState);
    if (store.currentState === "autoplayBlocked") {
      this.managers.cursorManager.stopAnimation();
      this.managers.cursorManager.setShouldShowCursor(false);
    } else if (store.currentState === "playing" && _ManagerOrchestrator.prevState.currentState === "autoplayBlocked") {
      const manifest = store.manifest;
      const currentStep = manifest == null ? void 0 : manifest.steps.find((step) => step.id === store.currentStepId);
      if ((currentStep == null ? void 0 : currentStep.cursorAnimations) && currentStep.cursorAnimations.length > 0) {
        this.managers.cursorManager.setShouldShowCursor(true);
        this.scheduleCursorAnimation(currentStep.cursorAnimations[0], store.currentStepId || "unknown");
      }
    } else if (store.currentState === "completed" || store.currentState === "closing") {
      this.cleanupPlaylist();
    }
    if (store.isMinimized !== _ManagerOrchestrator.prevState.isMinimized) {
      this.managers.uiManager.handleMinimizeStateChange(store.isMinimized);
    }
    _ManagerOrchestrator.prevState = {
      currentState: store.currentState,
      currentStepId: store.currentStepId,
      isMinimized: store.isMinimized
    };
  }
  /**
   * Clean up current playlist state
   */
  cleanupCurrentPlaylist() {
    try {
      this.cleanupCursorAnimationListener();
      if (this.eventUpdaterUnsubscribe) {
        this.eventUpdaterUnsubscribe();
        this.eventUpdaterUnsubscribe = null;
      }
      resetEventUpdater();
      if (this.managers.stepTimeoutManager) {
        this.managers.stepTimeoutManager.reset();
      }
      if (this.managers.videoManager) {
        this.managers.videoManager.pause();
        this.managers.videoManager.reset();
      }
      if (this.managers.cursorManager) {
        this.managers.cursorManager.stopAnimation();
        this.managers.cursorManager.setShouldShowCursor(false);
      }
      if (this.managers.transitionManager) {
        this.managers.transitionManager.cleanupTransitions();
      }
      if (this.managers.interactionManager) {
        this.managers.interactionManager.clearButtons();
      }
      this.managers.uiManager.reset();
      log("ManagerOrchestrator: Current playlist cleanup completed");
    } catch (error2) {
      ErrorHandler.handleCleanupError(
        error2,
        {
          component: "ManagerOrchestrator",
          method: "cleanupCurrentPlaylist"
        }
      );
    }
  }
  /**
   * Clean up playlist while preserving trigger monitoring
   */
  cleanupPlaylist() {
    var _a;
    if (!this.isInitialized) {
      console.warn("Saltfish playlist Player is not initialized");
      return;
    }
    try {
      const store = getSaltfishStore();
      log("ManagerOrchestrator: Current state before playlist cleanup:", {
        currentState: store.currentState,
        currentStepId: store.currentStepId,
        isMinimized: store.isMinimized,
        manifestId: (_a = store.manifest) == null ? void 0 : _a.id
      });
      if (this.uiUpdaterUnsubscribe) {
        this.uiUpdaterUnsubscribe();
        this.uiUpdaterUnsubscribe = null;
      }
      this.managers.transitionManager.destroy();
      this.managers.videoManager.destroy();
      this.managers.cursorManager.destroy();
      this.managers.interactionManager.destroy();
      this.managers.playlistManager.destroy();
      this.managers.stepTimeoutManager.destroy();
      this.managers.uiManager.destroy();
      log("ManagerOrchestrator: Playlist cleanup completed, trigger monitoring preserved");
    } catch (error2) {
      ErrorHandler.handleCleanupError(
        error2,
        {
          component: "ManagerOrchestrator",
          method: "cleanupPlaylist"
        }
      );
    }
  }
  /**
   * Destroy all managers and cleanup
   */
  destroyAll() {
    var _a;
    if (!this.isInitialized) {
      console.warn("Saltfish playlist Player is not initialized");
      return;
    }
    try {
      const store = getSaltfishStore();
      log("ManagerOrchestrator: Current state before destroying:", {
        currentState: store.currentState,
        currentStepId: store.currentStepId,
        isMinimized: store.isMinimized,
        manifestId: (_a = store.manifest) == null ? void 0 : _a.id
      });
      this.cleanupCursorAnimationListener();
      if (this.uiUpdaterUnsubscribe) {
        this.uiUpdaterUnsubscribe();
        this.uiUpdaterUnsubscribe = null;
      }
      if (this.eventUpdaterUnsubscribe) {
        this.eventUpdaterUnsubscribe();
        this.eventUpdaterUnsubscribe = null;
      }
      this.managers.transitionManager.destroy();
      this.managers.triggerManager.destroy();
      this.managers.videoManager.destroy();
      this.managers.cursorManager.destroy();
      this.managers.interactionManager.destroy();
      this.managers.analyticsManager.destroy();
      this.managers.sessionManager.destroy();
      this.managers.playlistManager.destroy();
      this.managers.stepTimeoutManager.destroy();
      this.managers.uiManager.destroy();
      this.isInitialized = false;
      store.reset();
      log("ManagerOrchestrator: All managers destroyed successfully");
    } catch (error2) {
      ErrorHandler.handleCleanupError(
        error2,
        {
          component: "ManagerOrchestrator",
          method: "destroyAll"
        }
      );
      try {
        log("ManagerOrchestrator: Attempting emergency cleanup after error");
        this.isInitialized = false;
        const store = getSaltfishStore();
        store.reset();
        log("ManagerOrchestrator: Emergency cleanup completed");
      } catch (cleanupError) {
        ErrorHandler.handleCleanupError(
          cleanupError,
          {
            component: "ManagerOrchestrator",
            method: "destroyAll",
            additionalData: { originalError: error2 }
          }
        );
      }
    }
  }
  /**
   * Get all managed managers for external access
   */
  getManagers() {
    return this.managers;
  }
  destroy() {
    this.destroyAll();
  }
};
// State tracking for store changes
__publicField(_ManagerOrchestrator, "prevState", {
  currentState: "idle",
  currentStepId: null,
  isMinimized: false
});
let ManagerOrchestrator = _ManagerOrchestrator;
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} ";
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} ";
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-md);\n  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15), 0 5px 15px rgba(0, 0, 0, 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/* Gradient overlay now moved to video container - see _video.css */\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.20), 0 5px 15px rgba(0, 0, 0, 0.12);\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.20), 0 5px 15px rgba(0, 0, 0, 0.12);\n  cursor: pointer; /* Whole bubble is clickable */\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/* Hide center play button in compact mode - the whole bubble is clickable */\n.sf-player--compact .sf-player__center-play-button {\n  display: none !important;\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 - see _video.css */\n.sf-player--compact .sf-video-container::after {\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: 4px;\n  width: var(--sf-minimize-button-size);\n  height: var(--sf-minimize-button-size);\n  background: none !important;\n  border-radius: 0;\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 background circle */\n.sf-player__minimize-button::before {\n  content: '';\n  position: absolute;\n  width: 80%;\n  height: 80%;\n  background-color: rgba(0, 0, 0, 0.1);\n  border-radius: 50%;\n  z-index: -1;\n  transition: all var(--sf-transition-normal);\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), opacity var(--sf-transition-normal);\n  backdrop-filter: blur(1px);\n  -webkit-backdrop-filter: blur(1px);\n  pointer-events: auto; /* Enable pointer events to capture clicks */\n}\n\n/* Icon visibility control - use opacity for smooth transitions */\n.sf-player__center-play-button__pause-icon,\n.sf-player__center-play-button__play-icon {\n  position: absolute;\n  transition: opacity var(--sf-transition-normal);\n}\n\n.sf-player__center-play-button__pause-icon {\n  opacity: 0;\n}\n\n.sf-player__center-play-button__play-icon {\n  opacity: 1;\n}\n\n/* When playing, make button hoverable but invisible (desktop only) */\n@media (hover: hover) and (pointer: fine) {\n  .sf-player--playing .sf-player__center-play-button {\n    display: flex;\n    opacity: 0;\n    pointer-events: auto;\n  }\n\n  /* Show pause icon on hover when playing */\n  .sf-player--playing .sf-player__center-play-button:hover {\n    opacity: 1;\n  }\n\n  .sf-player--playing .sf-player__center-play-button:hover .sf-player__center-play-button__play-icon {\n    opacity: 0;\n    transition: none; /* Instant switch when hovering during playback */\n  }\n\n  .sf-player--playing .sf-player__center-play-button:hover .sf-player__center-play-button__pause-icon {\n    opacity: 1;\n    transition: none; /* Instant switch when hovering during playback */\n  }\n\n  /* Delay icon switch when not hovering during playback so pause icon stays visible during fade-out */\n  .sf-player--playing .sf-player__center-play-button .sf-player__center-play-button__play-icon,\n  .sf-player--playing .sf-player__center-play-button .sf-player__center-play-button__pause-icon {\n    transition: opacity 0s 0.2s; /* Instant transition but delayed by button fade duration */\n  }\n}\n\n/* Replay icon hidden by default */\n.sf-player__center-play-button__replay-icon {\n  opacity: 0;\n  position: absolute;\n  transition: opacity var(--sf-transition-normal);\n}\n\n/* Show replay icon when video completed or waiting for interaction */\n.sf-player--completedWaitingForInteraction .sf-player__center-play-button__replay-icon,\n.sf-player--waitingForInteraction .sf-player__center-play-button__replay-icon {\n  opacity: 1;\n  transition: none;\n}\n\n/* Hide play/pause icons when showing replay */\n.sf-player--completedWaitingForInteraction .sf-player__center-play-button__play-icon,\n.sf-player--completedWaitingForInteraction .sf-player__center-play-button__pause-icon,\n.sf-player--waitingForInteraction .sf-player__center-play-button__play-icon,\n.sf-player--waitingForInteraction .sf-player__center-play-button__pause-icon {\n  opacity: 0;\n  transition: none;\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}\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 dynamic positioning when buttons are present */\n/* Positions button centered between top of player and top of first button */\n.sf-player__center-play-button--with-buttons {\n  /* Available space above buttons = playerHeight - bottomSpacing - buttonContainerHeight */\n  /* Center point = availableSpace / 2 */\n  top: calc((var(--sf-player-height) - var(--sf-spacing-xl) - var(--sf-button-container-height, 0px)) / 2) !important;\n  transform: translate(-50%, -50%) !important;\n}\n\n/* Maintain scale on hover for buttons with dynamic positioning */\n.sf-player__center-play-button--with-buttons:hover {\n  transform: translate(-50%, -50%) scale(1.1) !important;\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: 22px;\n  height: 22px;\n  background-color: rgba(0, 0, 0, 0.5);\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}\n\n.sf-player__exit-button svg {\n  width: 18px;\n  height: 18px;\n}\n\n/* Exit button hover state */\n.sf-player__exit-button:hover {\n  transform: scale(1.1);\n  background-color: rgba(0, 0, 0, 0.7);\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} ";
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  /* Force clean border-radius clipping */\n  isolation: isolate;\n  transform: translateZ(0);\n  -webkit-mask-image: -webkit-radial-gradient(white, black);\n  /* Ensure video container and its children (including captions) sit above the gradient overlay */\n  z-index: 3;\n  /* Smooth transition for border-radius changes */\n  transition: border-radius var(--sf-transition-slow);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container 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: 0 0 var(--sf-border-radius-md) var(--sf-border-radius-md);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container height */\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);\n  pointer-events: none;\n  z-index: var(--sf-z-index-overlay);\n  border-radius: 0 0 var(--sf-border-radius-md) var(--sf-border-radius-md);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container height */\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);\n  pointer-events: none;\n  z-index: var(--sf-z-index-overlay);\n  border-radius: 0 0 var(--sf-border-radius-lg) var(--sf-border-radius-lg);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container height */\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);\n  pointer-events: none;\n  z-index: var(--sf-z-index-overlay);\n  border-radius: 0 0 var(--sf-border-radius-lg) var(--sf-border-radius-lg);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container height */\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);\n  pointer-events: none;\n  z-index: var(--sf-z-index-overlay);\n  border-radius: 0 0 var(--sf-border-radius-md) var(--sf-border-radius-md);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container height */\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);\n  pointer-events: none;\n  z-index: var(--sf-z-index-overlay);\n  border-radius: 0 0 var(--sf-border-radius-md) var(--sf-border-radius-md);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container height */\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);\n  pointer-events: none;\n  z-index: var(--sf-z-index-overlay);\n  border-radius: 0 0 var(--sf-border-radius-md) var(--sf-border-radius-md);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container height */\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);\n  pointer-events: none;\n  z-index: var(--sf-z-index-overlay);\n  border-radius: 0 0 var(--sf-border-radius-md) var(--sf-border-radius-md);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container height */\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);\n  pointer-events: none;\n  z-index: var(--sf-z-index-overlay);\n  border-radius: 0 0 var(--sf-border-radius-md) var(--sf-border-radius-md);\n}\n\n/* Dark gradient overlay at bottom of video - ensures captions are visible above it */\n.sf-video-container::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 33.33%; /* One third of container height */\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);\n  pointer-events: none;\n  z-index: var(--sf-z-index-overlay);\n  border-radius: 0 0 var(--sf-border-radius-md) var(--sf-border-radius-md);\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  /* Remove any border-radius - let container's overflow: hidden do the clipping */\n  border-radius: 0;\n  /* Force GPU acceleration for cleaner clipping with scale effect */\n  transform: scale(1.04) translateZ(0);\n  backface-visibility: hidden;\n  transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n/* Video blur effect (for end-of-video transitions) */\n.sf-video-container__video--blurred {\n  filter: blur(3px);\n  transform: scale(1.04) translateZ(0);\n  transition: filter 0.3s ease-out, transform 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    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  /* Adjust pseudo-element for mobile touch target */\n  .sf-video-container__controls::before {\n    height: 14px; /* Larger touch target on mobile */\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  /* Maintain consistent clickable area on touch devices */\n  .sf-video-container__controls::before,\n  .sf-video-container__controls:hover::before {\n    height: 14px; /* Consistent touch target */\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  /* Ensure clean clipping in circular state */\n  isolation: isolate;\n  transform: translateZ(0);\n}\n\n/* Hide gradient overlay when minimized */\n.sf-player--minimized .sf-video-container::after {\n  display: none;\n}\n\n.sf-player--minimized .sf-video-container__video {\n  border-radius: 0; /* Let container handle clipping */\n  object-fit: cover;\n  width: 100%;\n  height: 100%;\n  transform: scale(1.2) translateZ(0);\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  right: 0;\n  width: 100%;\n  height: 4px;\n  background-color: rgba(255, 255, 255, 0.3);\n  z-index: var(--sf-z-index-controls);\n  /* Match the container's top border-radius for seamless integration */\n  border-radius: var(--sf-border-radius-md) var(--sf-border-radius-md) 0 0;\n  cursor: pointer;\n  /* Ensure pixel-perfect alignment and edge-to-edge coverage */\n  transform: translateZ(0);\n  backface-visibility: hidden;\n  /* Smooth transition for height change on hover */\n  transition: height 0.15s ease-in-out, background-color 0.15s ease-in-out;\n  /* Ensure it covers the full width */\n  margin: 0;\n  box-sizing: border-box;\n}\n\n/* Invisible pseudo-element to extend clickable area downward */\n.sf-video-container__controls::before {\n  content: '';\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  height: 10px; /* Visual height (4px) + extended clickable area (6px) */\n  cursor: pointer;\n}\n\n/* Show slightly thicker progress bar on hover for better UX */\n.sf-video-container__controls:hover {\n  height: 6px;\n  background-color: rgba(255, 255, 255, 0.35);\n}\n\n/* Extend pseudo-element when progress bar grows on hover */\n.sf-video-container__controls:hover::before {\n  height: 12px; /* Visual height (6px) + extended clickable area (6px) */\n}\n\n/* Progress indicator */\n.sf-video-container__progress {\n  height: 100%;\n  background: linear-gradient(90deg, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0.85) 100%);\n  width: 0%;\n  transition: width 0.1s linear;\n  /* Only apply top-left border-radius to maintain seamless look */\n  border-radius: var(--sf-border-radius-md) 0 0 0;\n  cursor: pointer;\n  transform-origin: left;\n  /* Ensure smooth rendering */\n  will-change: width;\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: 4px;\n  width: var(--sf-mute-button-size);\n  height: var(--sf-mute-button-size);\n  background: none !important;\n  border-radius: 0;\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 background circle */\n.sf-video-container__mute-button::before {\n  content: '';\n  position: absolute;\n  width: 80%;\n  height: 80%;\n  background-color: rgba(0, 0, 0, 0.1);\n  border-radius: 50%;\n  z-index: -1;\n  transition: all var(--sf-transition-normal);\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: 4px;\n  width: var(--sf-cc-button-size);\n  height: var(--sf-cc-button-size);\n  background: none !important;\n  border-radius: 0;\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/* CC button background circle */\n.sf-video-container__cc-button::before {\n  content: '';\n  position: absolute;\n  width: 80%;\n  height: 80%;\n  background-color: rgba(0, 0, 0, 0.1);\n  border-radius: 50%;\n  z-index: -1;\n  transition: all var(--sf-transition-normal);\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/* Compact mode styling - circular border-radius for video container */\n.sf-player--compact .sf-video-container {\n  border-radius: var(--sf-border-radius-circle);\n}\n\n/* Hide gradient overlay in compact mode (already handled in _player.css but kept for clarity) */\n.sf-player--compact .sf-video-container::after {\n  display: none;\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} ";
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 5+ buttons */\n.sf-choice-buttons-container--scrollable {\n  max-height: 176px; /* Show ~4 buttons with gaps (4 * 36px button + 4 * 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 !important;\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 !important;\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} ";
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-controls);\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  display: flex;\n  justify-content: center;\n  align-items: center;\n  width: 100%;\n  text-align: center;\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 - progressive reveal style */\n.sf-transcript__word-container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  gap: 0.04em; /* Much tighter gap to compensate for inverse scaling effect */\n  min-height: 60px;\n  padding: 0 var(--sf-spacing-md);\n  flex-wrap: nowrap;\n  overflow: hidden;\n  width: 100%;\n  max-width: 100%;\n  box-sizing: border-box;\n  text-align: center;\n  transition: opacity 0.15s ease-in-out; /* Smooth fade during window transitions */\n}\n\n/* Individual word styling - base state (hidden by default) */\n.sf-transcript__word {\n  color: rgba(255, 255, 255, 0.65);\n  font-size: 20px; /* Increased from 18px to compensate for 0.893 scale (20 * 0.893 ≈ 18px) */\n  font-weight: 500; /* Set to 500 from start to prevent pixel shift on activation */\n  line-height: 1.3;\n  white-space: nowrap;\n  text-align: center;\n  opacity: 0;\n  transform: scale(0.893) translateY(0); /* Scaled down - active word will be normal size (1/1.12 ≈ 0.893) */\n  transition: opacity 0.2s ease, color 0.2s ease, text-shadow 0.2s ease, transform 0.2s ease;\n  padding-left: 0.12em; /* Consistent padding */\n  padding-right: 0.12em;\n}\n\n/* Visible state - word has appeared */\n.sf-transcript__word--visible {\n  opacity: 0.8;\n  transform: scale(0.893) translateY(0); /* Keep scaled down */\n}\n\n/* Active word (currently speaking) - highlighted at normal size (inverse scale approach) */\n.sf-transcript__word--active {\n  color: rgba(255, 255, 255, 1) !important;\n  opacity: 1 !important;\n  transform: scale(1) translateY(0); /* Normal size - appears 12% larger relative to scaled-down neighbors */\n  text-shadow: 0 0 20px rgba(255, 255, 255, 0.3);\n  /* font-weight removed - already 500 from base to prevent pixel shift */\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: 35px; /* Positioned lower on mobile for better visibility */\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: 0 var(--sf-spacing-sm);\n    gap: 0.2em; /* Tighter gap for mobile */\n  }\n  \n  .sf-transcript__word {\n    font-size: var(--sf-font-size-md);\n    /* font-weight already 500 from base styles */\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}";
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}";
const componentsLoadingCss = "/**\n * Loading spinner styles\n */\n\n.sf-loading-spinner {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background-color: rgba(255, 255, 255, 0.95);\n  backdrop-filter: blur(2px);\n  z-index: 100;\n  border-radius: 12px;\n  opacity: 1 !important; /* Always visible when shown, overrides parent opacity */\n}\n\n.sf-loading-spinner__content {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 12px;\n  padding: 24px;\n}\n\n.sf-loading-spinner__icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: #000000;\n  opacity: 0.9;\n}\n\n.sf-loading-spinner__icon svg {\n  width: 60px;\n  height: 60px;\n  /* Remove the rotation animation since we now have internal SVG animation */\n}\n\n.sf-loading-spinner__text {\n  color: #333333;\n  font-size: 14px;\n  font-weight: 500;\n  text-align: center;\n  opacity: 0.8;\n  letter-spacing: 0.5px;\n}\n\n/* CSS keyframe animation removed - using SVG animateTransform instead */\n\n/* Responsive adjustments */\n@media (max-width: 480px) {\n  .sf-loading-spinner__content {\n    padding: 20px;\n    gap: 10px;\n  }\n  \n  .sf-loading-spinner__icon svg {\n    width: 50px;\n    height: 50px;\n  }\n  \n  .sf-loading-spinner__text {\n    font-size: 13px;\n  }\n}";
const componentsCursorCss = "/*\n * Cursor animation component styles for the Saltfish playlist Player\n * CSP-compliant: All styles use CSS classes and CSS custom properties instead of inline styles\n */\n\n/* Base cursor element */\n.sf-cursor {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 36px;\n  height: 36px;\n  z-index: 9999999;\n  pointer-events: none;\n  display: none;\n  will-change: transform;\n  transform: var(--sf-cursor-transform, translate(0, 0));\n  opacity: var(--sf-cursor-opacity, 1);\n}\n\n.sf-cursor--visible {\n  display: block;\n}\n\n/* Selection element */\n.sf-selection {\n  position: fixed;\n  pointer-events: none;\n  display: none;\n  z-index: 9999998;\n  border: 2px solid var(--sf-selection-color, #ff7614);\n  background: var(--sf-selection-bg-color, rgba(255, 118, 20, 0.1));\n  border-radius: 4px;\n  left: var(--sf-selection-left, 0);\n  top: var(--sf-selection-top, 0);\n  width: var(--sf-selection-width, 0);\n  height: var(--sf-selection-height, 0);\n}\n\n.sf-selection--visible {\n  display: block;\n}\n\n/* Flashlight overlay */\n.sf-flashlight-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100vw;\n  height: 100vh;\n  pointer-events: none;\n  z-index: 999997;\n  display: none;\n  background: var(--sf-flashlight-bg, radial-gradient(circle 150px at 50% 50%, transparent 0%, rgba(0, 0, 0, 0.4) 100%));\n  clip-path: var(--sf-flashlight-clip, none);\n}\n\n.sf-flashlight-overlay--visible {\n  display: block;\n}\n";
const componentsCompactLabelCss = "/**\n * Compact Label Styles\n * Label that appears next to the player when in compact first step mode\n * Position adapts based on player placement (left/right)\n */\n\n.sf-compact-label {\n  position: absolute;\n  top: 50%;\n  transform: translateY(-50%);\n\n  /* Typography */\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n  font-size: 14px;\n  font-weight: 500;\n  line-height: 1.4;\n  color: #ffffff;\n\n  /* Visual styling */\n  background: rgba(0, 0, 0, 0.85);\n  backdrop-filter: blur(10px);\n  padding: 10px 16px;\n  border-radius: 8px;\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3),\n              0 0 0 1px rgba(255, 255, 255, 0.1);\n\n  /* Ensure it doesn't wrap */\n  white-space: nowrap;\n\n  /* Layering */\n  z-index: 10;\n\n  /* Pointer events - will be enabled via JS when clickable */\n  pointer-events: none;\n  user-select: none;\n\n  /* Smooth transitions for hover effects */\n  transition: transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),\n              box-shadow 0.2s cubic-bezier(0.25, 0.8, 0.25, 1),\n              background 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n/* Position to the LEFT of player (when player is on the right side) */\n.sf-compact-label--left {\n  right: calc(100% + 16px); /* 16px spacing from player edge */\n  animation: sf-compact-label-enter-from-left 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;\n}\n\n/* Position to the RIGHT of player (when player is on the left side) */\n.sf-compact-label--right {\n  left: calc(100% + 16px); /* 16px spacing from player edge */\n  animation: sf-compact-label-enter-from-right 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;\n}\n\n/* Entrance animation from LEFT - slide in from left with fade */\n@keyframes sf-compact-label-enter-from-left {\n  0% {\n    opacity: 0;\n    transform: translateY(-50%) translateX(-20px);\n  }\n  100% {\n    opacity: 1;\n    transform: translateY(-50%) translateX(0);\n  }\n}\n\n/* Entrance animation from RIGHT - slide in from right with fade */\n@keyframes sf-compact-label-enter-from-right {\n  0% {\n    opacity: 0;\n    transform: translateY(-50%) translateX(20px);\n  }\n  100% {\n    opacity: 1;\n    transform: translateY(-50%) translateX(0);\n  }\n}\n\n/* Hover effect - only applies when label is clickable (pointer-events: auto) */\n.sf-compact-label--left:hover,\n.sf-compact-label--right:hover {\n  background: rgba(0, 0, 0, 0.95);\n  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4),\n              0 0 0 1px rgba(255, 255, 255, 0.2),\n              0 0 25px rgba(255, 255, 255, 0.08);\n}\n\n.sf-compact-label--left:hover {\n  transform: translateY(-50%) translateX(-2px);\n}\n\n.sf-compact-label--right:hover {\n  transform: translateY(-50%) translateX(2px);\n}\n\n/* Active/click state */\n.sf-compact-label--left:active,\n.sf-compact-label--right:active {\n  transform: translateY(-50%) scale(0.98);\n}\n\n/* Mobile adjustments */\n@media (max-width: 768px) {\n  .sf-compact-label {\n    font-size: 12px;\n    padding: 8px 12px;\n  }\n\n  .sf-compact-label--left {\n    right: calc(100% + 12px); /* Slightly less spacing on mobile */\n  }\n\n  .sf-compact-label--right {\n    left: calc(100% + 12px); /* Slightly less spacing on mobile */\n  }\n}\n\n/* Very small screens - keep to side with tighter spacing */\n@media (max-width: 480px) {\n  .sf-compact-label {\n    /* Use most of the available screen width - on 390px wide screen this gives ~270px */\n    max-width: calc(100vw - 90px - 20px - 8px - 10px); /* viewport - player - margin - spacing - small buffer */\n    /* Keep nowrap - we have plenty of space on mobile screens */\n  }\n\n  .sf-compact-label--left {\n    right: calc(100% + 8px); /* Reduce spacing slightly for mobile */\n  }\n\n  .sf-compact-label--right {\n    left: calc(100% + 8px); /* Reduce spacing slightly for mobile */\n  }\n}\n";
const animationsTransitionsCss = "/* \n * Transitions and animations for Saltfish playlist Player\n */\n\n/* Fade in animation */\n@keyframes sf-fade-in {\n  from { opacity: 0; }\n  to { opacity: 1; }\n}\n\n.sf-fade-in {\n  animation: sf-fade-in 0.3s ease-in-out forwards;\n}\n\n/* Slide in from bottom animation */\n@keyframes sf-slide-in-bottom {\n  from { transform: translateY(100%); opacity: 0; }\n  to { transform: translateY(0); opacity: 1; }\n}\n\n.sf-slide-in-bottom {\n  animation: sf-slide-in-bottom 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;\n}\n\n/* Slide in from right animation */\n@keyframes sf-slide-in-right {\n  from { transform: translateX(100%); opacity: 0; }\n  to { transform: translateX(0); opacity: 1; }\n}\n\n.sf-slide-in-right {\n  animation: sf-slide-in-right 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;\n}\n\n/* Scale in animation */\n@keyframes sf-scale-in {\n  from { transform: scale(0.8); opacity: 0; }\n  to { transform: scale(1); opacity: 1; }\n}\n\n.sf-scale-in {\n  animation: sf-scale-in 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;\n}\n\n/* Scale out animation */\n@keyframes sf-scale-out {\n  from { transform: scale(1); opacity: 1; }\n  to { transform: scale(0.8); opacity: 0; }\n}\n\n.sf-scale-out {\n  animation: sf-scale-out 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;\n} ";
const getPlayerStyles = () => `
  ${baseResetCss}
  ${baseVariablesCss}

  ${componentsPlayerCss}
  ${componentsVideoCss}
  ${componentsControlsCss}
  ${componentsTranscriptCss}
  ${componentsErrorCss}
  ${componentsLoadingCss}
  ${componentsCursorCss}
  ${componentsCompactLabelCss}

  ${animationsTransitionsCss}
`;
class ShadowDOMManager {
  constructor() {
    __publicField(this, "container", null);
    __publicField(this, "shadowRoot", null);
    __publicField(this, "styleElement", null);
  }
  /**
   * Creates a new shadow DOM container on the page
   */
  create() {
    if (this.container) {
      return;
    }
    this.container = document.createElement("div");
    this.container.id = "saltfish-container";
    this.container.appendChild(document.createTextNode("​"));
    document.body.appendChild(this.container);
    this.shadowRoot = this.container.attachShadow({ mode: "open" });
    try {
      const sheet = new CSSStyleSheet();
      sheet.replaceSync(this.getBaseStyles());
      this.shadowRoot.adoptedStyleSheets = [sheet];
    } catch (error2) {
      this.styleElement = document.createElement("style");
      this.styleElement.textContent = this.getBaseStyles();
      this.shadowRoot.appendChild(this.styleElement);
    }
    const rootElement = document.createElement("div");
    rootElement.id = "sf-player-root";
    this.shadowRoot.appendChild(rootElement);
  }
  /**
   * Returns the shadow root
   */
  getShadowRoot() {
    return this.shadowRoot;
  }
  /**
   * Returns the root element inside the shadow DOM
   */
  getRootElement() {
    if (!this.shadowRoot) {
      return null;
    }
    const rootElement = this.shadowRoot.getElementById("sf-player-root");
    return rootElement;
  }
  /**
   * Adds a stylesheet to the shadow DOM
   */
  addStyles(styles) {
    if (!this.shadowRoot) {
      return;
    }
    if (this.shadowRoot.adoptedStyleSheets && this.shadowRoot.adoptedStyleSheets.length > 0) {
      try {
        const existingSheet = this.shadowRoot.adoptedStyleSheets[0];
        const currentRules = Array.from(existingSheet.cssRules).map((rule) => rule.cssText).join("\n");
        existingSheet.replaceSync(currentRules + "\n" + styles);
        return;
      } catch (error2) {
        console.warn("ShadowDOMManager: Failed to append to adoptedStyleSheet:", error2);
      }
    }
    if (this.styleElement) {
      this.styleElement.textContent += styles;
    }
  }
  /**
   * Removes the shadow DOM container
   */
  remove() {
    if (this.container) {
      document.body.removeChild(this.container);
      this.container = null;
      this.shadowRoot = null;
      this.styleElement = null;
    }
  }
  /**
   * Returns base styles for the shadow DOM
   * Now using our organized CSS structure imported from styles/index.ts
   */
  getBaseStyles() {
    return getPlayerStyles();
  }
}
class DeviceDetector {
  /**
   * Detects if the current device is mobile using multiple methods
   * @returns boolean indicating if device is mobile
   */
  static isMobile() {
    return this.getDeviceInfo().isMobile;
  }
  /**
   * Detects if the current device is a tablet
   * @returns boolean indicating if device is a tablet
   */
  static isTablet() {
    return this.getDeviceInfo().isTablet;
  }
  /**
   * Detects if the current device is desktop
   * @returns boolean indicating if device is desktop
   */
  static isDesktop() {
    return this.getDeviceInfo().isDesktop;
  }
  /**
   * Detects if the current device supports touch
   * @returns boolean indicating if device supports touch
   */
  static isTouchDevice() {
    return this.getDeviceInfo().isTouchDevice;
  }
  /**
   * Gets the current screen orientation
   * @returns 'portrait' or 'landscape'
   */
  static getOrientation() {
    return this.getDeviceInfo().orientation;
  }
  /**
   * Gets comprehensive device information
   * @returns DeviceInfo object with all device details
   */
  static getDeviceInfo() {
    if (this.cachedDeviceInfo) {
      this.cachedDeviceInfo.orientation = this.detectOrientation();
      return this.cachedDeviceInfo;
    }
    const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
    const isTouchDevice = this.detectTouchSupport();
    const { width, height } = this.getScreenDimensions();
    const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
    const tabletRegex = /iPad|Android(?!.*Mobile)|Tablet|tablet/i;
    const isMobileUserAgent = mobileRegex.test(userAgent) && !tabletRegex.test(userAgent);
    const isTabletUserAgent = tabletRegex.test(userAgent);
    const screenSize = this.getScreenSize(width, height);
    const isMobileByScreen = screenSize === "small" && Math.min(width, height) < 768;
    const isTabletByScreen = screenSize === "medium" && !isMobileByScreen;
    const isMobile = isMobileUserAgent || isMobileByScreen && isTouchDevice;
    const isTablet = isTabletUserAgent || isTabletByScreen && isTouchDevice && !isMobile;
    const isDesktop = !isMobile && !isTablet;
    const deviceInfo = {
      isMobile,
      isTablet,
      isDesktop,
      isTouchDevice,
      screenSize,
      orientation: this.detectOrientation(),
      userAgent
    };
    this.cachedDeviceInfo = deviceInfo;
    return deviceInfo;
  }
  /**
   * Detects touch support
   * @returns boolean indicating if touch is supported
   */
  static detectTouchSupport() {
    if (typeof window === "undefined") {
      return false;
    }
    return "ontouchstart" in window || navigator.maxTouchPoints > 0 || // @ts-ignore - some older browsers
    navigator.msMaxTouchPoints > 0;
  }
  /**
   * Gets screen dimensions
   * @returns object with width and height
   */
  static getScreenDimensions() {
    if (typeof window === "undefined") {
      return { width: 1920, height: 1080 };
    }
    return {
      width: window.innerWidth || document.documentElement.clientWidth || 1920,
      height: window.innerHeight || document.documentElement.clientHeight || 1080
    };
  }
  /**
   * Determines screen size category
   * @param width Screen width
   * @param height Screen height
   * @returns Screen size category
   */
  static getScreenSize(width, height) {
    const minDimension = Math.min(width, height);
    if (minDimension < 768) {
      return "small";
    }
    if (minDimension < 1024) {
      return "medium";
    }
    return "large";
  }
  /**
   * Detects screen orientation
   * @returns Current orientation
   */
  static detectOrientation() {
    if (typeof window === "undefined") {
      return "landscape";
    }
    const { width, height } = this.getScreenDimensions();
    return height > width ? "portrait" : "landscape";
  }
  /**
   * Clears the cached device info (useful for testing or when device capabilities change)
   */
  static clearCache() {
    this.cachedDeviceInfo = null;
  }
  /**
   * Sets up listeners for orientation and resize changes
   * @param callback Function to call when device info changes
   */
  static onDeviceChange(callback) {
    if (typeof window === "undefined") {
      return () => {
      };
    }
    const handleChange = () => {
      try {
        this.clearCache();
        callback(this.getDeviceInfo());
      } catch (e) {
        console.warn("DeviceDetector: Error in device change handler:", e);
      }
    };
    window.addEventListener("orientationchange", handleChange);
    window.addEventListener("resize", handleChange);
    return () => {
      window.removeEventListener("orientationchange", handleChange);
      window.removeEventListener("resize", handleChange);
    };
  }
}
__publicField(DeviceDetector, "cachedDeviceInfo", null);
const isDeviceCompatible = (deviceType) => {
  if (!deviceType || deviceType === "both") {
    return true;
  }
  const deviceInfo = DeviceDetector.getDeviceInfo();
  if (deviceType === "mobile") {
    return deviceInfo.isMobile || deviceInfo.isTablet;
  }
  if (deviceType === "desktop") {
    return deviceInfo.isDesktop;
  }
  return false;
};
const deviceDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
  __proto__: null,
  DeviceDetector,
  isDeviceCompatible
}, Symbol.toStringTag, { value: "Module" }));
const _TranscriptManager = class _TranscriptManager {
  constructor() {
    __publicField(this, "transcriptContainer", null);
    __publicField(this, "transcriptContent", null);
    __publicField(this, "ccButton", null);
    __publicField(this, "isVisible", true);
    __publicField(this, "currentTranscript", null);
    __publicField(this, "chunks", []);
    __publicField(this, "videoElement", null);
    __publicField(this, "timeUpdateListener", null);
    __publicField(this, "videoManager", null);
    // Force first word display for minimum duration on new videos
    __publicField(this, "firstWordForceDisplayUntil", 0);
    __publicField(this, "isNewVideo", false);
    // Track last displayed chunk to prevent duplicate renders
    __publicField(this, "lastDisplayedChunkIndex", -1);
    // Animation frame for high-frequency transcript updates
    __publicField(this, "animationFrameId", null);
    __publicField(this, "isAnimationLoopActive", false);
    // Track current word window for karaoke-style display
    __publicField(this, "currentWindowStartIndex", -1);
    __publicField(this, "currentWindowEndIndex", -1);
  }
  /**
   * Setup the transcript manager with a video element and CC button
   */
  setup(videoElement, ccButton, videoManager) {
    this.videoElement = videoElement;
    this.ccButton = ccButton;
    this.videoManager = videoManager;
    this.ccButton.addEventListener("click", this.handleCCButtonClick.bind(this));
    this.timeUpdateListener = this.handleTimeUpdate.bind(this);
    this.videoElement.addEventListener("timeupdate", this.timeUpdateListener);
  }
  /**
   * Update the video element reference (used when VideoManager switches between dual videos)
   */
  updateVideoElement(newVideoElement) {
    if (this.videoElement && this.timeUpdateListener) {
      this.videoElement.removeEventListener("timeupdate", this.timeUpdateListener);
    }
    this.videoElement = newVideoElement;
    if (this.videoElement && this.timeUpdateListener) {
      this.videoElement.addEventListener("timeupdate", this.timeUpdateListener);
    }
  }
  /**
   * Load transcript data for the current step
   */
  loadTranscript(transcript, initiallyVisible = true) {
    if (this.transcriptContent) {
      this.transcriptContent.innerHTML = "";
    }
    this.lastDisplayedChunkIndex = -1;
    this.currentWindowStartIndex = -1;
    this.currentWindowEndIndex = -1;
    this.currentTranscript = transcript;
    if (transcript) {
      this.createTranscriptUI();
      this.chunks = transcript.chunks || [];
      this.isNewVideo = true;
      this.firstWordForceDisplayUntil = Date.now() + 500;
      this.setupWordDisplay();
      if (_TranscriptManager.userCaptionPreference !== null) {
        this.isVisible = _TranscriptManager.userCaptionPreference;
        log(`TranscriptManager: Using user preference for captions: ${this.isVisible}`);
      } else {
        this.isVisible = initiallyVisible;
        log(`TranscriptManager: Using manifest setting for captions: ${this.isVisible}`);
      }
      this.updateCCButtonState(true);
      if (this.isVisible) {
        this.showTranscript();
      } else {
        this.hideTranscript();
      }
      log(`TranscriptManager: Loaded transcript with ${this.chunks.length} word chunks, isVisible: ${this.isVisible}`);
    } else {
      this.updateCCButtonState(false);
      this.hideTranscript();
    }
  }
  /**
   * Show or hide the transcript display
   */
  toggleVisibility() {
    if (!this.currentTranscript) {
      return;
    }
    this.isVisible = !this.isVisible;
    _TranscriptManager.userCaptionPreference = this.isVisible;
    log(`TranscriptManager: User toggled captions, saving preference: ${this.isVisible}`);
    if (this.isVisible) {
      this.showTranscript();
      this.startAnimationLoop();
    } else {
      this.hideTranscript();
      this.stopAnimationLoop();
    }
    this.updateCCButtonState(!!this.currentTranscript);
    log(`TranscriptManager: Transcript visibility toggled to ${this.isVisible}`);
  }
  /**
   * Handle CC button click
   */
  handleCCButtonClick(event) {
    event.preventDefault();
    event.stopPropagation();
    this.toggleVisibility();
  }
  /**
   * Handle video time updates for transcript synchronization
   */
  handleTimeUpdate(_event) {
    if (!this.isAnimationLoopActive && this.isVisible && this.chunks.length > 0) {
      this.startAnimationLoop();
    }
  }
  /**
   * High-frequency animation loop for smooth caption updates
   * Uses requestAnimationFrame to catch short-duration words
   */
  startAnimationLoop() {
    if (this.isAnimationLoopActive) {
      return;
    }
    this.isAnimationLoopActive = true;
    const updateLoop = () => {
      if (!this.currentTranscript || !this.isVisible || !this.videoElement || this.chunks.length === 0) {
        this.stopAnimationLoop();
        return;
      }
      const currentTime = this.videoElement.currentTime;
      let activeChunkIndex = this.findActiveChunkIndex(currentTime);
      const now = Date.now();
      if (this.isNewVideo && now < this.firstWordForceDisplayUntil && this.chunks.length > 0) {
        activeChunkIndex = 0;
      } else if (this.isNewVideo && now >= this.firstWordForceDisplayUntil) {
        this.isNewVideo = false;
      }
      this.updateIntelligentWordDisplay(activeChunkIndex);
      this.animationFrameId = requestAnimationFrame(updateLoop);
    };
    this.animationFrameId = requestAnimationFrame(updateLoop);
  }
  /**
   * Stop the animation loop
   */
  stopAnimationLoop() {
    if (this.animationFrameId !== null) {
      cancelAnimationFrame(this.animationFrameId);
      this.animationFrameId = null;
    }
    this.isAnimationLoopActive = false;
  }
  /**
   * Find the active chunk index based on current video time
   */
  findActiveChunkIndex(currentTime) {
    if (this.chunks.length === 0) {
      return -1;
    }
    for (let i = 0; i < this.chunks.length; i++) {
      const chunk = this.chunks[i];
      if (currentTime >= chunk.start && currentTime <= chunk.end) {
        return i;
      }
    }
    if (currentTime < this.chunks[0].start) {
      return 0;
    }
    if (currentTime > this.chunks[this.chunks.length - 1].end) {
      return this.chunks.length - 1;
    }
    for (let i = 0; i < this.chunks.length - 1; i++) {
      if (currentTime > this.chunks[i].end && currentTime < this.chunks[i + 1].start) {
        const timeToNext = this.chunks[i + 1].start - currentTime;
        if (timeToNext < 0.1) {
          return i + 1;
        }
        const timeSincePrev = currentTime - this.chunks[i].end;
        if (timeSincePrev < 0.1) {
          return i;
        }
        return i + 1;
      }
    }
    return -1;
  }
  /**
   * Handle state changes to show/hide transcript based on player state
   */
  handleStateChange(currentState) {
    log(`TranscriptManager: handleStateChange called with state: ${currentState}, hasTranscript: ${!!this.currentTranscript}, chunks: ${this.chunks.length}`);
    if (!this.currentTranscript || this.chunks.length === 0) {
      this.stopAnimationLoop();
      return;
    }
    if (currentState === "waitingForInteraction" || currentState === "completedWaitingForInteraction") {
      if (this.transcriptContent) {
        this.transcriptContent.innerHTML = "";
      }
      this.stopAnimationLoop();
      return;
    }
    if (this.isVisible) {
      log(`TranscriptManager: State is ${currentState} and isVisible=${this.isVisible}, showing transcript`);
      this.showTranscript();
      this.startAnimationLoop();
    } else {
      log(`TranscriptManager: State is ${currentState} and isVisible=${this.isVisible}, hiding transcript`);
      this.hideTranscript();
      this.stopAnimationLoop();
    }
  }
  /**
   * Update word display with karaoke-style highlighting
   * Shows 1-4 words in a static window, only moving the highlight
   */
  updateIntelligentWordDisplay(currentIndex) {
    if (!this.transcriptContent) {
      return;
    }
    if (currentIndex === this.lastDisplayedChunkIndex) {
      return;
    }
    const currentChunk = currentIndex >= 0 && currentIndex < this.chunks.length ? this.chunks[currentIndex] : null;
    if (!currentChunk) {
      this.lastDisplayedChunkIndex = -1;
      this.transcriptContent.innerHTML = "";
      return;
    }
    const isWithinWindow = currentIndex >= this.currentWindowStartIndex && currentIndex <= this.currentWindowEndIndex;
    if (isWithinWindow && this.currentWindowStartIndex !== -1) {
      this.updateHighlightInWindow(currentIndex);
    } else {
      this.createNewWindow(currentIndex);
    }
    this.lastDisplayedChunkIndex = currentIndex;
  }
  /**
   * Update which word is highlighted within the existing window (no DOM rebuild)
   * Progressive reveal - shows words up to current index
   */
  updateHighlightInWindow(currentIndex) {
    if (!this.transcriptContent) {
      return;
    }
    const wordElements = this.transcriptContent.querySelectorAll(".sf-transcript__word");
    const relativeIndex = currentIndex - this.currentWindowStartIndex;
    wordElements.forEach((el, index) => {
      el.classList.remove("sf-transcript__word--active");
      if (index <= relativeIndex) {
        el.classList.add("sf-transcript__word--visible");
      } else {
        el.classList.remove("sf-transcript__word--visible");
      }
    });
    if (wordElements[relativeIndex]) {
      wordElements[relativeIndex].classList.add("sf-transcript__word--active");
    }
  }
  /**
   * Create a new window of 1-4 words (rebuilds DOM)
   * Words are created hidden and will progressively reveal
   * Uses fade transition to prevent jittery movement during window changes
   */
  createNewWindow(currentIndex) {
    if (!this.transcriptContent) {
      return;
    }
    const windowInfo = this.calculateWordWindow(currentIndex);
    this.currentWindowStartIndex = windowInfo.startIndex;
    this.currentWindowEndIndex = windowInfo.endIndex;
    const existingContainer = this.transcriptContent.querySelector(".sf-transcript__word-container");
    const createContainer = () => {
      const wordContainer = document.createElement("div");
      wordContainer.className = "sf-transcript__word-container";
      const relativeIndex = currentIndex - windowInfo.startIndex;
      for (let i = windowInfo.startIndex; i <= windowInfo.endIndex; i++) {
        const chunk = this.chunks[i];
        const wordElement = document.createElement("span");
        const relativePosition = i - windowInfo.startIndex;
        const isActive = i === currentIndex;
        const isVisible = relativePosition <= relativeIndex;
        let className = "sf-transcript__word";
        if (isVisible) {
          className += " sf-transcript__word--visible";
        }
        if (isActive) {
          className += " sf-transcript__word--active";
        }
        wordElement.className = className;
        wordElement.textContent = chunk.text;
        wordContainer.appendChild(wordElement);
      }
      return wordContainer;
    };
    if (existingContainer) {
      existingContainer.style.opacity = "0";
      setTimeout(() => {
        if (!this.transcriptContent) {
          return;
        }
        this.transcriptContent.innerHTML = "";
        const newContainer = createContainer();
        newContainer.style.opacity = "0";
        this.transcriptContent.appendChild(newContainer);
        requestAnimationFrame(() => {
          newContainer.style.opacity = "1";
        });
      }, 150);
    } else {
      const newContainer = createContainer();
      this.transcriptContent.appendChild(newContainer);
    }
  }
  /**
   * Calculate the word window (start and end indices) for display
   * Shows 1-4 words based on text length to prevent overflow and line breaks
   * Always splits at sentence boundaries (. ! and ?)
   */
  calculateWordWindow(currentIndex) {
    const maxChars = 18;
    const maxWords = 4;
    const scaleFactor = 1.12;
    if (currentIndex < 0 || currentIndex >= this.chunks.length) {
      return { startIndex: -1, endIndex: -1 };
    }
    let startIndex = currentIndex;
    let endIndex = currentIndex;
    let totalChars = this.chunks[currentIndex].text.length;
    const currentText = this.chunks[currentIndex].text.trim();
    const endsWithSentencePunctuation = currentText.endsWith(".") || currentText.endsWith("!") || currentText.endsWith("?");
    if (endsWithSentencePunctuation) {
      return { startIndex, endIndex };
    }
    while (endIndex - startIndex + 1 < maxWords && endIndex + 1 < this.chunks.length) {
      const nextText = this.chunks[endIndex + 1].text;
      const nextLength = Math.ceil(nextText.length * scaleFactor);
      if (totalChars + nextLength > maxChars) {
        break;
      }
      endIndex++;
      totalChars += nextLength;
      const trimmedText = nextText.trim();
      if (trimmedText.endsWith(".") || trimmedText.endsWith("!") || trimmedText.endsWith("?")) {
        break;
      }
    }
    return { startIndex, endIndex };
  }
  /**
   * Create the transcript UI container
   */
  createTranscriptUI() {
    var _a;
    if (this.transcriptContainer && this.transcriptContainer.parentNode) {
      this.transcriptContainer.parentNode.removeChild(this.transcriptContainer);
      this.transcriptContainer = null;
      this.transcriptContent = null;
    }
    const videoContainer = (_a = this.videoElement) == null ? void 0 : _a.closest(".sf-video-container");
    if (!videoContainer) {
      return;
    }
    this.transcriptContainer = document.createElement("div");
    this.transcriptContainer.className = "sf-transcript";
    this.transcriptContainer.classList.add("sf-hidden");
    this.transcriptContent = document.createElement("div");
    this.transcriptContent.className = "sf-transcript__content";
    if (this.transcriptContainer && this.transcriptContent) {
      this.transcriptContainer.appendChild(this.transcriptContent);
      videoContainer.appendChild(this.transcriptContainer);
    }
  }
  /**
   * Setup word-by-word display containers (now uses dynamic approach)
   */
  setupWordDisplay() {
    if (!this.transcriptContent) {
      return;
    }
    this.transcriptContent.innerHTML = "";
  }
  /**
   * Show the transcript
   */
  showTranscript() {
    log(`TranscriptManager: showTranscript called, container exists: ${!!this.transcriptContainer}`);
    if (this.transcriptContainer) {
      this.transcriptContainer.classList.remove("sf-hidden");
      this.transcriptContainer.classList.add("sf-transcript--visible");
    }
  }
  /**
   * Hide the transcript
   */
  hideTranscript() {
    log(`TranscriptManager: hideTranscript called, container exists: ${!!this.transcriptContainer}`);
    if (this.transcriptContainer) {
      this.transcriptContainer.classList.add("sf-hidden");
      this.transcriptContainer.classList.remove("sf-transcript--visible");
    }
  }
  /**
   * Update CC button state
   */
  updateCCButtonState(hasTranscript) {
    var _a;
    if (!this.ccButton) {
      return;
    }
    if (hasTranscript) {
      this.ccButton.classList.remove("sf-hidden");
      (_a = this.videoManager) == null ? void 0 : _a.updateCCButtonIcon(this.isVisible);
    } else {
      this.ccButton.classList.add("sf-hidden");
    }
  }
  /**
   * Clean up resources
   */
  /**
   * Reset the TranscriptManager to a clean state for new playlist
   * Clears transcript data and UI state without destroying DOM elements
   */
  reset() {
    this.stopAnimationLoop();
    if (this.transcriptContent) {
      this.transcriptContent.innerHTML = "";
    }
    this.currentTranscript = null;
    this.chunks = [];
    this.isNewVideo = false;
    this.firstWordForceDisplayUntil = 0;
    this.lastDisplayedChunkIndex = -1;
    this.currentWindowStartIndex = -1;
    this.currentWindowEndIndex = -1;
    if (this.isVisible) {
      this.hideTranscript();
    }
    this.updateCCButtonState(false);
    this.transcriptContainer = null;
    this.transcriptContent = null;
  }
  /**
   * Reset user caption preference (called when player is destroyed)
   */
  static resetUserPreference() {
    _TranscriptManager.userCaptionPreference = null;
  }
  async destroy() {
    this.stopAnimationLoop();
    if (this.timeUpdateListener && this.videoElement) {
      this.videoElement.removeEventListener("timeupdate", this.timeUpdateListener);
    }
    if (this.transcriptContainer && this.transcriptContainer.parentNode) {
      this.transcriptContainer.parentNode.removeChild(this.transcriptContainer);
    }
    this.transcriptContainer = null;
    this.transcriptContent = null;
    this.ccButton = null;
    this.videoElement = null;
    this.timeUpdateListener = null;
    this.currentTranscript = null;
    this.isVisible = false;
  }
};
// Track user's caption preference across all videos (null = use backend default)
__publicField(_TranscriptManager, "userCaptionPreference", null);
let TranscriptManager = _TranscriptManager;
class DevicePlaybackHandler {
  constructor(deviceInfo) {
    __publicField(this, "deviceInfo");
    this.deviceInfo = deviceInfo;
  }
  /**
   * Handle device change updates
   */
  updateDeviceInfo(newDeviceInfo) {
    this.deviceInfo = newDeviceInfo;
  }
}
class MobilePlaybackHandler extends DevicePlaybackHandler {
  getVideoElementConfig() {
    return {
      playsInline: true,
      muted: true,
      // Start muted for autoplay compatibility
      controls: false,
      preload: "metadata",
      additionalAttributes: {
        "webkit-playsinline": "true",
        "playsinline": "true",
        "x-webkit-airplay": "allow"
      },
      styles: {
        width: "100%",
        height: "100%",
        objectFit: "cover",
        backgroundColor: "white"
      }
    };
  }
  getControlsConfig() {
    return {
      buttonMinSize: { width: "44px", height: "44px" },
      // Touch-friendly size
      useTouch: true,
      progressUpdateInterval: 500
      // Slower for mobile performance
    };
  }
  getAutoplayConfig(hasUserInteracted) {
    return {
      shouldStartMuted: !hasUserInteracted,
      // Mute until first interaction
      enableFallbackLoop: true,
      fallbackTimeout: 3e3,
      // 3 second timeout for mobile
      requiresUserInteraction: true
    };
  }
  configureVideoElement(video) {
    const config = this.getVideoElementConfig();
    video.playsInline = config.playsInline;
    video.muted = config.muted;
    video.controls = config.controls;
    video.preload = config.preload;
    Object.entries(config.additionalAttributes).forEach(([key, value]) => {
      try {
        video.setAttribute(key, value);
      } catch (e) {
        console.warn(`MobilePlaybackHandler: Failed to set attribute ${key}:`, e);
      }
    });
    Object.entries(config.styles).forEach(([key, value]) => {
      try {
        video.style.setProperty(key, value);
      } catch (e) {
        console.warn(`MobilePlaybackHandler: Failed to set style ${key}:`, e);
      }
    });
  }
  configureControlElement(element) {
    const config = this.getControlsConfig();
    element.style.setProperty("min-width", config.buttonMinSize.width);
    element.style.setProperty("min-height", config.buttonMinSize.height);
  }
  async handlePlayAttempt(video, hasUserInteracted) {
    const store = getSaltfishStore();
    const userWantsMuted = store.isMuted;
    video.muted = userWantsMuted;
    video.loop = false;
    try {
      await video.play();
      return true;
    } catch (error2) {
      if (!video.muted) {
        await this.handleAutoplayFallback(video);
        return false;
      } else {
        await this.handleAutoplayFallback(video);
        return false;
      }
    }
  }
  async handleAutoplayFallback(video) {
    video.muted = true;
    video.loop = true;
    video.playsInline = true;
    video.setAttribute("playsinline", "true");
    video.setAttribute("webkit-playsinline", "true");
    try {
      await video.play();
    } catch (fallbackError) {
      console.error("MobilePlaybackHandler: All autoplay attempts failed");
    }
  }
  /**
   * Clean up resources when handler is destroyed
   */
  destroy() {
  }
  getProgressUpdateFrequency(isAutoplayFallback) {
    return isAutoplayFallback ? 500 : 16;
  }
}
class DesktopPlaybackHandler extends DevicePlaybackHandler {
  getVideoElementConfig() {
    return {
      playsInline: true,
      muted: false,
      // Desktop can start unmuted
      controls: false,
      preload: "metadata",
      additionalAttributes: {},
      styles: {
        backgroundColor: "white"
      }
    };
  }
  getControlsConfig() {
    return {
      buttonMinSize: { width: "auto", height: "auto" },
      // Standard desktop size
      useTouch: false,
      progressUpdateInterval: 16
      // 60fps for smooth desktop experience
    };
  }
  getAutoplayConfig(_hasUserInteracted) {
    return {
      shouldStartMuted: false,
      // Desktop usually allows unmuted autoplay
      enableFallbackLoop: false,
      fallbackTimeout: 5e3,
      // 5 second timeout for desktop
      requiresUserInteraction: false
    };
  }
  configureVideoElement(video) {
    const config = this.getVideoElementConfig();
    video.playsInline = config.playsInline;
    video.muted = config.muted;
    video.controls = config.controls;
    video.preload = config.preload;
    Object.entries(config.styles).forEach(([key, value]) => {
      video.style.setProperty(key, value);
    });
  }
  configureControlElement(_element) {
  }
  async handlePlayAttempt(video, _hasUserInteracted) {
    const store = getSaltfishStore();
    const userWantsMuted = store.isMuted;
    video.muted = userWantsMuted;
    video.loop = false;
    try {
      await video.play();
      return true;
    } catch (error2) {
      if (!video.muted) {
        await this.handleAutoplayFallback(video);
        return false;
      } else {
        await this.handleAutoplayFallback(video);
        return false;
      }
    }
  }
  async handleAutoplayFallback(video) {
    video.muted = true;
    video.loop = true;
    try {
      await video.play();
    } catch (fallbackError) {
      console.error("DesktopPlaybackHandler: All autoplay attempts failed");
      throw new Error("Desktop autoplay completely blocked");
    }
  }
  getProgressUpdateFrequency(_isAutoplayFallback) {
    return 16;
  }
  /**
   * Clean up resources when handler is destroyed
   */
  destroy() {
  }
}
function createDevicePlaybackHandler(deviceInfo) {
  if (deviceInfo.isMobile) {
    return new MobilePlaybackHandler(deviceInfo);
  } else {
    return new DesktopPlaybackHandler(deviceInfo);
  }
}
const ICON_CLOSE = `<svg width="18" height="18" viewBox="0 0 256 256" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></svg>`;
const ICON_PLUS = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const ICON_PLAY = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>`;
const ICON_SPEAKER = `<svg width="18" height="18" viewBox="0 0 256 256" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M155.51,24.81a8,8,0,0,0-8.42.88L77.25,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H77.25l69.84,54.31A8,8,0,0,0,160,224V32A8,8,0,0,0,155.51,24.81ZM32,96H72v64H32ZM144,207.64,88,164.09V91.91l56-43.55Zm54-106.08a40,40,0,0,1,0,52.88,8,8,0,0,1-12-10.58,24,24,0,0,0,0-31.72,8,8,0,0,1,12-10.58ZM248,128a79.9,79.9,0,0,1-20.37,53.34,8,8,0,0,1-11.92-10.67,64,64,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67A79.83,79.83,0,0,1,248,128Z"/></svg>`;
const ICON_SPEAKER_MUTED = `<svg width="18" height="18" viewBox="0 0 256 256" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M155.51,24.81a8,8,0,0,0-8.42.88L77.25,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H77.25l69.84,54.31A8,8,0,0,0,160,224V32A8,8,0,0,0,155.51,24.81ZM32,96H72v64H32ZM144,207.64,88,164.09V91.91l56-43.55Zm101.66-61.3a8,8,0,0,1-11.32,11.32L216,139.31l-18.34,18.35a8,8,0,0,1-11.32-11.32L204.69,128l-18.35-18.34a8,8,0,0,1,11.32-11.32L216,116.69l18.34-18.35a8,8,0,0,1,11.32,11.32L227.31,128Z"/></svg>`;
const ICON_CC = `<svg width="18" height="18" viewBox="0 0 256 256" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192ZM118.92,151.71A8,8,0,0,1,116,162.64a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58A8,8,0,0,1,118.92,151.71Zm80,0A8,8,0,0,1,196,162.64a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58A8,8,0,0,1,198.92,151.71Z"/></svg>`;
const ICON_CC_DISABLED = `<svg width="18" height="18" viewBox="0 0 256 256" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192ZM118.92,151.71A8,8,0,0,1,116,162.64a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58A8,8,0,0,1,118.92,151.71Zm80,0A8,8,0,0,1,196,162.64a40,40,0,1,1,0-69.28,8,8,0,1,1-8,13.85,24,24,0,1,0,0,41.58A8,8,0,0,1,198.92,151.71Z"/><line x1="48" y1="48" x2="208" y2="208" stroke="currentColor" stroke-width="16" stroke-linecap="round"/></svg>`;
const ICON_CURSOR = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none"><defs><filter id="cursor-shadow" x="-50%" y="-50%" width="200%" height="200%"><feDropShadow dx="0" dy="1" stdDeviation="1.2" flood-color="rgba(0, 0, 0, 0.22)"/></filter></defs><path d="M3.5 3.5L10.5 20.5L13.3 13.3L20.5 10.5L3.5 3.5Z" fill="#ff7614" stroke="white" stroke-width="0.7" stroke-linejoin="round" stroke-linecap="round" filter="url(#cursor-shadow)"/></svg>`;
const ICON_LOADING_SPINNER = `<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg"><g transform="translate(10, 10) scale(0.656)"><g clip-path="url(#saltfish-clip)"><path d="M61.0002 30.1906C61.0002 46.8644 47.4834 60.3812 30.8097 60.3812C14.136 60.3812 27.1659 46.8644 27.1659 30.1906C27.1659 13.5168 14.136 0 30.8097 0C47.4834 0 61.0002 13.5168 61.0002 30.1906Z" fill="black" fill-opacity="0.9"/><path d="M24.13 29.8618C24.13 40.3565 15.6978 48.8642 5.29602 48.8642C-5.10576 48.8642 3.02294 40.3565 3.02294 29.8618C3.02294 19.3671 -5.10576 10.8594 5.29602 10.8594C15.6978 10.8594 24.13 19.3671 24.13 29.8618Z" fill="black" fill-opacity="0.9"/></g><defs><clipPath id="saltfish-clip"><rect width="61" height="61" fill="white"/></clipPath></defs></g><circle cx="30" cy="30" r="28" stroke="black" stroke-width="2" fill="none" stroke-linecap="round" stroke-dasharray="8 6" stroke-opacity="0.3"><animateTransform attributeName="transform" type="rotate" values="0 30 30;360 30 30" dur="2s" repeatCount="indefinite"/></circle></svg>`;
const ICON_SPEAKER_OUTLINE = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" fill="none"/><path d="M15 9c0.8 0.8 1.5 1.9 1.5 3s-0.7 2.2-1.5 3" stroke="currentColor"/><path d="M19 7c1.6 1.6 2.5 3.8 2.5 6s-0.9 4.4-2.5 6" stroke="currentColor"/></svg>`;
function createCloseIcon(size = 18) {
  return `<svg width="${size}" height="${size}" viewBox="0 0 256 256" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></svg>`;
}
class VideoControlsUI {
  constructor(container, deviceHandler, callbacks) {
    // Container and control elements
    __publicField(this, "container");
    __publicField(this, "controlsElement", null);
    __publicField(this, "progressBar", null);
    __publicField(this, "muteButton", null);
    __publicField(this, "ccButton", null);
    // Configuration and callbacks
    __publicField(this, "deviceHandler");
    __publicField(this, "callbacks");
    // Progress tracking state
    __publicField(this, "updateInterval", null);
    __publicField(this, "animationFrameId", null);
    __publicField(this, "lastTimeupdateEvent", 0);
    __publicField(this, "videoElement", null);
    // Track if buttons have been shown at 90% for current video
    __publicField(this, "buttonsShownAt90Percent", false);
    /**
     * Handles detailed time updates for smooth progress bar and button visibility
     */
    __publicField(this, "handleDetailedTimeUpdate", () => {
      var _a;
      this.lastTimeupdateEvent = Date.now();
      if (this.progressBar && ((_a = this.videoElement) == null ? void 0 : _a.paused) === false) {
        this.progressBar.style.setProperty("transition", "width 0.1s linear");
      }
      if (this.videoElement && !this.buttonsShownAt90Percent && this.videoElement.duration > 0) {
        const progress = this.videoElement.currentTime / this.videoElement.duration;
        if (progress >= 0.9) {
          this.callbacks.on90PercentReached();
          this.buttonsShownAt90Percent = true;
        }
      }
    });
    /**
     * Handles seeking events to ensure smooth progress updates
     */
    __publicField(this, "handleSeeking", () => {
      if (this.progressBar) {
        this.progressBar.style.setProperty("transition", "none");
        this.updateProgress();
      }
    });
    /**
     * Handles seeked events (seeking ended)
     */
    __publicField(this, "handleSeeked", () => {
      if (this.progressBar && this.videoElement) {
        if (!this.videoElement.paused) {
          this.updateProgress();
          void this.progressBar.offsetWidth;
          this.progressBar.style.setProperty("transition", "width 0.1s linear");
        }
      }
    });
    /**
     * Handles click on the progress bar to seek
     */
    __publicField(this, "handleProgressBarClick", (event) => {
      const controls = event.currentTarget;
      if (!this.videoElement || this.videoElement.duration <= 0) return;
      const rect = controls.getBoundingClientRect();
      const clickPosition = (event.clientX - rect.left) / rect.width;
      const seekTime = this.videoElement.duration * clickPosition;
      if (this.progressBar) {
        this.progressBar.style.setProperty("transition", "none");
        this.progressBar.style.setProperty("width", `${clickPosition * 100}%`);
      }
      this.callbacks.onSeek(seekTime);
      event.preventDefault();
      event.stopPropagation();
    });
    this.container = container;
    this.deviceHandler = deviceHandler;
    this.callbacks = callbacks;
  }
  /**
   * Creates all UI control elements
   */
  create(videoElement) {
    this.videoElement = videoElement;
    this.controlsElement = document.createElement("div");
    this.controlsElement.className = "sf-video-container__controls";
    this.container.appendChild(this.controlsElement);
    this.progressBar = document.createElement("div");
    this.progressBar.className = "sf-video-container__progress";
    this.controlsElement.appendChild(this.progressBar);
    const controlsConfig = this.deviceHandler.getControlsConfig();
    if (controlsConfig.useTouch) {
      this.controlsElement.addEventListener("touchend", this.handleProgressBarClick);
    }
    this.controlsElement.addEventListener("click", this.handleProgressBarClick);
    this.muteButton = document.createElement("button");
    this.muteButton.className = "sf-video-container__mute-button";
    this.deviceHandler.configureControlElement(this.muteButton);
    this.muteButton.innerHTML = ICON_SPEAKER_OUTLINE;
    if (controlsConfig.useTouch) {
      this.muteButton.addEventListener("touchend", (event) => {
        event.preventDefault();
        this.toggleMute();
      });
    }
    this.muteButton.addEventListener("click", (event) => {
      event.preventDefault();
      event.stopPropagation();
      this.toggleMute();
    });
    this.container.appendChild(this.muteButton);
    this.updateMuteButtonIcon();
    this.ccButton = document.createElement("button");
    this.ccButton.className = "sf-video-container__cc-button";
    this.deviceHandler.configureControlElement(this.ccButton);
    this.updateCCButtonIcon(false);
    if (controlsConfig.useTouch) {
      this.ccButton.addEventListener("touchend", (event) => {
        event.preventDefault();
      });
    }
    this.ccButton.addEventListener("click", (event) => {
      event.preventDefault();
    });
    this.container.appendChild(this.ccButton);
    if (videoElement) {
      videoElement.addEventListener("seeking", this.handleSeeking);
      videoElement.addEventListener("seeked", this.handleSeeked);
      videoElement.addEventListener("timeupdate", this.handleDetailedTimeUpdate);
    }
  }
  /**
   * Updates the video element reference (used when swapping videos)
   */
  updateVideoElement(videoElement) {
    if (this.videoElement) {
      this.videoElement.removeEventListener("seeking", this.handleSeeking);
      this.videoElement.removeEventListener("seeked", this.handleSeeked);
      this.videoElement.removeEventListener("timeupdate", this.handleDetailedTimeUpdate);
    }
    this.videoElement = videoElement;
    if (videoElement) {
      videoElement.addEventListener("seeking", this.handleSeeking);
      videoElement.addEventListener("seeked", this.handleSeeked);
      videoElement.addEventListener("timeupdate", this.handleDetailedTimeUpdate);
    }
  }
  /**
   * Resets controls for new video
   */
  reset() {
    this.stopProgressTracking();
    this.buttonsShownAt90Percent = false;
    this.lastTimeupdateEvent = 0;
    if (this.progressBar) {
      this.progressBar.style.setProperty("transition", "none");
      this.progressBar.style.setProperty("width", "0%");
    }
  }
  /**
   * Resets the 90% button trigger flag (called when loading new video)
   */
  reset90PercentTrigger() {
    this.buttonsShownAt90Percent = false;
  }
  /**
   * Destroys controls and cleans up
   */
  destroy() {
    this.stopProgressTracking();
    if (this.videoElement) {
      this.videoElement.removeEventListener("seeking", this.handleSeeking);
      this.videoElement.removeEventListener("seeked", this.handleSeeked);
      this.videoElement.removeEventListener("timeupdate", this.handleDetailedTimeUpdate);
    }
    if (this.controlsElement) {
      const controlsConfig = this.deviceHandler.getControlsConfig();
      if (controlsConfig.useTouch) {
        this.controlsElement.removeEventListener("touchend", this.handleProgressBarClick);
      }
      this.controlsElement.removeEventListener("click", this.handleProgressBarClick);
    }
    this.controlsElement = null;
    this.progressBar = null;
    this.muteButton = null;
    this.ccButton = null;
    this.videoElement = null;
  }
  /**
   * Starts progress tracking using RAF or setTimeout based on device
   */
  startProgressTracking() {
    this.stopProgressTracking();
    if (!this.videoElement) return;
    const isAutoplayFallback = this.videoElement.loop && this.videoElement.muted;
    const updateFrequency = this.deviceHandler.getProgressUpdateFrequency(isAutoplayFallback);
    if (updateFrequency >= 16) {
      const updateFrame = () => {
        if (this.videoElement && !this.videoElement.paused && !this.videoElement.ended) {
          this.updateProgress();
        }
        this.animationFrameId = requestAnimationFrame(updateFrame);
      };
      this.animationFrameId = requestAnimationFrame(updateFrame);
    } else {
      const updateInterval = () => {
        if (this.updateInterval !== null && this.videoElement && !this.videoElement.paused && !this.videoElement.ended) {
          this.updateProgress();
          this.updateInterval = window.setTimeout(updateInterval, updateFrequency);
        }
      };
      this.updateInterval = window.setTimeout(updateInterval, updateFrequency);
    }
  }
  /**
   * Stops progress tracking
   */
  stopProgressTracking() {
    if (this.updateInterval !== null) {
      window.clearInterval(this.updateInterval);
      this.updateInterval = null;
    }
    if (this.animationFrameId !== null) {
      cancelAnimationFrame(this.animationFrameId);
      this.animationFrameId = null;
    }
  }
  /**
   * Immediately updates progress bar to specific values (for pause/seek)
   */
  updateProgressImmediate(currentTime, duration) {
    if (!this.progressBar || duration <= 0) return;
    const percent = currentTime / duration * 100;
    this.progressBar.style.setProperty("transition", "none");
    this.progressBar.style.setProperty("width", `${percent}%`);
  }
  /**
   * Updates the progress bar based on current playback position
   */
  updateProgress() {
    if (!this.progressBar || !this.videoElement) return;
    const currentTime = this.videoElement.currentTime || 0;
    const duration = this.videoElement.duration || 0;
    if (duration > 0) {
      const percent = currentTime / duration * 100;
      const wasTransitioning = this.progressBar.style.getPropertyValue("transition") !== "none";
      const timeSinceLastUpdate = Date.now() - this.lastTimeupdateEvent;
      this.progressBar.style.setProperty("transition", "none");
      this.progressBar.style.setProperty("width", `${percent}%`);
      void this.progressBar.offsetWidth;
      if (this.videoElement.ended) {
        this.progressBar.style.setProperty("transition", "width 0.2s ease-out");
      } else if (!this.videoElement.paused && !this.videoElement.seeking && wasTransitioning && timeSinceLastUpdate < 500) {
        this.progressBar.style.setProperty("transition", "width 0.1s linear");
      }
    }
  }
  /**
   * Sets the muted state
   */
  setMuted(muted) {
    const store = getSaltfishStore();
    store.setMuted(muted);
    this.updateMuteButtonIcon();
  }
  /**
   * Toggles the muted state
   */
  toggleMute() {
    const store = getSaltfishStore();
    const newMutedState = !store.isMuted;
    this.setMuted(newMutedState);
    this.callbacks.onMuteToggle();
  }
  /**
   * Gets the current muted state
   */
  getMuted() {
    const store = getSaltfishStore();
    return store.isMuted;
  }
  /**
   * Updates the mute button icon based on muted state
   */
  updateMuteButtonIcon() {
    if (!this.muteButton) return;
    const store = getSaltfishStore();
    const isMuted = store.isMuted;
    if (isMuted) {
      this.muteButton.innerHTML = ICON_SPEAKER_MUTED;
    } else {
      this.muteButton.innerHTML = ICON_SPEAKER;
    }
  }
  /**
   * Updates the CC button icon based on enabled state
   */
  updateCCButtonIcon(isEnabled) {
    if (!this.ccButton) return;
    if (isEnabled) {
      this.ccButton.innerHTML = ICON_CC;
    } else {
      this.ccButton.innerHTML = ICON_CC_DISABLED;
    }
  }
  /**
   * Shows the progress bar
   */
  showProgressBar() {
    if (this.controlsElement) {
      const playerElement = this.controlsElement.closest(".sf-player");
      const isMinimized = playerElement == null ? void 0 : playerElement.classList.contains("sf-player--minimized");
      if (!isMinimized) {
        this.controlsElement.classList.remove("sf-hidden");
      } else {
        this.controlsElement.classList.remove("sf-hidden");
      }
    }
    if (this.progressBar) {
      this.progressBar.style.setProperty("transition", "none");
      this.progressBar.style.setProperty("width", "0%");
      void this.progressBar.offsetWidth;
      this.progressBar.style.setProperty("transition", "width 0.1s linear");
    }
  }
  /**
   * Hides the progress bar
   */
  hideProgressBar() {
    if (this.controlsElement) {
      this.controlsElement.classList.add("sf-hidden");
    }
  }
  /**
   * Shows the mute button
   */
  showMuteButton() {
    if (this.muteButton) {
      this.muteButton.classList.remove("sf-hidden");
    }
  }
  /**
   * Hides the mute button
   */
  hideMuteButton() {
    if (this.muteButton) {
      this.muteButton.classList.add("sf-hidden");
    }
  }
  /**
   * Gets the CC button element (for TranscriptManager)
   */
  getCCButton() {
    return this.ccButton;
  }
}
class AudioVisualizationManager {
  constructor() {
    __publicField(this, "audioContext", null);
    __publicField(this, "analyser", null);
    __publicField(this, "sourceVideo1", null);
    __publicField(this, "sourceVideo2", null);
    __publicField(this, "activeSource", null);
    __publicField(this, "videoElement1", null);
    __publicField(this, "videoElement2", null);
    __publicField(this, "dataArray", null);
    __publicField(this, "animationId", null);
    __publicField(this, "isInitialized", false);
    __publicField(this, "useFallbackMode", false);
    // Use animated fallback if Web Audio fails
    __publicField(this, "isPaused", false);
    // Pause state - when true, bars stay at minimal height
    // Configuration
    __publicField(this, "fftSize", 2048);
    __publicField(this, "barCount", 12);
    // Moderate detail (10-15 range)
    __publicField(this, "smoothingTimeConstant", 0.75);
    // Smooth out rapid changes
    // Fallback animation state
    __publicField(this, "fallbackTime", 0);
    // CORS detection - wait for multiple frames before declaring failure
    __publicField(this, "zeroDataFrameCount", 0);
    __publicField(this, "maxZeroFramesBeforeFallback", 60);
  }
  // ~1 second at 60fps
  /**
   * Initializes the Web Audio API context and connects to the video element
   * Handles dual video element system by creating sources for both elements
   * @param videoElement - The video/audio element to analyze
   */
  initialize(videoElement) {
    try {
      if (!this.isInitialized) {
        const AudioContextClass = window.AudioContext || window.webkitAudioContext;
        this.audioContext = new AudioContextClass();
        this.analyser = this.audioContext.createAnalyser();
        this.analyser.fftSize = this.fftSize;
        this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
        const bufferLength = this.analyser.frequencyBinCount;
        this.dataArray = new Uint8Array(bufferLength);
        this.analyser.connect(this.audioContext.destination);
        this.isInitialized = true;
        this.useFallbackMode = false;
        this.zeroDataFrameCount = 0;
      }
      if (this.videoElement1 === videoElement) {
        this.resumeAudioContext();
        this.switchToSource(this.sourceVideo1);
        return;
      }
      if (this.videoElement2 === videoElement) {
        this.resumeAudioContext();
        this.switchToSource(this.sourceVideo2);
        return;
      }
      this.resumeAudioContext();
      const newSource = this.audioContext.createMediaElementSource(videoElement);
      if (!this.videoElement1) {
        this.videoElement1 = videoElement;
        this.sourceVideo1 = newSource;
        this.switchToSource(newSource);
      } else if (!this.videoElement2) {
        this.videoElement2 = videoElement;
        this.sourceVideo2 = newSource;
        this.switchToSource(newSource);
      } else {
        console.warn("AudioVisualizationManager: More than 2 video elements detected - this should not happen with dual video system");
      }
      this.zeroDataFrameCount = 0;
    } catch (error2) {
      console.warn("AudioVisualizationManager: Web Audio API failed, using fallback animation", error2);
      this.isInitialized = true;
      this.useFallbackMode = true;
    }
  }
  /**
   * Switches active source and connects it to the analyser
   * Disconnects previous source to avoid audio doubling
   */
  switchToSource(source) {
    if (this.activeSource && this.activeSource !== source) {
      this.activeSource.disconnect();
    }
    source.connect(this.analyser);
    this.activeSource = source;
  }
  /**
   * Gets frequency data for visualization bars
   * Maps lower frequencies to left bars (bass) and higher frequencies to right bars (treble)
   * Falls back to animated visualization if Web Audio API fails or CORS blocks analysis
   * Returns flat bars when paused
   * @returns Array of normalized bar heights (0-1) for each bar
   */
  getBarHeights() {
    if (this.isPaused) {
      return this.getFlatBarHeights();
    }
    if (this.useFallbackMode || !this.analyser || !this.dataArray) {
      return this.getFallbackBarHeights();
    }
    this.analyser.getByteFrequencyData(this.dataArray);
    const hasNonZeroData = this.dataArray.some((value) => value > 0);
    if (!hasNonZeroData) {
      this.zeroDataFrameCount++;
      if (this.zeroDataFrameCount >= this.maxZeroFramesBeforeFallback) {
        if (!this.useFallbackMode) {
          console.warn(
            "AudioVisualizationManager: Detected CORS blocking audio analysis after",
            this.zeroDataFrameCount,
            "frames, switching to fallback animation"
          );
          this.useFallbackMode = true;
        }
      }
      return this.getFallbackBarHeights();
    }
    this.zeroDataFrameCount = 0;
    if (this.useFallbackMode && hasNonZeroData) {
      this.useFallbackMode = false;
    }
    const halfBarCount = Math.floor(this.barCount / 2);
    const barHeights = [];
    const bufferLength = this.dataArray.length;
    const samplesPerBar = Math.floor(bufferLength / this.barCount);
    for (let i = 0; i < halfBarCount; i++) {
      let sum = 0;
      const startIndex = i * samplesPerBar;
      const endIndex = startIndex + samplesPerBar;
      for (let j = startIndex; j < endIndex && j < bufferLength; j++) {
        sum += this.dataArray[j];
      }
      const average = sum / samplesPerBar;
      const normalized = average / 255;
      barHeights.push(normalized);
    }
    const reversedBars = [...barHeights].reverse();
    reversedBars.pop();
    const mirroredBars = reversedBars.concat(barHeights);
    return mirroredBars;
  }
  /**
   * Generates fallback bar heights when Web Audio API is unavailable
   * Returns all zeros (no animation) for audio-only content
   * Mirrors the first 50% to match real audio visualization
   * @returns Array of normalized bar heights (0-1) all at zero
   */
  getFallbackBarHeights() {
    const halfBarCount = Math.floor(this.barCount / 2);
    const barHeights = [];
    for (let i = 0; i < halfBarCount; i++) {
      barHeights.push(0);
    }
    const reversedBars = [...barHeights].reverse();
    reversedBars.pop();
    const mirroredBars = reversedBars.concat(barHeights);
    return mirroredBars;
  }
  /**
   * Returns flat bar heights for paused state
   * All bars at minimal height for visual presence
   * @returns Array of normalized bar heights (0-1) all at minimal value
   */
  getFlatBarHeights() {
    const halfBarCount = Math.floor(this.barCount / 2);
    const barHeights = [];
    for (let i = 0; i < halfBarCount; i++) {
      barHeights.push(0.1);
    }
    const reversedBars = [...barHeights].reverse();
    reversedBars.pop();
    const mirroredBars = reversedBars.concat(barHeights);
    return mirroredBars;
  }
  /**
   * Gets the average amplitude across all frequencies
   * Used for dynamic color intensity
   * Falls back to zero if Web Audio API fails
   * @returns Normalized amplitude value (0-1)
   */
  getAverageAmplitude() {
    if (this.useFallbackMode || !this.analyser || !this.dataArray) {
      return 0;
    }
    this.analyser.getByteFrequencyData(this.dataArray);
    let sum = 0;
    for (let i = 0; i < this.dataArray.length; i++) {
      sum += this.dataArray[i];
    }
    const average = sum / this.dataArray.length;
    const normalized = average / 255;
    if (normalized === 0) {
      return 0;
    }
    return normalized;
  }
  /**
   * Starts the animation loop for continuous visualization updates
   * @param callback - Function called on each animation frame with bar heights and amplitude
   */
  startVisualization(callback) {
    if (!this.isInitialized) {
      console.warn("AudioVisualizationManager: Cannot start visualization - not initialized");
      return;
    }
    if (this.animationId !== null) {
      this.stopVisualization();
    }
    if (this.audioContext && this.audioContext.state === "suspended") {
      this.audioContext.resume();
    }
    this.fallbackTime = 0;
    this.zeroDataFrameCount = 0;
    const animate = () => {
      this.fallbackTime += 16;
      const barHeights = this.getBarHeights();
      const amplitude = this.getAverageAmplitude();
      callback(barHeights, amplitude);
      this.animationId = requestAnimationFrame(animate);
    };
    animate();
  }
  /**
   * Stops the visualization animation loop
   */
  stopVisualization() {
    if (this.animationId !== null) {
      cancelAnimationFrame(this.animationId);
      this.animationId = null;
    }
  }
  /**
   * Pauses the visualization - bars will stay at minimal height
   * Animation loop continues but returns flat bars
   */
  pause() {
    this.isPaused = true;
  }
  /**
   * Resumes the visualization - bars will react to audio again
   */
  resume() {
    this.isPaused = false;
  }
  /**
   * Resumes the AudioContext if it's suspended
   * Safe to call multiple times - will only resume if needed
   * MUST be called after user interaction for audio to work
   */
  resumeAudioContext() {
    if (this.audioContext && this.audioContext.state === "suspended") {
      this.audioContext.resume().catch((error2) => {
        console.warn("AudioVisualizationManager: Failed to resume AudioContext", error2);
      });
    }
  }
  /**
   * Resets fallback mode to attempt real-time audio analysis again
   * Call this when unmuting or when conditions change that might allow audio analysis
   */
  resetFallbackMode() {
    if (this.useFallbackMode) {
      this.useFallbackMode = false;
      this.zeroDataFrameCount = 0;
    }
  }
  /**
   * Checks if visualization is currently initialized
   */
  getIsInitialized() {
    return this.isInitialized;
  }
  /**
   * Resets video element references for playlist restart
   * Keeps AudioContext and analyser intact but clears video element tracking
   * This allows new video elements to be assigned when playlist restarts
   */
  reset() {
    this.stopVisualization();
    if (this.sourceVideo1) {
      try {
        this.sourceVideo1.disconnect();
      } catch (e) {
      }
    }
    if (this.sourceVideo2) {
      try {
        this.sourceVideo2.disconnect();
      } catch (e) {
      }
    }
    this.videoElement1 = null;
    this.videoElement2 = null;
    this.sourceVideo1 = null;
    this.sourceVideo2 = null;
    this.activeSource = null;
    this.useFallbackMode = false;
    this.isPaused = false;
    this.fallbackTime = 0;
    this.zeroDataFrameCount = 0;
  }
  /**
   * Cleans up Web Audio resources
   * Important to call this to prevent memory leaks
   * WARNING: Only call this when truly destroying - not when switching video elements!
   */
  cleanup() {
    this.stopVisualization();
    if (this.sourceVideo1) {
      this.sourceVideo1.disconnect();
    }
    if (this.sourceVideo2) {
      this.sourceVideo2.disconnect();
    }
    if (this.analyser) {
      this.analyser.disconnect();
    }
    if (this.audioContext && this.audioContext.state !== "closed") {
      this.audioContext.close();
    }
    this.audioContext = null;
    this.analyser = null;
    this.sourceVideo1 = null;
    this.sourceVideo2 = null;
    this.activeSource = null;
    this.videoElement1 = null;
    this.videoElement2 = null;
    this.dataArray = null;
    this.isInitialized = false;
    this.useFallbackMode = false;
    this.isPaused = false;
    this.fallbackTime = 0;
    this.zeroDataFrameCount = 0;
  }
  /**
   * Destroys the manager and releases all resources
   */
  destroy() {
    this.cleanup();
  }
}
class VideoManager {
  constructor() {
    // Dual video elements for seamless transitions
    __publicField(this, "currentVideo", null);
    __publicField(this, "nextVideo", null);
    __publicField(this, "activeVideoIndex", 0);
    // Tracks which video is currently active
    __publicField(this, "container", null);
    __publicField(this, "controls", null);
    __publicField(this, "transcriptManager");
    __publicField(this, "preloadedVideos", /* @__PURE__ */ new Map());
    __publicField(this, "audioFallbackOverlay", null);
    __publicField(this, "audioVisualizationManager");
    __publicField(this, "soundbarElement", null);
    // Track the current video URL and position
    __publicField(this, "currentVideoUrl", "");
    __publicField(this, "nextVideoUrl", "");
    __publicField(this, "playbackPositions", /* @__PURE__ */ new Map());
    // Controls how video handles completion
    __publicField(this, "completionPolicy", "auto");
    __publicField(this, "videoEndedCallback", null);
    // Device-specific playback handling
    __publicField(this, "deviceHandler");
    __publicField(this, "deviceChangeCleanup", null);
    __publicField(this, "hasUserInteracted", false);
    // Autoplay fallback timeout
    __publicField(this, "autoplayFallbackTimeout", null);
    /**
     * Handles video ended event
     */
    __publicField(this, "handleVideoEnded", (event) => {
      const video = event.target;
      const activeVideo = this.getActiveVideo();
      if (video !== activeVideo) {
        return;
      }
      const store = getSaltfishStore();
      if (store.currentState === "autoplayBlocked" || store.currentState === "idleMode") {
        store.currentState === "autoplayBlocked" ? "autoplay blocked" : "idle";
        return;
      }
      if (this.controls && activeVideo) {
        this.controls.updateProgressImmediate(activeVideo.duration, activeVideo.duration);
      }
      if (activeVideo) {
        activeVideo.classList.add("sf-video-container__video--blurred");
      }
      if (this.completionPolicy === "auto") {
        this.handleAutoVideoEnded();
      } else {
        this.handleManualVideoEnded();
      }
    });
    /**
     * Handles video time update (for debugging)
     */
    __publicField(this, "handleTimeUpdate", (event) => {
      const video = event.target;
      const activeVideo = this.getActiveVideo();
      if (video === activeVideo && Math.floor(video.currentTime) % 10 === 0) ;
    });
    /**
     * Handles video error event
     */
    __publicField(this, "handleVideoError", (event) => {
      const video = event.target;
      const activeVideo = this.getActiveVideo();
      if (video === activeVideo) {
        console.error("VideoManager: Video error", video.error);
      }
    });
    /**
     * Handles video ended event for automatic completion policy
     */
    __publicField(this, "handleAutoVideoEnded", () => {
      if (this.videoEndedCallback) {
        this.videoEndedCallback();
      }
    });
    /**
     * Handles video ended event for manual completion policy
     */
    __publicField(this, "handleManualVideoEnded", () => {
      if (this.videoEndedCallback) {
        this.videoEndedCallback();
      }
    });
    const deviceInfo = DeviceDetector.getDeviceInfo();
    this.deviceHandler = createDevicePlaybackHandler(deviceInfo);
    this.transcriptManager = new TranscriptManager();
    this.audioVisualizationManager = new AudioVisualizationManager();
    this.deviceChangeCleanup = DeviceDetector.onDeviceChange((newDeviceInfo) => {
      this.deviceHandler.updateDeviceInfo(newDeviceInfo);
    });
  }
  /**
   * Gets current device information
   * @returns DeviceInfo object with device details
   */
  getDeviceInfo() {
    return DeviceDetector.getDeviceInfo();
  }
  /**
   * Checks if the current device is mobile
   * @returns boolean indicating if device is mobile
   */
  isMobileDevice() {
    return this.getDeviceInfo().isMobile;
  }
  /**
   * Creates video player elements
   * @param container - The container element for the video player
   */
  create(container) {
    this.container = document.createElement("div");
    this.container.className = "sf-video-container";
    container.appendChild(this.container);
    this.currentVideo = document.createElement("video");
    this.currentVideo.className = "sf-video-container__video sf-video-container__video--current";
    this.currentVideo.crossOrigin = "anonymous";
    this.deviceHandler.configureVideoElement(this.currentVideo);
    this.container.appendChild(this.currentVideo);
    this.nextVideo = document.createElement("video");
    this.nextVideo.className = "sf-video-container__video sf-video-container__video--next sf-hidden";
    this.nextVideo.crossOrigin = "anonymous";
    this.deviceHandler.configureVideoElement(this.nextVideo);
    this.container.appendChild(this.nextVideo);
    const store = getSaltfishStore();
    if (store.isMuted) {
      this.currentVideo.muted = true;
      this.nextVideo.muted = true;
    }
    const controlsCallbacks = {
      onSeek: (time) => this.seek(time),
      onMuteToggle: () => {
        var _a;
        const isMuted = ((_a = this.controls) == null ? void 0 : _a.getMuted()) ?? false;
        if (this.currentVideo) this.currentVideo.muted = isMuted;
        if (this.nextVideo) this.nextVideo.muted = isMuted;
        if (!isMuted && this.audioVisualizationManager.getIsInitialized()) {
          this.audioVisualizationManager.resetFallbackMode();
        }
      },
      on90PercentReached: () => {
        if (this.container) {
          const event = new CustomEvent("video90PercentReached", {
            bubbles: true,
            detail: { timestamp: Date.now() }
          });
          this.container.dispatchEvent(event);
        }
      }
    };
    this.controls = new VideoControlsUI(this.container, this.deviceHandler, controlsCallbacks);
    this.controls.create(this.currentVideo);
    this.addEventListeners();
    if (this.currentVideo && this.controls) {
      const ccButton = this.controls.getCCButton();
      if (ccButton) {
        this.transcriptManager.setup(this.currentVideo, ccButton, this);
      }
    }
  }
  /**
   * Returns the currently active video element
   */
  getActiveVideo() {
    return this.activeVideoIndex === 0 ? this.currentVideo : this.nextVideo;
  }
  /**
   * Returns the inactive video element (for preloading)
   */
  getInactiveVideo() {
    return this.activeVideoIndex === 0 ? this.nextVideo : this.currentVideo;
  }
  /**
   * Swaps between the two video elements
   */
  swapVideos() {
    const activeVideo = this.getActiveVideo();
    const inactiveVideo = this.getInactiveVideo();
    if (!activeVideo || !inactiveVideo) {
      return;
    }
    activeVideo.pause();
    const store = getSaltfishStore();
    inactiveVideo.muted = store.isMuted;
    activeVideo.classList.add("sf-hidden");
    inactiveVideo.classList.remove("sf-hidden");
    this.activeVideoIndex = this.activeVideoIndex === 0 ? 1 : 0;
    this.currentVideoUrl = this.nextVideoUrl;
    this.nextVideoUrl = "";
  }
  /**
   * Loads a video from URL
   * @param url - URL of the video to load
   */
  async loadVideo(url) {
    var _a;
    const activeVideo = this.getActiveVideo();
    if (!activeVideo) {
      return;
    }
    if (!url || url.trim() === "") {
      throw new Error("Cannot load video: No video URL provided for this step");
    }
    if (this.controls) {
      this.controls.reset90PercentTrigger();
    }
    const store = getSaltfishStore();
    activeVideo.muted = store.isMuted;
    try {
      if (this.controls) {
        this.controls.reset();
      }
      const store2 = getSaltfishStore();
      const isPersistenceEnabled = ((_a = store2.playlistOptions) == null ? void 0 : _a.persistence) ?? true;
      if (this.currentVideoUrl === url && activeVideo.src && (activeVideo.src === url || activeVideo.src.endsWith(url))) {
        if (isPersistenceEnabled) {
          const savedPosition = this.playbackPositions.get(url);
          if (savedPosition !== void 0 && savedPosition > 0 && Math.abs(activeVideo.currentTime - savedPosition) > 0.5) {
            activeVideo.currentTime = savedPosition;
          }
        }
        return;
      }
      if (isPersistenceEnabled && this.currentVideoUrl && activeVideo.currentTime > 0) {
        this.playbackPositions.set(this.currentVideoUrl, activeVideo.currentTime);
      }
      const inactiveVideo = this.getInactiveVideo();
      if (inactiveVideo && this.nextVideoUrl === url) {
        this.swapVideos();
        const activeVideoAfterSwap = this.getActiveVideo();
        if (activeVideoAfterSwap) {
          this.transcriptManager.updateVideoElement(activeVideoAfterSwap);
          if (this.controls) {
            this.controls.updateVideoElement(activeVideoAfterSwap);
          }
        }
        await new Promise((resolve) => {
          const activeVideo2 = this.getActiveVideo();
          if (!activeVideo2) {
            return resolve();
          }
          if (activeVideo2.readyState >= 3) {
            resolve();
          } else {
            const onCanPlay = () => {
              activeVideo2.removeEventListener("canplay", onCanPlay);
              resolve();
            };
            activeVideo2.addEventListener("canplay", onCanPlay);
          }
        });
        return;
      }
      this.currentVideoUrl = url;
      const preloadedVideo = this.preloadedVideos.get(url);
      if (preloadedVideo) {
        const objectUrl = URL.createObjectURL(preloadedVideo);
        activeVideo.src = objectUrl;
        this.preloadedVideos.delete(url);
      } else {
        activeVideo.src = url;
      }
      activeVideo.load();
      await new Promise((resolve, reject) => {
        if (!activeVideo) {
          return resolve(void 0);
        }
        let loadTimeout;
        const onLoadedData = () => {
          if (!activeVideo) {
            return;
          }
          clearTimeout(loadTimeout);
          if (isPersistenceEnabled) {
            const savedPosition = this.playbackPositions.get(url);
            if (savedPosition !== void 0 && savedPosition > 0) {
              const safePosition = Math.min(savedPosition, activeVideo.duration - 0.5);
              activeVideo.currentTime = safePosition;
            }
          } else {
            activeVideo.currentTime = 0;
          }
          this.transcriptManager.updateVideoElement(activeVideo);
          if (this.controls) {
            this.controls.updateVideoElement(activeVideo);
          }
          activeVideo.removeEventListener("loadeddata", onLoadedData);
          activeVideo.removeEventListener("error", onError);
          resolve(void 0);
        };
        const onError = (error2) => {
          clearTimeout(loadTimeout);
          const videoElement = error2.target;
          const mediaError = videoElement == null ? void 0 : videoElement.error;
          console.error("VideoManager: Video load error:", mediaError);
          activeVideo.removeEventListener("loadeddata", onLoadedData);
          activeVideo.removeEventListener("error", onError);
          const enrichedError = new Error(`Video load failed: ${(mediaError == null ? void 0 : mediaError.message) || "Unknown error"}`);
          enrichedError.videoUrl = url;
          enrichedError.mediaErrorCode = (mediaError == null ? void 0 : mediaError.code) || null;
          enrichedError.mediaErrorMessage = (mediaError == null ? void 0 : mediaError.message) || null;
          enrichedError.failureReason = "media_error";
          reject(enrichedError);
        };
        loadTimeout = window.setTimeout(() => {
          console.error("VideoManager: Video load timeout after 10 seconds");
          activeVideo.removeEventListener("loadeddata", onLoadedData);
          activeVideo.removeEventListener("error", onError);
          const timeoutError = new Error("Video load timeout");
          timeoutError.videoUrl = url;
          timeoutError.mediaErrorCode = null;
          timeoutError.mediaErrorMessage = null;
          timeoutError.failureReason = "timeout";
          reject(timeoutError);
        }, 1e4);
        activeVideo.addEventListener("loadeddata", onLoadedData);
        activeVideo.addEventListener("error", onError);
      });
    } catch (error2) {
      console.error("VideoManager: Failed to load video:", error2);
      if (error2 && typeof error2 === "object" && "videoUrl" in error2) {
        throw error2;
      }
      const enrichedError = new Error("Failed to load video");
      enrichedError.videoUrl = url;
      enrichedError.mediaErrorCode = null;
      enrichedError.mediaErrorMessage = null;
      enrichedError.failureReason = "load_error";
      throw enrichedError;
    }
  }
  /**
   * Preloads a video for future playback to reduce transition delays
   * @param url - URL of the video to preload
   */
  preloadNextVideo(url) {
    if (!url || this.preloadedVideos.has(url)) {
      return;
    }
    if (this.currentVideoUrl === url) {
      return;
    }
    if (this.nextVideoUrl === url) {
      return;
    }
    const inactiveVideo = this.getInactiveVideo();
    if (inactiveVideo) {
      this.nextVideoUrl = url;
      inactiveVideo.src = url;
      inactiveVideo.load();
      inactiveVideo.preload = "auto";
      const store = getSaltfishStore();
      inactiveVideo.muted = store.isMuted;
    } else {
      fetch(url).then((response) => {
        if (!response.ok) {
          throw new Error(`Failed to fetch video: ${response.statusText}`);
        }
        return response.blob();
      }).then((blob) => {
        this.preloadedVideos.set(url, blob);
      }).catch((error2) => {
        console.error(`VideoManager: Error preloading video ${url}:`, error2);
      });
    }
  }
  /**
   * Plays the video
   */
  play() {
    var _a;
    const activeVideo = this.getActiveVideo();
    if (!activeVideo) {
      console.error("VideoManager: No active video element found");
      return;
    }
    activeVideo.classList.remove("sf-video-container__video--blurred");
    if (activeVideo.ended) {
      activeVideo.currentTime = 0;
    }
    const store = getSaltfishStore();
    const isPersistenceEnabled = ((_a = store.playlistOptions) == null ? void 0 : _a.persistence) ?? true;
    if (isPersistenceEnabled && this.currentVideoUrl) {
      const savedPosition = this.playbackPositions.get(this.currentVideoUrl);
      if (savedPosition && Math.abs(activeVideo.currentTime - savedPosition) > 0.5) {
        activeVideo.currentTime = savedPosition;
      }
    }
    activeVideo.loop = false;
    if (this.audioVisualizationManager.getIsInitialized()) {
      this.audioVisualizationManager.resumeAudioContext();
    }
    if (!activeVideo.paused) {
      if (this.controls) {
        this.controls.startProgressTracking();
      }
      if (this.audioVisualizationManager.getIsInitialized()) {
        this.audioVisualizationManager.resume();
      }
      return;
    }
    this.deviceHandler.handlePlayAttempt(activeVideo, this.hasUserInteracted).then((playSucceeded) => {
      if (!activeVideo) {
        return;
      }
      if (playSucceeded) {
        if (this.controls) {
          this.controls.startProgressTracking();
        }
        if (this.audioVisualizationManager.getIsInitialized()) {
          this.audioVisualizationManager.resume();
        }
      } else {
        store.setAutoplayFallback();
        if (this.isMobileDevice()) {
          activeVideo.playsInline = true;
          activeVideo.setAttribute("playsinline", "true");
          activeVideo.setAttribute("webkit-playsinline", "true");
          setTimeout(() => {
            if (activeVideo.paused) {
              activeVideo.play().catch(() => {
              });
            }
          }, 200);
        }
      }
    }).catch(() => {
      console.warn("VideoManager: Autoplay handler threw error - browser has strict autoplay policy");
      const store2 = getSaltfishStore();
      store2.setAutoplayFallback();
    });
  }
  /**
   * Pauses the video
   */
  pause() {
    const activeVideo = this.getActiveVideo();
    if (!activeVideo) {
      return;
    }
    if (activeVideo.paused) {
      return;
    }
    activeVideo.muted = true;
    if (this.controls) {
      this.controls.updateProgressImmediate(activeVideo.currentTime, activeVideo.duration);
    }
    activeVideo.pause();
    if (this.audioVisualizationManager.getIsInitialized()) {
      this.audioVisualizationManager.pause();
    }
    if (this.controls) {
      this.controls.stopProgressTracking();
    }
  }
  /**
   * Seeks to a specified time in the video
   * @param time - The time to seek to in seconds
   */
  seek(time) {
    const activeVideo = this.getActiveVideo();
    if (!activeVideo) {
      return;
    }
    if (this.controls && activeVideo.duration > 0) {
      this.controls.updateProgressImmediate(time, activeVideo.duration);
    }
    activeVideo.currentTime = time;
  }
  /**
   * Gets the current playback time of the video
   * @returns The current time in seconds
   */
  getCurrentTime() {
    const activeVideo = this.getActiveVideo();
    return activeVideo ? activeVideo.currentTime : 0;
  }
  /**
   * Gets the duration of the video
   * @returns The duration in seconds
   */
  getDuration() {
    const activeVideo = this.getActiveVideo();
    return activeVideo ? activeVideo.duration : 0;
  }
  /**
   * Gets the active video element
   * @returns The video element
   */
  getVideoElement() {
    return this.getActiveVideo();
  }
  /**
   * Destroys the video player and cleans up resources
   */
  /**
   * Reset the VideoManager to a clean state for new playlist
   * Clears cached data and resets internal state without destroying DOM elements
   */
  reset() {
    if (this.controls) {
      this.controls.reset();
    }
    this.preloadedVideos.clear();
    this.playbackPositions.clear();
    this.currentVideoUrl = "";
    this.nextVideoUrl = "";
    this.activeVideoIndex = 0;
    this.hasUserInteracted = false;
    if (this.autoplayFallbackTimeout !== null) {
      window.clearTimeout(this.autoplayFallbackTimeout);
      this.autoplayFallbackTimeout = null;
    }
    this.completionPolicy = "auto";
    this.videoEndedCallback = null;
    this.transcriptManager.reset();
    this.hideAudioFallbackOverlay();
    const isWebAudioActive = this.audioVisualizationManager.getIsInitialized();
    if (isWebAudioActive) {
      this.audioVisualizationManager.reset();
    }
    if (this.currentVideo) {
      if (!isWebAudioActive) {
        this.currentVideo.src = "";
      }
      this.currentVideo.currentTime = 0;
      this.currentVideo.pause();
    }
    if (this.nextVideo) {
      if (!isWebAudioActive) {
        this.nextVideo.src = "";
      }
      this.nextVideo.currentTime = 0;
      this.nextVideo.pause();
    }
  }
  async destroy() {
    this.removeEventListeners();
    if (this.controls) {
      this.controls.destroy();
      this.controls = null;
    }
    this.transcriptManager.destroy();
    this.hideAudioFallbackOverlay();
    if (this.autoplayFallbackTimeout !== null) {
      window.clearTimeout(this.autoplayFallbackTimeout);
      this.autoplayFallbackTimeout = null;
    }
    if (this.deviceChangeCleanup) {
      this.deviceChangeCleanup();
      this.deviceChangeCleanup = null;
    }
    if (this.container && this.container.parentNode) {
      this.container.parentNode.removeChild(this.container);
    }
    this.currentVideo = null;
    this.nextVideo = null;
    this.container = null;
    this.currentVideoUrl = "";
    this.nextVideoUrl = "";
    this.videoEndedCallback = null;
  }
  /**
   * Adds event listeners to the video elements
   */
  addEventListeners() {
    const currentVideo = this.currentVideo;
    const nextVideo = this.nextVideo;
    if (currentVideo) {
      currentVideo.addEventListener("ended", this.handleVideoEnded);
      currentVideo.addEventListener("error", this.handleVideoError);
    }
    if (nextVideo) {
      nextVideo.addEventListener("ended", this.handleVideoEnded);
      nextVideo.addEventListener("error", this.handleVideoError);
    }
  }
  /**
   * Removes event listeners from the video elements
   */
  removeEventListeners() {
    const currentVideo = this.currentVideo;
    const nextVideo = this.nextVideo;
    if (currentVideo) {
      currentVideo.removeEventListener("ended", this.handleVideoEnded);
      currentVideo.removeEventListener("error", this.handleVideoError);
    }
    if (nextVideo) {
      nextVideo.removeEventListener("ended", this.handleVideoEnded);
      nextVideo.removeEventListener("error", this.handleVideoError);
    }
  }
  /**
   * Sets the muted state of both video elements
   * @param muted - Whether to mute the video
   */
  setMuted(muted) {
    if (this.currentVideo) {
      this.currentVideo.muted = muted;
    }
    if (this.nextVideo) {
      this.nextVideo.muted = muted;
    }
    if (this.controls) {
      this.controls.setMuted(muted);
    }
    if (!muted && this.audioVisualizationManager.getIsInitialized()) {
      this.audioVisualizationManager.resetFallbackMode();
    }
  }
  /**
   * Updates the CC button icon based on enabled state
   */
  updateCCButtonIcon(isEnabled) {
    if (this.controls) {
      this.controls.updateCCButtonIcon(isEnabled);
    }
  }
  /**
   * Checks if the video is currently muted
   * @returns Whether the video is muted
   */
  isMuted() {
    var _a;
    return ((_a = this.controls) == null ? void 0 : _a.getMuted()) ?? true;
  }
  /**
   * Shows the progress bar
   */
  showProgressBar() {
    if (this.controls) {
      this.controls.showProgressBar();
    }
  }
  /**
   * Hides the progress bar
   */
  hideProgressBar() {
    if (this.controls) {
      this.controls.hideProgressBar();
    }
  }
  /**
   * Shows the mute button
   */
  showMuteButton() {
    if (this.controls) {
      this.controls.showMuteButton();
    }
  }
  /**
   * Hides the mute button
   */
  hideMuteButton() {
    if (this.controls) {
      this.controls.hideMuteButton();
    }
  }
  /**
   * Sets the completion policy for the video
   * @param policy - The completion policy to use
   * @param callback - Optional callback to execute when video ends
   */
  setCompletionPolicy(policy, callback) {
    this.completionPolicy = policy;
    this.videoEndedCallback = callback || null;
    this.updateVideoEndedHandler();
  }
  /**
   * Updates the video ended handler based on completion policy
   */
  updateVideoEndedHandler() {
  }
  /**
   * Marks that the user has interacted with the video player
   * This is important for mobile autoplay policies
   */
  markUserInteraction() {
    if (!this.hasUserInteracted) {
      this.hasUserInteracted = true;
      if (this.autoplayFallbackTimeout !== null) {
        window.clearTimeout(this.autoplayFallbackTimeout);
        this.autoplayFallbackTimeout = null;
      }
    }
  }
  /**
   * Checks if user has interacted with the player
   * @returns boolean indicating if user has interacted
   */
  hasUserInteractedWith() {
    return this.hasUserInteracted;
  }
  /**
   * Resets user interaction state (called when starting a new playlist)
   */
  resetUserInteraction() {
    this.hasUserInteracted = false;
    if (this.autoplayFallbackTimeout !== null) {
      window.clearTimeout(this.autoplayFallbackTimeout);
      this.autoplayFallbackTimeout = null;
    }
  }
  /**
   * Handles autoplay fallback click specifically for mobile Chrome
   * @param videoElement - The video element to configure
   */
  handleAutoplayFallbackClick(videoElement) {
    this.markUserInteraction();
    videoElement.muted = false;
    videoElement.loop = false;
    videoElement.currentTime = 0;
  }
  /**
   * Load transcript data for the current video
   * @param transcript - Transcript data to load
   * @param initiallyVisible - Whether transcript should be initially visible
   */
  loadTranscript(transcript, initiallyVisible = true) {
    this.transcriptManager.loadTranscript(transcript, initiallyVisible);
  }
  /**
   * Creates a soundbar visualization element with 11 frequency bars (mirrored with single center)
   * @returns HTMLElement containing the soundbar visualization
   */
  createSoundbar() {
    const soundbar = document.createElement("div");
    soundbar.className = "sf-audio-soundbar";
    for (let i = 0; i < 11; i++) {
      const bar = document.createElement("div");
      bar.className = "sf-audio-soundbar__bar";
      bar.dataset.barIndex = i.toString();
      soundbar.appendChild(bar);
    }
    return soundbar;
  }
  /**
   * Shows audio fallback overlay with optional poster image or avatar thumbnail
   * Includes real-time audio visualization soundbar
   * @param posterUrl - Optional URL for poster image (e.g., GIF)
   * @param avatarThumbnailUrl - Optional URL for avatar thumbnail to display instead of icon/text
   */
  showAudioFallbackOverlay(posterUrl, avatarThumbnailUrl) {
    if (!this.container) {
      console.warn("VideoManager: Cannot show audio fallback overlay - container not available");
      return;
    }
    if (posterUrl) {
      this.container.style.setProperty("--sf-audio-poster-url", `url('${posterUrl}')`);
      this.container.classList.add("sf-video-container--audio-fallback");
    }
    if (!this.audioFallbackOverlay) {
      this.audioFallbackOverlay = document.createElement("div");
      if (avatarThumbnailUrl) {
        this.audioFallbackOverlay.className = "sf-audio-fallback-overlay sf-audio-fallback-overlay--avatar";
        const avatar = document.createElement("img");
        avatar.className = "sf-audio-fallback-overlay__avatar";
        avatar.src = avatarThumbnailUrl;
        avatar.alt = "Speaker avatar";
        const overlay = document.createElement("div");
        overlay.className = "sf-audio-fallback-overlay__dim";
        this.audioFallbackOverlay.appendChild(avatar);
        this.audioFallbackOverlay.appendChild(overlay);
      } else {
        this.audioFallbackOverlay.className = "sf-audio-fallback-overlay sf-audio-fallback-overlay--soundbar-only";
      }
      this.soundbarElement = this.createSoundbar();
      this.audioFallbackOverlay.appendChild(this.soundbarElement);
      this.container.appendChild(this.audioFallbackOverlay);
    }
  }
  /**
   * Starts audio visualization for the active video element
   * Should be called AFTER video is loaded and ready to play
   */
  startAudioVisualization() {
    if (!this.soundbarElement) {
      return;
    }
    const activeVideo = this.getActiveVideo();
    if (!activeVideo) {
      return;
    }
    try {
      this.audioVisualizationManager.initialize(activeVideo);
      this.audioVisualizationManager.startVisualization((barHeights, _amplitude) => {
        if (!this.soundbarElement) return;
        const bars = this.soundbarElement.querySelectorAll(".sf-audio-soundbar__bar");
        bars.forEach((bar, index) => {
          const height = barHeights[index] || 0;
          const htmlBar = bar;
          const heightPercent = Math.max(10, height * 100);
          htmlBar.style.height = `${heightPercent}%`;
          const center = 5;
          const distanceFromCenter = Math.abs(index - center);
          const maxDistance = 5;
          const hue = 180 + distanceFromCenter / maxDistance * 120;
          const opacity = 1;
          htmlBar.style.backgroundColor = `hsla(${hue}, 70%, 60%, ${opacity})`;
        });
      });
    } catch (error2) {
      console.error("VideoManager: Failed to initialize audio visualization", error2);
    }
  }
  /**
   * Initializes/switches Web Audio source for regular video nodes
   * Does NOT start visualization - only ensures audio routing
   * Must be called after video loads for proper audio routing once Web Audio is initialized
   */
  initializeAudioForVideo() {
    const activeVideo = this.getActiveVideo();
    if (!activeVideo) {
      return;
    }
    try {
      this.audioVisualizationManager.initialize(activeVideo);
    } catch (error2) {
      console.error("VideoManager: Failed to initialize audio for video", error2);
    }
  }
  /**
   * Hides audio fallback overlay and removes poster image
   * Note: Does not cleanup Web Audio connections - they must persist for audio playback
   */
  hideAudioFallbackOverlay() {
    if (!this.container) {
      return;
    }
    this.audioVisualizationManager.stopVisualization();
    this.container.style.removeProperty("--sf-audio-poster-url");
    this.container.classList.remove("sf-video-container--audio-fallback");
    if (this.audioFallbackOverlay && this.audioFallbackOverlay.parentNode) {
      this.audioFallbackOverlay.parentNode.removeChild(this.audioFallbackOverlay);
      this.audioFallbackOverlay = null;
    }
    this.soundbarElement = null;
  }
}
const DEFAULT_CONFIG = {
  tolerance: 0.3,
  // 30% size difference allowed (70% match required)
  minSize: 1
  // Minimum 1x1px (reject only zero-size elements)
};
function normalizeText(text) {
  return text.trim().replace(/\s+/g, " ");
}
function matchesExpectedElement(element, expected) {
  if (element.tagName.toUpperCase() !== expected.tagName.toUpperCase()) {
    return false;
  }
  const actualText = normalizeText(element.textContent || "");
  const expectedText = normalizeText(expected.textContent);
  return actualText === expectedText;
}
function findElementByTagAndText(elements, expected, config) {
  const matches = [];
  for (const element of elements) {
    const rejection = getBasicRejectionReason(element, config);
    if (rejection) {
      continue;
    }
    if (matchesExpectedElement(element, expected)) {
      matches.push(element);
    }
  }
  if (matches.length === 0) {
    log(`ElementValidator: No tag+text match for tagName='${expected.tagName}', text='${expected.textContent.substring(0, 50)}${expected.textContent.length > 50 ? "..." : ""}'`);
    return null;
  }
  if (matches.length === 1) {
    return matches[0];
  }
  return matches[0];
}
function findAllElementsByTagAndText(elements, expected, config) {
  const matches = [];
  for (const element of elements) {
    const rejection = getBasicRejectionReason(element, config);
    if (rejection) {
      continue;
    }
    if (matchesExpectedElement(element, expected)) {
      matches.push(element);
    }
  }
  if (matches.length > 0) {
    log(`ElementValidator: Found ${matches.length} element(s) matching tag+text`);
  } else {
    log(`ElementValidator: No tag+text match for tagName='${expected.tagName}'`);
  }
  return matches;
}
function getBasicRejectionReason(element, config) {
  const rect = element.getBoundingClientRect();
  if (rect.width === 0 || rect.height === 0) {
    return "zero size";
  }
  if (rect.width < config.minSize || rect.height < config.minSize) {
    return `too small (${rect.width.toFixed(0)}x${rect.height.toFixed(0)})`;
  }
  return null;
}
function calculateSizeScore(element, expected) {
  const rect = element.getBoundingClientRect();
  const widthRatio = Math.min(rect.width, expected.width) / Math.max(rect.width, expected.width);
  const heightRatio = Math.min(rect.height, expected.height) / Math.max(rect.height, expected.height);
  return (widthRatio + heightRatio) / 2 * 100;
}
function findValidElement(selector, expectedElement, expectedSize, config = DEFAULT_CONFIG) {
  const elements = document.querySelectorAll(selector);
  if (elements.length === 0) {
    return null;
  }
  if (expectedElement) {
    const tagTextMatch = findElementByTagAndText(elements, expectedElement, config);
    if (tagTextMatch) {
      return tagTextMatch;
    }
    if (!expectedSize) {
      return null;
    }
  }
  if (expectedSize) {
    return findValidElementBySize(selector, elements, expectedSize, config);
  }
  for (const element of elements) {
    const rejection = getBasicRejectionReason(element, config);
    if (!rejection) {
      return element;
    }
  }
  return elements[0];
}
function findValidElementBySize(selector, elements, expectedSize, config) {
  const threshold = (1 - config.tolerance) * 100;
  const scored = [];
  for (const element of elements) {
    const rejection = getBasicRejectionReason(element, config);
    if (rejection) {
      scored.push({ element, score: -1, rejection });
      continue;
    }
    const score = calculateSizeScore(element, expectedSize);
    if (score < threshold) {
      const rect = element.getBoundingClientRect();
      scored.push({
        element,
        score,
        rejection: `size mismatch: expected ~${expectedSize.width.toFixed(0)}x${expectedSize.height.toFixed(0)}, got ${rect.width.toFixed(0)}x${rect.height.toFixed(0)}`
      });
    } else {
      scored.push({ element, score });
    }
  }
  log(`ElementValidator: ${scored.length} element(s) for '${selector}' (expected ~${expectedSize.width.toFixed(0)}x${expectedSize.height.toFixed(0)})`);
  for (let i = 0; i < scored.length; i++) {
    const s = scored[i];
    const rect = s.element.getBoundingClientRect();
    const status = s.rejection ? `REJECTED (${s.rejection})` : `OK (${s.score.toFixed(0)}%)`;
    log(`  [${i}] ${rect.width.toFixed(0)}x${rect.height.toFixed(0)} - ${status}`);
  }
  const valid = scored.filter((s) => !s.rejection);
  if (valid.length === 0) {
    return null;
  }
  valid.sort((a, b) => b.score - a.score);
  log(`ElementValidator: Selected element with ${valid[0].score.toFixed(0)}% match`);
  return valid[0].element;
}
function findAllValidElements(selector, expectedElement, expectedSize, config = DEFAULT_CONFIG) {
  const elements = document.querySelectorAll(selector);
  if (elements.length === 0) {
    return [];
  }
  if (expectedElement) {
    const tagTextMatches = findAllElementsByTagAndText(elements, expectedElement, config);
    if (tagTextMatches.length > 0) {
      return tagTextMatches;
    }
    if (!expectedSize) {
      return [];
    }
  }
  if (expectedSize) {
    const threshold = (1 - config.tolerance) * 100;
    const valid2 = [];
    for (const element of elements) {
      const rejection = getBasicRejectionReason(element, config);
      if (rejection) {
        continue;
      }
      const score = calculateSizeScore(element, expectedSize);
      if (score >= threshold) {
        valid2.push(element);
      }
    }
    if (valid2.length > 0) {
      log(`ElementValidator: Found ${valid2.length} valid element(s) for '${selector}'`);
    }
    return valid2;
  }
  const valid = [];
  for (const element of elements) {
    const rejection = getBasicRejectionReason(element, config);
    if (!rejection) {
      valid.push(element);
    }
  }
  return valid.length > 0 ? valid : Array.from(elements);
}
function isElementValid(element, expectedElement, expectedSize, config = DEFAULT_CONFIG) {
  const rejection = getBasicRejectionReason(element, config);
  if (rejection) {
    return false;
  }
  if (expectedElement) {
    if (matchesExpectedElement(element, expectedElement)) {
      return true;
    }
  }
  if (expectedSize) {
    const threshold = (1 - config.tolerance) * 100;
    const score = calculateSizeScore(element, expectedSize);
    return score >= threshold;
  }
  return true;
}
const RRWEB_SNAPSHOT_URL = "https://storage.saltfish.ai/libs/rrweb-snapshot-2.0.0-alpha.18.js";
async function captureDOMSnapshot() {
  try {
    const rrwebSnapshot = await import(
      /* webpackIgnore: true */
      RRWEB_SNAPSHOT_URL
    );
    const domSnapshot = rrwebSnapshot.snapshot(document);
    return domSnapshot;
  } catch (error2) {
    console.warn("Failed to capture DOM snapshot:", error2);
    return null;
  }
}
async function captureDOMSnapshotAsString() {
  const snapshot = await captureDOMSnapshot();
  if (!snapshot) {
    return null;
  }
  try {
    return JSON.stringify(snapshot);
  } catch (error2) {
    console.warn("Failed to serialize DOM snapshot:", error2);
    return null;
  }
}
const API_URL = "https://player.saltfish.ai/element-errors";
function determineFailureReason(selector, expectedElement, expectedSize) {
  const elements = document.querySelectorAll(selector);
  if (elements.length === 0) {
    return "no_elements";
  }
  if (expectedElement) {
    const hasMatchingTag = Array.from(elements).some(
      (el) => el.tagName.toUpperCase() === expectedElement.tagName.toUpperCase()
    );
    if (!hasMatchingTag) {
      return "tag_mismatch";
    }
    return "text_mismatch";
  }
  if (expectedSize) {
    return "size_mismatch";
  }
  return "no_elements";
}
function reportElementError(playlistId, stepId, selector, expectedElement, expectedSize) {
  const failureReason = determineFailureReason(selector, expectedElement, expectedSize);
  (async () => {
    try {
      const payload = {
        playlistId,
        stepId,
        failureReason,
        selector
      };
      const domSnapshot = await captureDOMSnapshotAsString();
      if (domSnapshot) {
        payload.domSnapshot = domSnapshot;
      }
      await fetch(API_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(payload)
      });
    } catch (error2) {
      console.warn("Failed to report element error:", error2);
    }
  })();
}
function parseColorToRgb(color) {
  const el = document.createElement("div");
  el.style.color = color;
  document.body.appendChild(el);
  const computed = window.getComputedStyle(el).color;
  document.body.removeChild(el);
  const match = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  return match ? { r: +match[1], g: +match[2], b: +match[3] } : null;
}
function colorToRgba(color, opacity) {
  const rgb = parseColorToRgb(color);
  return rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})` : color;
}
function getContrastingTextColor(bgColor) {
  const rgb = parseColorToRgb(bgColor);
  if (!rgb) return "#fff";
  const luminance = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1e3;
  return luminance > 150 ? "#333" : "#fff";
}
class CursorManager {
  constructor() {
    __publicField(this, "cursor", null);
    __publicField(this, "animationFrameId", null);
    __publicField(this, "animationStartTime", null);
    __publicField(this, "currentAnimation", null);
    __publicField(this, "flashlightOverlay", null);
    __publicField(this, "startX", null);
    __publicField(this, "startY", null);
    __publicField(this, "targetX", null);
    __publicField(this, "targetY", null);
    // Track if the cursor should be shown based on step configuration
    __publicField(this, "shouldShowCursor", false);
    // Store the last cursor position to avoid moving in from the side every time
    __publicField(this, "lastCursorX", 50);
    __publicField(this, "lastCursorY", 50);
    // Track if this is the first animation (to determine if we should move in from the side)
    __publicField(this, "isFirstAnimation", true);
    // Track the current target element to maintain position when scrolling
    __publicField(this, "currentTargetElement", null);
    // Store reference to bound event handler for proper cleanup
    __publicField(this, "boundScrollHandler", null);
    // RequestAnimationFrame ID for smooth scroll updates
    __publicField(this, "scrollRafId", null);
    // Flag to track if scroll update is pending
    __publicField(this, "scrollUpdatePending", false);
    // Add properties to track scrollable parent containers
    __publicField(this, "scrollableParents", []);
    __publicField(this, "parentScrollHandlers", /* @__PURE__ */ new Map());
    // Selection mode related properties
    __publicField(this, "selectionElement", null);
    __publicField(this, "isSelectionMode", false);
    __publicField(this, "selectionPadding", 4);
    // Default padding in pixels
    // Label element for cursor name/text
    __publicField(this, "labelElement", null);
    __publicField(this, "labelText", null);
    // Selection drag properties
    __publicField(this, "dragStartX", null);
    __publicField(this, "dragStartY", null);
    __publicField(this, "dragEndX", null);
    __publicField(this, "dragEndY", null);
    __publicField(this, "dragPhase", "move-to-start");
    __publicField(this, "dragAnimationStartTime", null);
    // Duration-based animation parameters for consistent, human-like speed
    __publicField(this, "TARGET_SPEED", 350);
    // Target speed in pixels per second (slower, more natural)
    __publicField(this, "MIN_ANIMATION_DURATION", 600);
    // Minimum animation duration in ms (more deliberate)
    __publicField(this, "MAX_ANIMATION_DURATION", 2e3);
    // Maximum animation duration in ms (comfortable pace)
    __publicField(this, "animationDuration", 800);
    // Calculated duration for current animation
    __publicField(this, "totalDistance", 0);
    // Total distance to travel
    __publicField(this, "controlPointX", null);
    // Control point for curved path
    __publicField(this, "controlPointY", null);
    // Control point for curved path
    // Add a private property for the mutation observer
    __publicField(this, "targetMutationObserver", null);
    // Cursor offset constants for visual alignment
    __publicField(this, "POINTER_HORIZONTAL_OFFSET", 16);
    __publicField(this, "POINTER_VERTICAL_OFFSET", 16);
    __publicField(this, "SELECTION_HORIZONTAL_OFFSET", 8);
    __publicField(this, "SELECTION_VERTICAL_OFFSET", 8);
    // Animation utilities for shared animation logic
    __publicField(this, "animationUtils", {
      // Validates that animation can proceed
      validateAnimationState: () => {
        if (this.isAutoplayBlocked()) {
          this.stopAnimation();
          return false;
        }
        return true;
      },
      // Calculates eased progress (0 to 1) based on time elapsed
      calculateEasedProgress: (elapsed, duration) => {
        const rawProgress = duration > 0 ? elapsed / duration : 1;
        const progress = Math.min(rawProgress, 1);
        return 0.5 - 0.5 * Math.cos(progress * Math.PI);
      },
      // Calculates point on quadratic Bezier curve
      calculateBezierPoint: (t, start, control, target) => {
        const oneMinusT = 1 - t;
        const x = Math.pow(oneMinusT, 2) * start.x + 2 * oneMinusT * t * control.x + Math.pow(t, 2) * target.x;
        const y = Math.pow(oneMinusT, 2) * start.y + 2 * oneMinusT * t * control.y + Math.pow(t, 2) * target.y;
        return { x, y };
      },
      // Checks if position changed significantly and needs control point recalc
      hasSignificantPositionChange: (previous, current2, threshold = 10) => {
        const deltaX = Math.abs(current2.x - previous.x);
        const deltaY = Math.abs(current2.y - previous.y);
        return deltaX > threshold || deltaY > threshold;
      }
    });
  }
  /**
   * Extracts padding value from selection styles
   * @param styles - Optional selection styles
   * @returns Padding value in pixels
   */
  getPaddingFromStyles(styles) {
    return typeof (styles == null ? void 0 : styles.padding) === "number" ? styles.padding : (styles == null ? void 0 : styles.padding) ? parseInt(styles.padding, 10) : this.selectionPadding;
  }
  /**
   * Calculates distance between two points
   * @param x1 - Start X coordinate
   * @param y1 - Start Y coordinate
   * @param x2 - End X coordinate
   * @param y2 - End Y coordinate
   * @returns Distance in pixels
   */
  calculateDistance(x1, y1, x2, y2) {
    return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
  }
  /**
   * Calculates animation duration based on distance for consistent, human-like speed
   * Uses speed-clamped approach: maintains ~350px/s speed within 600-2000ms bounds
   * @param distance - Distance to travel in pixels
   * @returns Duration in milliseconds
   */
  calculateAnimationDuration(distance) {
    const baseDuration = distance / (this.TARGET_SPEED / 1e3);
    return Math.min(Math.max(baseDuration, this.MIN_ANIMATION_DURATION), this.MAX_ANIMATION_DURATION);
  }
  /**
   * Gets the current cursor position from lastCursorX/Y
   * @returns Current cursor coordinates
   */
  getCurrentCursorPosition() {
    return {
      x: this.lastCursorX,
      y: this.lastCursorY
    };
  }
  /**
   * Checks if cursor can be shown (not blocked by autoplay and should show)
   * @returns Whether cursor can be shown
   */
  canShowCursor() {
    return this.shouldShowCursor && !this.isAutoplayBlocked();
  }
  /**
   * Waits for a scroll operation to complete with timeout
   * @param scrollable - Element or window that is scrolling
   * @param onComplete - Callback when scroll completes
   * @param maxTimeout - Maximum time to wait in milliseconds
   */
  waitForScrollComplete(scrollable, onComplete, maxTimeout = 1e3) {
    let scrollEndTimer = null;
    const onScroll = () => {
      if (scrollEndTimer !== null) {
        clearTimeout(scrollEndTimer);
      }
      scrollEndTimer = window.setTimeout(() => {
        scrollable.removeEventListener("scroll", onScroll);
        onComplete();
      }, 100);
    };
    scrollable.addEventListener("scroll", onScroll);
    setTimeout(() => {
      scrollable.removeEventListener("scroll", onScroll);
      if (scrollEndTimer !== null) {
        clearTimeout(scrollEndTimer);
      }
      onComplete();
    }, maxTimeout);
  }
  /**
   * Sets up a MutationObserver to wait for an element to appear in the DOM
   * @param selector - CSS selector to wait for
   * @param callback - Callback to execute when element is found
   * @param expectedElement - Optional expected tag+text for validation
   * @param expectedSize - Optional expected size for validation
   */
  waitForElement(selector, callback, expectedElement, expectedSize) {
    if (this.targetMutationObserver) {
      this.targetMutationObserver.disconnect();
      this.targetMutationObserver = null;
    }
    const MAX_RETRIES = 10;
    let retryCount = 0;
    let periodicCheckId = null;
    let errorReported = false;
    const reportFailure = () => {
      var _a;
      if (errorReported) return;
      errorReported = true;
      const state = saltfishStore.getState();
      const playlistId = (_a = state.manifest) == null ? void 0 : _a.id;
      const stepId = state.currentStepId;
      if (playlistId && stepId) {
        reportElementError(playlistId, stepId, selector, expectedElement, expectedSize);
      }
    };
    const cleanupWatchers = () => {
      if (this.targetMutationObserver) {
        this.targetMutationObserver.disconnect();
        this.targetMutationObserver = null;
      }
      if (periodicCheckId !== null) {
        clearInterval(periodicCheckId);
        periodicCheckId = null;
      }
    };
    const tryFindElement = async () => {
      retryCount++;
      if (retryCount > MAX_RETRIES) {
        console.warn(`CursorManager: Stopped waiting for element '${selector}' after ${MAX_RETRIES} attempts`);
        reportFailure();
        cleanupWatchers();
        return null;
      }
      return this.findElementAndScrollIntoView(selector, expectedElement, expectedSize);
    };
    this.targetMutationObserver = new MutationObserver(async () => {
      if (this.isAutoplayBlocked()) {
        cleanupWatchers();
        return;
      }
      const el = await tryFindElement();
      if (el) {
        cleanupWatchers();
        callback(el);
      }
    });
    this.targetMutationObserver.observe(document.body, { childList: true, subtree: true });
    periodicCheckId = setInterval(() => {
      if (!this.targetMutationObserver || this.isAutoplayBlocked()) {
        cleanupWatchers();
        return;
      }
      retryCount++;
      if (retryCount > MAX_RETRIES) {
        console.warn(`CursorManager: Stopped waiting for element '${selector}' after ${MAX_RETRIES} attempts`);
        reportFailure();
        cleanupWatchers();
        return;
      }
      const found = this.findElement(selector, expectedElement, expectedSize);
      if (found) {
        cleanupWatchers();
        callback(found);
      }
    }, 1e3);
  }
  /**
   * Finds all scrollable parent containers of an element
   * @param element - The element to find scrollable parents for
   * @returns Array of scrollable parent elements
   */
  findScrollableParents(element) {
    const scrollableParents = [];
    let parent = element.parentElement;
    while (parent && parent !== document.body) {
      const computedStyle = window.getComputedStyle(parent);
      const overflow = computedStyle.overflow;
      const overflowX = computedStyle.overflowX;
      const overflowY = computedStyle.overflowY;
      if (overflow === "scroll" || overflow === "auto" || overflowX === "scroll" || overflowX === "auto" || overflowY === "scroll" || overflowY === "auto") {
        if (parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) {
          scrollableParents.push(parent);
        }
      }
      parent = parent.parentElement;
    }
    return scrollableParents;
  }
  /**
   * Adds scroll event listeners to scrollable parent containers
   * @param element - The target element whose parents should be monitored
   */
  addScrollListenersToParents(element) {
    this.removeScrollListenersFromParents();
    this.scrollableParents = this.findScrollableParents(element);
    this.scrollableParents.forEach((parent) => {
      const handler = this.handleScroll.bind(this);
      this.parentScrollHandlers.set(parent, handler);
      parent.addEventListener("scroll", handler, { passive: true });
    });
  }
  /**
   * Removes scroll event listeners from all tracked parent containers
   */
  removeScrollListenersFromParents() {
    this.parentScrollHandlers.forEach((handler, parent) => {
      parent.removeEventListener("scroll", handler);
    });
    this.parentScrollHandlers.clear();
    this.scrollableParents = [];
  }
  /**
   * Helper function to find an element in the document
   * Uses tag+text validation first (if provided), then size validation
   * Falls back to viewport-based selection when no validation criteria or when validation passes
   * @param selector - CSS selector
   * @param expectedElement - Optional expected tag+text for validation
   * @param expectedSize - Optional expected size for validation
   * @returns - The found element or null
   */
  findElement(selector, expectedElement, expectedSize) {
    if (expectedElement || expectedSize) {
      const validElement = findValidElement(selector, expectedElement, expectedSize);
      if (validElement) {
        return validElement;
      }
      return null;
    }
    const elements = document.querySelectorAll(selector);
    if (elements.length === 0) {
      return null;
    }
    if (elements.length === 1) {
      return elements[0];
    }
    for (const element of elements) {
      if (this.isElementInViewport(element)) {
        element.getBoundingClientRect();
        return element;
      }
    }
    const viewportCenterX = window.innerWidth / 2;
    const viewportCenterY = window.innerHeight / 2;
    let closestElement = elements[0];
    let closestDistance = Infinity;
    for (const element of elements) {
      const rect = element.getBoundingClientRect();
      const elementCenterX = rect.left + rect.width / 2;
      const elementCenterY = rect.top + rect.height / 2;
      const distance = Math.sqrt(
        Math.pow(elementCenterX - viewportCenterX, 2) + Math.pow(elementCenterY - viewportCenterY, 2)
      );
      if (distance < closestDistance) {
        closestDistance = distance;
        closestElement = element;
      }
    }
    closestElement.getBoundingClientRect();
    return closestElement;
  }
  /**
   * Checks if an element is completely visible in the viewport and within all scrollable parent containers
   * @param element - The element to check
   * @returns - Whether the entire element is visible in viewport and all scrollable parents
   */
  isElementInViewport(element) {
    const rect = element.getBoundingClientRect();
    const windowHeight = window.innerHeight || document.documentElement.clientHeight;
    const windowWidth = window.innerWidth || document.documentElement.clientWidth;
    const isCompletelyInWindow = rect.top >= 0 && // Top edge is visible
    rect.bottom <= windowHeight && // Bottom edge is visible
    rect.left >= 0 && // Left edge is visible
    rect.right <= windowWidth;
    if (!isCompletelyInWindow) {
      return false;
    }
    const scrollableParents = this.findScrollableParents(element);
    for (const parent of scrollableParents) {
      const parentRect = parent.getBoundingClientRect();
      const isCompletelyInParent = rect.top >= parentRect.top && // Top edge is within parent
      rect.bottom <= parentRect.bottom && // Bottom edge is within parent
      rect.left >= parentRect.left && // Left edge is within parent
      rect.right <= parentRect.right;
      if (!isCompletelyInParent) {
        return false;
      }
    }
    return true;
  }
  /**
   * Scrolls an element into view smoothly, handling both window and parent container scrolling
   * @param element - The element to scroll into view
   * @returns - Promise that resolves when scrolling is complete
   */
  async scrollElementIntoView(element) {
    return new Promise((resolve) => {
      const scrollableParents = this.findScrollableParents(element);
      const elementRect = element.getBoundingClientRect();
      const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
      const scrollBlock = elementRect.height > viewportHeight ? "start" : "center";
      if (scrollableParents.length === 0) {
        element.scrollIntoView({
          behavior: "smooth",
          block: scrollBlock,
          inline: "center"
        });
        this.waitForScrollComplete(window, resolve);
      } else {
        this.scrollParentContainersToShowElement(element, scrollableParents).then(() => {
          if (!this.isElementInViewport(element)) {
            element.scrollIntoView({
              behavior: "smooth",
              block: scrollBlock,
              inline: "center"
            });
          }
          setTimeout(() => {
            resolve();
          }, 200);
        });
      }
    });
  }
  /**
   * Scrolls parent containers to make the element visible
   * @param element - The target element
   * @param scrollableParents - Array of scrollable parent containers
   * @returns Promise that resolves when scrolling is complete
   */
  async scrollParentContainersToShowElement(element, scrollableParents) {
    return new Promise((resolve) => {
      let completedScrolls = 0;
      const totalScrolls = scrollableParents.length;
      if (totalScrolls === 0) {
        resolve();
        return;
      }
      const onScrollComplete = () => {
        completedScrolls++;
        if (completedScrolls >= totalScrolls) {
          resolve();
        }
      };
      scrollableParents.forEach((parent) => {
        const elementRect = element.getBoundingClientRect();
        const parentRect = parent.getBoundingClientRect();
        const scrollTop = parent.scrollTop;
        const scrollLeft = parent.scrollLeft;
        let targetScrollTop;
        if (elementRect.height > parentRect.height) {
          targetScrollTop = scrollTop + (elementRect.top - parentRect.top);
        } else {
          targetScrollTop = scrollTop + (elementRect.top - parentRect.top) - parentRect.height / 2 + elementRect.height / 2;
        }
        const targetScrollLeft = scrollLeft + (elementRect.left - parentRect.left) - parentRect.width / 2 + elementRect.width / 2;
        parent.scrollTo({
          top: Math.max(0, targetScrollTop),
          left: Math.max(0, targetScrollLeft),
          behavior: "smooth"
        });
        this.waitForScrollComplete(parent, onScrollComplete, 800);
      });
    });
  }
  /**
   * Finds an element and scrolls it into view if necessary
   * @param selector - CSS selector
   * @param expectedElement - Optional expected tag+text for validation
   * @param expectedSize - Optional expected size for validation
   * @returns - Promise that resolves with the element or null
   */
  async findElementAndScrollIntoView(selector, expectedElement, expectedSize) {
    const element = this.findElement(selector, expectedElement, expectedSize);
    if (!element) {
      return null;
    }
    if (!this.isElementInViewport(element)) {
      await this.scrollElementIntoView(element);
    }
    return element;
  }
  /**
   * Cleans up any existing cursor elements from the DOM
   * This prevents duplicate elements when switching playlists
   */
  cleanupExistingElements() {
    const existingCursors = document.querySelectorAll(".sf-cursor");
    existingCursors.forEach((el) => el.remove());
    const existingLabels = document.querySelectorAll(".sf-cursor-label");
    existingLabels.forEach((el) => el.remove());
    const existingSelections = document.querySelectorAll(".sf-selection");
    existingSelections.forEach((el) => el.remove());
    const existingFlashlights = document.querySelectorAll(".sf-flashlight-overlay");
    existingFlashlights.forEach((el) => el.remove());
    this.cursor = null;
    this.labelElement = null;
    this.selectionElement = null;
    this.flashlightOverlay = null;
  }
  /**
   * Creates the virtual cursor element
   */
  create() {
    this.cleanupExistingElements();
    this.injectCursorStyles();
    this.cursor = document.createElement("div");
    this.cursor.className = "sf-cursor";
    this.cursor.innerHTML = ICON_CURSOR;
    this.labelElement = document.createElement("div");
    this.labelElement.className = "sf-cursor-label";
    this.selectionElement = document.createElement("div");
    this.selectionElement.className = "sf-selection";
    document.body.appendChild(this.cursor);
    document.body.appendChild(this.labelElement);
    document.body.appendChild(this.selectionElement);
    this.flashlightOverlay = document.createElement("div");
    this.flashlightOverlay.className = "sf-flashlight-overlay";
    document.body.appendChild(this.flashlightOverlay);
    this.boundScrollHandler = this.handleScroll.bind(this);
    window.addEventListener("scroll", this.boundScrollHandler, { passive: true });
    this.lastCursorX = window.innerWidth - 50;
    this.lastCursorY = window.innerHeight - 50;
  }
  /**
   * Injects cursor styles into the main document for CSP compliance
   * Uses adoptedStyleSheets API when available, falls back to <style> element
   */
  injectCursorStyles() {
    const cursorStyles = `
      .sf-cursor {
        position: fixed;
        top: 0;
        left: 0;
        width: 40px;
        height: 40px;
        z-index: 9999999;
        pointer-events: none;
        display: none;
        will-change: transform;
        transform: var(--sf-cursor-transform, translate(0, 0));
        opacity: var(--sf-cursor-opacity, 1);
        /* Defensive styles to prevent host page CSS interference */
        margin: 0;
        padding: 0;
        border: 0;
        box-sizing: content-box;
        line-height: 0;
        font-size: 0;
        vertical-align: baseline;
        overflow: visible;
      }

      .sf-cursor--visible {
        display: block;
      }

      .sf-cursor svg {
        display: block;
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
        border: 0;
      }

      .sf-selection {
        position: fixed;
        pointer-events: none;
        display: none;
        z-index: 9999998;
        top: 0;
        left: 0;
        border: 2px solid var(--sf-selection-color, #ff7614);
        background: var(--sf-selection-bg-color, rgba(255, 118, 20, 0));
        border-radius: 4px;
        width: var(--sf-selection-width, 0);
        height: var(--sf-selection-height, 0);
        transform: var(--sf-selection-transform, translate(0, 0));
        will-change: transform;
        box-sizing: border-box;
      }

      .sf-selection--visible {
        display: block;
      }

      .sf-flashlight-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        pointer-events: none;
        z-index: 9999997;
        display: none;
        background: var(--sf-flashlight-bg, radial-gradient(circle 150px at 50% 50%, transparent 0%, rgba(0, 0, 0, 0.4) 100%));
        clip-path: var(--sf-flashlight-clip, none);
      }

      .sf-flashlight-overlay--visible {
        display: block;
      }

      .sf-cursor-label {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 9999999;
        pointer-events: none;
        display: none;
        will-change: transform;
        transform: var(--sf-cursor-label-transform, translate(0, 0));
        opacity: var(--sf-cursor-opacity, 1);
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
        font-size: 13px;
        font-weight: 500;
        color: var(--sf-cursor-label-text, #fff);
        background: var(--sf-cursor-label-bg, #ff7614);
        padding: 4px 10px;
        border-radius: 12px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
        white-space: nowrap;
        margin: 0;
        border: 0;
        box-sizing: border-box;
        line-height: 1.4;
      }

      .sf-cursor-label--visible {
        display: block;
      }
    `;
    try {
      const sheet = new CSSStyleSheet();
      sheet.replaceSync(cursorStyles);
      document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
    } catch (error2) {
      const styleElement = document.createElement("style");
      styleElement.textContent = cursorStyles;
      document.head.appendChild(styleElement);
    }
  }
  /**
   * Handles scrolling to keep the cursor positioned on the target element
   * Uses requestAnimationFrame for smooth 60fps updates
   */
  handleScroll() {
    if (this.isAutoplayBlocked() || !this.shouldShowCursor || !this.currentTargetElement) {
      return;
    }
    if (this.animationFrameId !== null || this.scrollUpdatePending) {
      return;
    }
    this.scrollUpdatePending = true;
    this.scrollRafId = requestAnimationFrame(() => {
      this.updateCursorPositionOnScroll();
      this.scrollUpdatePending = false;
      this.scrollRafId = null;
    });
  }
  /**
   * Updates cursor and selection positions during scroll
   * Called via requestAnimationFrame for smooth updates
   */
  updateCursorPositionOnScroll() {
    var _a, _b;
    if (!this.currentTargetElement) {
      return;
    }
    const targetRect = this.currentTargetElement.getBoundingClientRect();
    let newX;
    let newY;
    if (this.isSelectionMode) {
      const padding = this.getPaddingFromStyles((_a = this.currentAnimation) == null ? void 0 : _a.selectionStyles);
      newX = targetRect.right + padding + this.SELECTION_HORIZONTAL_OFFSET;
      newY = targetRect.bottom + padding + this.SELECTION_VERTICAL_OFFSET;
    } else {
      newX = targetRect.left + targetRect.width / 2 + this.POINTER_HORIZONTAL_OFFSET;
      newY = targetRect.top + targetRect.height / 2 + this.POINTER_VERTICAL_OFFSET;
    }
    this.show(newX, newY);
    this.lastCursorX = newX;
    this.lastCursorY = newY;
    if (this.selectionElement && this.isSelectionMode) {
      const padding = this.getPaddingFromStyles((_b = this.currentAnimation) == null ? void 0 : _b.selectionStyles);
      const left = Math.floor(targetRect.left - padding);
      const top = Math.floor(targetRect.top - padding);
      const width = Math.ceil(targetRect.width + padding * 2);
      const height = Math.ceil(targetRect.height + padding * 2);
      this.selectionElement.style.setProperty("--sf-selection-transform", `translate(${left}px, ${top}px)`);
      this.selectionElement.style.setProperty("--sf-selection-width", `${width}px`);
      this.selectionElement.style.setProperty("--sf-selection-height", `${height}px`);
      this.dragStartX = left;
      this.dragStartY = top;
      this.dragEndX = left + width;
      this.dragEndY = top + height;
      this.updateFlashlightWithCutout(left, top, width, height);
    }
  }
  /**
   * Checks if autoplay is blocked and cursor should be disabled
   * @returns - Whether autoplay is blocked
   */
  isAutoplayBlocked() {
    const state = saltfishStore.getState();
    return state.currentState === "autoplayBlocked";
  }
  /**
   * Sets whether the cursor should be shown based on step configuration
   * @param shouldShow - Whether the cursor should be shown
   */
  setShouldShowCursor(shouldShow) {
    if (this.isAutoplayBlocked()) {
      this.shouldShowCursor = false;
      this.hideCursorElements();
      return;
    }
    this.shouldShowCursor = shouldShow;
    if (!shouldShow) {
      this.hideCursorElements();
    } else if (!this.isFirstAnimation) {
      this.show(this.lastCursorX, this.lastCursorY);
    }
  }
  /**
   * Shows the cursor and flashlight at a specific position
   * Note: Will only show if shouldShowCursor is true and autoplay is not blocked
   * @param x - X coordinate
   * @param y - Y coordinate
   */
  show(x, y) {
    if (!this.canShowCursor()) {
      return;
    }
    this.lastCursorX = x;
    this.lastCursorY = y;
    if (this.cursor) {
      this.cursor.classList.add("sf-cursor--visible");
      this.cursor.style.setProperty("--sf-cursor-transform", `translate(${x}px, ${y}px) translate(-50%, -50%)`);
    }
    if (this.labelElement && this.labelText) {
      this.labelElement.classList.add("sf-cursor-label--visible");
      this.labelElement.style.setProperty("--sf-cursor-label-transform", `translate(${x + 24}px, ${y + 8}px)`);
    }
    if (this.flashlightOverlay) {
      this.flashlightOverlay.classList.add("sf-flashlight-overlay--visible");
      this.flashlightOverlay.style.setProperty("--sf-flashlight-bg", `radial-gradient(circle 150px at ${x}px ${y}px, transparent 0%, rgba(0, 0, 0, 0.4) 100%)`);
    }
  }
  /**
   * Internal method to hide cursor elements without affecting shouldShowCursor state
   */
  hideCursorElements() {
    if (this.cursor) {
      this.cursor.classList.remove("sf-cursor--visible");
    }
    if (this.labelElement) {
      this.labelElement.classList.remove("sf-cursor-label--visible");
    }
    if (this.selectionElement) {
      this.selectionElement.classList.remove("sf-selection--visible");
    }
    if (this.flashlightOverlay) {
      this.flashlightOverlay.classList.remove("sf-flashlight-overlay--visible");
      this.resetFlashlightOverlay();
    }
  }
  /**
   * Resets the flashlight overlay to its original state without cutouts
   */
  resetFlashlightOverlay() {
    if (!this.flashlightOverlay) {
      return;
    }
    this.flashlightOverlay.style.setProperty("--sf-flashlight-clip", "none");
    const x = this.lastCursorX;
    const y = this.lastCursorY;
    this.flashlightOverlay.style.setProperty("--sf-flashlight-bg", `radial-gradient(circle 150px at ${x}px ${y}px, transparent 0%, rgba(0, 0, 0, 0.4) 100%)`);
  }
  /**
   * Animates the cursor along a path
   * @param animation - Animation configuration
   */
  async animate(animation) {
    if (this.isAutoplayBlocked()) {
      return;
    }
    this.stopAnimation();
    this.removeScrollListenersFromParents();
    this.currentTargetElement = null;
    this.isSelectionMode = false;
    this.resetFlashlightOverlay();
    if (this.selectionElement) {
      this.selectionElement.classList.remove("sf-selection--visible");
    }
    if (!(animation == null ? void 0 : animation.targetSelector)) {
      console.warn("CursorManager: No targetSelector provided in animation");
      return;
    }
    await new Promise((resolve) => setTimeout(resolve, TIMING.DOM_STABILIZATION_DELAY_MS));
    if (this.isAutoplayBlocked()) {
      return;
    }
    const targetElement = await this.findElementAndScrollIntoView(animation.targetSelector, animation.expectedElement, animation.expectedSize);
    if (!targetElement) {
      console.warn("CursorManager: Target element not found in animate:", animation.targetSelector);
      this.setShouldShowCursor(false);
      this.hideCursorElements();
      this.waitForElement(animation.targetSelector, () => this.animate(animation), animation.expectedElement, animation.expectedSize);
      return;
    }
    this.setShouldShowCursor(true);
    this.currentTargetElement = targetElement;
    this.addScrollListenersToParents(targetElement);
    const targetRect = targetElement.getBoundingClientRect();
    if (this.isFirstAnimation) {
      this.startX = window.innerWidth - 50;
      this.startY = window.innerHeight - 50;
      this.isFirstAnimation = false;
    } else {
      this.startX = this.lastCursorX;
      this.startY = this.lastCursorY;
    }
    const resolvedAnimation = {
      ...animation,
      mode: animation.mode || "selection"
    };
    this.isSelectionMode = resolvedAnimation.mode === "selection";
    if (this.isSelectionMode) {
      this.handleSelectionMode(resolvedAnimation, targetRect);
      return;
    } else {
      this.targetX = targetRect.left + targetRect.width / 2 + this.POINTER_HORIZONTAL_OFFSET;
      this.targetY = targetRect.top + targetRect.height / 2 + this.POINTER_VERTICAL_OFFSET;
      if (this.selectionElement) {
        this.selectionElement.classList.remove("sf-selection--visible");
      }
    }
    if (this.targetX !== null && this.targetY !== null && this.startX !== null && this.startY !== null) {
      this.totalDistance = this.calculateDistance(this.startX, this.startY, this.targetX, this.targetY);
      this.animationDuration = this.calculateAnimationDuration(this.totalDistance);
    } else {
      this.totalDistance = 100;
      this.animationDuration = this.MIN_ANIMATION_DURATION;
    }
    this.calculateControlPoint();
    this.currentAnimation = { ...resolvedAnimation };
    const safeStartX = this.startX !== null ? this.startX : 0;
    const safeStartY = this.startY !== null ? this.startY : 0;
    this.show(safeStartX, safeStartY);
    this.animationStartTime = performance.now();
    this.animationFrameId = requestAnimationFrame(this.unifiedAnimationFrame.bind(this));
  }
  /**
   * Handles selection mode setup and animation
   */
  handleSelectionMode(animation, targetRect) {
    const padding = this.getPaddingFromStyles(animation.selectionStyles);
    this.dragStartX = Math.floor(targetRect.left - padding);
    this.dragStartY = Math.floor(targetRect.top - padding);
    this.dragEndX = Math.ceil(targetRect.right + padding);
    this.dragEndY = Math.ceil(targetRect.bottom + padding);
    this.dragPhase = "move-to-start";
    this.targetX = this.dragStartX + this.SELECTION_HORIZONTAL_OFFSET;
    this.targetY = this.dragStartY + this.SELECTION_VERTICAL_OFFSET;
    if (this.targetX !== null && this.targetY !== null && this.startX !== null && this.startY !== null) {
      this.totalDistance = this.calculateDistance(this.startX, this.startY, this.targetX, this.targetY);
      this.animationDuration = this.calculateAnimationDuration(this.totalDistance);
    } else {
      this.totalDistance = 100;
      this.animationDuration = this.MIN_ANIMATION_DURATION;
    }
    if (this.selectionElement) {
      if (animation.selectionStyles) {
        if (animation.selectionStyles.borderColor) {
          this.selectionElement.style.setProperty("--sf-selection-border-color", animation.selectionStyles.borderColor);
        }
        if (animation.selectionStyles.borderWidth) {
          this.selectionElement.style.setProperty("--sf-selection-border-width", animation.selectionStyles.borderWidth);
        }
        if (animation.selectionStyles.borderRadius) {
          this.selectionElement.style.setProperty("--sf-selection-border-radius", animation.selectionStyles.borderRadius);
        }
      }
      this.selectionElement.classList.remove("sf-selection--visible");
    }
    this.calculateControlPoint();
    this.currentAnimation = { ...animation };
    const safeStartX = this.startX !== null ? this.startX : 0;
    const safeStartY = this.startY !== null ? this.startY : 0;
    this.show(safeStartX, safeStartY);
    this.animationStartTime = performance.now();
    this.animationFrameId = requestAnimationFrame(this.unifiedAnimationFrame.bind(this));
  }
  /**
   * Updates drag coordinates from current element position
   */
  updateDragCoordinatesFromElement() {
    var _a;
    if (!this.currentTargetElement) return;
    const rect = this.currentTargetElement.getBoundingClientRect();
    const padding = this.getPaddingFromStyles((_a = this.currentAnimation) == null ? void 0 : _a.selectionStyles);
    this.dragStartX = Math.floor(rect.left - padding);
    this.dragStartY = Math.floor(rect.top - padding);
    this.dragEndX = Math.ceil(rect.right + padding);
    this.dragEndY = Math.ceil(rect.bottom + padding);
  }
  /**
   * Updates selection rectangle dimensions during drag
   */
  updateSelectionRectangle(currentX, currentY) {
    if (!this.selectionElement) return;
    const width = Math.ceil(Math.abs(
      currentX - this.SELECTION_HORIZONTAL_OFFSET - this.dragStartX
    ));
    const height = Math.ceil(Math.abs(
      currentY - this.SELECTION_VERTICAL_OFFSET - this.dragStartY
    ));
    const left = Math.floor(Math.min(
      this.dragStartX,
      currentX - this.SELECTION_HORIZONTAL_OFFSET
    ));
    const top = Math.floor(Math.min(
      this.dragStartY,
      currentY - this.SELECTION_VERTICAL_OFFSET
    ));
    this.selectionElement.style.setProperty("--sf-selection-transform", `translate(${left}px, ${top}px)`);
    this.selectionElement.style.setProperty("--sf-selection-width", `${width}px`);
    this.selectionElement.style.setProperty("--sf-selection-height", `${height}px`);
    this.selectionElement.classList.add("sf-selection--visible");
    this.updateFlashlightWithCutout(left, top, width, height);
  }
  /**
   * Validates pointer animation state
   */
  validatePointerState() {
    return this.animationStartTime !== null && this.currentAnimation !== null && this.startX !== null && this.startY !== null && this.targetX !== null && this.targetY !== null && this.controlPointX !== null && this.controlPointY !== null;
  }
  /**
   * Validates selection animation state
   */
  validateSelectionState() {
    if (!this.animationStartTime || !this.currentAnimation) {
      return false;
    }
    if (this.dragPhase === "move-to-start") {
      return this.startX !== null && this.startY !== null && this.targetX !== null && this.targetY !== null && this.controlPointX !== null && this.controlPointY !== null;
    }
    if (this.dragPhase === "dragging") {
      return this.dragAnimationStartTime !== null && this.dragStartX !== null && this.dragStartY !== null && this.dragEndX !== null && this.dragEndY !== null;
    }
    return true;
  }
  /**
   * Completes pointer animation
   */
  completePointerAnimation() {
    this.show(this.targetX, this.targetY);
    this.lastCursorX = this.targetX;
    this.lastCursorY = this.targetY;
    this.click();
    setTimeout(() => {
      this.stopAnimation();
    }, 400);
  }
  /**
   * Completes move-to-start phase of selection animation
   */
  completeMoveToStartPhase() {
    this.show(this.targetX, this.targetY);
    this.lastCursorX = this.targetX;
    this.lastCursorY = this.targetY;
    this.dragPhase = "dragging";
    this.dragAnimationStartTime = performance.now();
    const dragDistance = this.calculateDistance(
      this.dragStartX + this.SELECTION_HORIZONTAL_OFFSET,
      this.dragStartY + this.SELECTION_VERTICAL_OFFSET,
      this.dragEndX + this.SELECTION_HORIZONTAL_OFFSET,
      this.dragEndY + this.SELECTION_VERTICAL_OFFSET
    );
    this.animationDuration = this.calculateAnimationDuration(dragDistance);
    if (this.cursor) {
      const x = this.lastCursorX;
      const y = this.lastCursorY;
      this.cursor.style.setProperty("--sf-cursor-transform", `translate(${x}px, ${y}px) translate(-50%, -50%) scale(0.9)`);
      this.cursor.style.setProperty("--sf-cursor-opacity", "0.9");
    }
    if (this.selectionElement) {
      this.selectionElement.style.setProperty("--sf-selection-transform", `translate(${this.dragStartX}px, ${this.dragStartY}px)`);
      this.selectionElement.style.setProperty("--sf-selection-width", "0px");
      this.selectionElement.style.setProperty("--sf-selection-height", "0px");
      this.selectionElement.classList.add("sf-selection--visible");
    }
  }
  /**
   * Completes dragging phase of selection animation
   */
  completeDraggingPhase() {
    this.dragPhase = "release";
    if (this.cursor) {
      const x = this.lastCursorX;
      const y = this.lastCursorY;
      this.cursor.style.setProperty("--sf-cursor-transform", `translate(${x}px, ${y}px) translate(-50%, -50%)`);
      this.cursor.style.setProperty("--sf-cursor-opacity", "1");
    }
    if (this.animationFrameId !== null) {
      cancelAnimationFrame(this.animationFrameId);
      this.animationFrameId = null;
    }
    this.animationStartTime = null;
    this.dragAnimationStartTime = null;
  }
  /**
   * Handles move-to-start phase of selection animation
   */
  handleMoveToStartPhase(timestamp) {
    if (this.currentTargetElement) {
      this.updateDragCoordinatesFromElement();
      const previousTarget = { x: this.targetX, y: this.targetY };
      this.targetX = this.dragStartX + this.SELECTION_HORIZONTAL_OFFSET;
      this.targetY = this.dragStartY + this.SELECTION_VERTICAL_OFFSET;
      const changed = this.animationUtils.hasSignificantPositionChange(
        previousTarget,
        { x: this.targetX, y: this.targetY }
      );
      if (changed) {
        const currentPos = this.getCurrentCursorPosition();
        this.startX = currentPos.x;
        this.startY = currentPos.y;
        this.calculateControlPoint();
        this.totalDistance = this.calculateDistance(this.startX, this.startY, this.targetX, this.targetY);
        this.animationDuration = this.calculateAnimationDuration(this.totalDistance);
        this.animationStartTime = timestamp;
      }
    }
    const elapsed = timestamp - this.animationStartTime;
    if (elapsed >= this.animationDuration) {
      this.completeMoveToStartPhase();
      return;
    }
    const easedProgress = this.animationUtils.calculateEasedProgress(
      elapsed,
      this.animationDuration
    );
    const position = this.animationUtils.calculateBezierPoint(
      easedProgress,
      { x: this.startX, y: this.startY },
      { x: this.controlPointX, y: this.controlPointY },
      { x: this.targetX, y: this.targetY }
    );
    this.show(position.x, position.y);
  }
  /**
   * Handles dragging phase of selection animation
   */
  handleDraggingPhase(timestamp) {
    if (this.currentTargetElement) {
      this.updateDragCoordinatesFromElement();
    }
    const elapsed = timestamp - this.dragAnimationStartTime;
    const progress = Math.min(elapsed / this.animationDuration, 1);
    const easedProgress = 0.5 - 0.5 * Math.cos(progress * Math.PI);
    const currentX = this.dragStartX + (this.dragEndX - this.dragStartX) * easedProgress + this.SELECTION_HORIZONTAL_OFFSET;
    const currentY = this.dragStartY + (this.dragEndY - this.dragStartY) * easedProgress + this.SELECTION_VERTICAL_OFFSET;
    this.show(currentX, currentY);
    this.updateSelectionRectangle(currentX, currentY);
    if (progress >= 1) {
      this.completeDraggingPhase();
    }
  }
  /**
   * Handles pointer mode animation
   */
  handlePointerAnimation(timestamp) {
    if (!this.validatePointerState()) {
      console.warn("CursorManager: Animation frame missing essential data");
      this.stopAnimation();
      return;
    }
    const elapsed = timestamp - this.animationStartTime;
    if (this.currentTargetElement) {
      const currentRect = this.currentTargetElement.getBoundingClientRect();
      const previousTarget = { x: this.targetX, y: this.targetY };
      this.targetX = currentRect.left + currentRect.width / 2 + this.POINTER_HORIZONTAL_OFFSET;
      this.targetY = currentRect.top + currentRect.height / 2 + this.POINTER_VERTICAL_OFFSET;
      if (this.animationUtils.hasSignificantPositionChange(previousTarget, { x: this.targetX, y: this.targetY })) {
        const currentPos = this.getCurrentCursorPosition();
        this.startX = currentPos.x;
        this.startY = currentPos.y;
        this.calculateControlPoint();
        this.totalDistance = this.calculateDistance(this.startX, this.startY, this.targetX, this.targetY);
        this.animationDuration = this.calculateAnimationDuration(this.totalDistance);
        this.animationStartTime = timestamp;
      }
    }
    if (elapsed >= this.animationDuration) {
      this.completePointerAnimation();
      return;
    }
    const easedProgress = this.animationUtils.calculateEasedProgress(
      elapsed,
      this.animationDuration
    );
    const position = this.animationUtils.calculateBezierPoint(
      easedProgress,
      { x: this.startX, y: this.startY },
      { x: this.controlPointX, y: this.controlPointY },
      { x: this.targetX, y: this.targetY }
    );
    this.show(position.x, position.y);
    this.animationFrameId = requestAnimationFrame(this.unifiedAnimationFrame.bind(this));
  }
  /**
   * Handles selection mode animation with phases
   */
  handleSelectionAnimation(timestamp) {
    if (!this.validateSelectionState()) {
      this.stopAnimation();
      return;
    }
    switch (this.dragPhase) {
      case "move-to-start":
        this.handleMoveToStartPhase(timestamp);
        break;
      case "dragging":
        this.handleDraggingPhase(timestamp);
        break;
    }
    if (this.dragPhase !== "release") {
      this.animationFrameId = requestAnimationFrame(this.unifiedAnimationFrame.bind(this));
    }
  }
  /**
   * Unified animation frame handler that dispatches to appropriate mode
   */
  unifiedAnimationFrame(timestamp) {
    if (!this.animationUtils.validateAnimationState()) {
      return;
    }
    if (this.isSelectionMode) {
      this.handleSelectionAnimation(timestamp);
    } else {
      this.handlePointerAnimation(timestamp);
    }
  }
  /**
   * Updates the flashlight overlay to exclude the selection area
   */
  updateFlashlightWithCutout(left, top, width, height) {
    if (!this.flashlightOverlay) {
      return;
    }
    const radius = 4;
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;
    const clipPath = `path(evenodd, "M 0 0 L ${viewportWidth} 0 L ${viewportWidth} ${viewportHeight} L 0 ${viewportHeight} Z M ${left + radius} ${top} L ${left + width - radius} ${top} A ${radius} ${radius} 0 0 1 ${left + width} ${top + radius} L ${left + width} ${top + height - radius} A ${radius} ${radius} 0 0 1 ${left + width - radius} ${top + height} L ${left + radius} ${top + height} A ${radius} ${radius} 0 0 1 ${left} ${top + height - radius} L ${left} ${top + radius} A ${radius} ${radius} 0 0 1 ${left + radius} ${top} Z")`;
    this.flashlightOverlay.style.setProperty("--sf-flashlight-clip", clipPath);
    const x = this.lastCursorX;
    const y = this.lastCursorY;
    this.flashlightOverlay.style.setProperty("--sf-flashlight-bg", `radial-gradient(circle 150px at ${x}px ${y}px, transparent 0%, rgba(0, 0, 0, 0.4) 100%)`);
  }
  /**
   * Calculates a control point for curved cursor movement
   */
  calculateControlPoint() {
    if (!this.startX || !this.startY || !this.targetX || !this.targetY) {
      console.warn("CursorManager: Missing start or target position for control point calculation", {
        startX: this.startX,
        startY: this.startY,
        targetX: this.targetX,
        targetY: this.targetY
      });
      return;
    }
    const midpointX = (this.startX + this.targetX) / 2;
    const midpointY = (this.startY + this.targetY) / 2;
    const distance = Math.sqrt(
      Math.pow(this.targetX - this.startX, 2) + Math.pow(this.targetY - this.startY, 2)
    );
    this.totalDistance = distance;
    const maxOffset = distance * 0.2;
    const randomOffset = (Math.random() - 0.5) * maxOffset * 2;
    const vectorX = this.targetX - this.startX;
    const vectorY = this.targetY - this.startY;
    const perpVectorX = -vectorY;
    const perpVectorY = vectorX;
    const perpLength = Math.sqrt(perpVectorX * perpVectorX + perpVectorY * perpVectorY);
    if (perpLength === 0) {
      this.controlPointX = midpointX + 5;
      this.controlPointY = midpointY + 5;
    } else {
      const normalizedPerpX = perpVectorX / perpLength;
      const normalizedPerpY = perpVectorY / perpLength;
      this.controlPointX = midpointX + normalizedPerpX * randomOffset;
      this.controlPointY = midpointY + normalizedPerpY * randomOffset;
    }
  }
  /**
   * Moves the cursor to a DOM element
   * @param selector - DOM element selector
   * @param mode - Optional mode for cursor (pointer or selection)
   * @param selectionStyles - Optional styles for selection mode
   */
  async moveToElement(selector, mode, selectionStyles) {
    if (!this.canShowCursor()) {
      return;
    }
    this.stopAnimation();
    this.removeScrollListenersFromParents();
    this.currentTargetElement = null;
    this.isSelectionMode = false;
    this.resetFlashlightOverlay();
    if (this.isAutoplayBlocked()) {
      return;
    }
    const targetElement = await this.findElementAndScrollIntoView(selector);
    if (!targetElement) {
      console.warn("CursorManager: Target element not found:", selector);
      this.setShouldShowCursor(false);
      this.hideCursorElements();
      this.waitForElement(selector, () => this.moveToElement(selector, mode, selectionStyles));
      return;
    }
    this.currentTargetElement = targetElement;
    this.addScrollListenersToParents(targetElement);
    const animation = {
      targetSelector: selector,
      // Store the selector for clicking after animation
      mode: mode || "selection",
      // Default to selection mode
      selectionStyles
    };
    this.animate(animation);
  }
  /**
   * Simulates a click action with the cursor
   */
  click() {
    if (!this.canShowCursor()) {
      return;
    }
    if (this.cursor) {
      const x = this.lastCursorX;
      const y = this.lastCursorY;
      this.cursor.style.setProperty("--sf-cursor-transform", `translate(${x}px, ${y}px) translate(-50%, -50%) scale(0.8)`);
      this.cursor.style.setProperty("--sf-cursor-opacity", "0.8");
      setTimeout(() => {
        if (this.cursor) {
          if (!this.canShowCursor()) {
            this.hideCursorElements();
            return;
          }
          this.cursor.style.setProperty("--sf-cursor-transform", `translate(${x}px, ${y}px) translate(-50%, -50%)`);
          this.cursor.style.setProperty("--sf-cursor-opacity", "1");
        }
      }, 300);
    }
  }
  /**
   * Stops the current animation
   */
  stopAnimation() {
    if (this.animationFrameId !== null) {
      cancelAnimationFrame(this.animationFrameId);
      this.animationFrameId = null;
    }
    this.animationStartTime = null;
    this.currentAnimation = null;
    this.controlPointX = null;
    this.controlPointY = null;
    this.dragPhase = "move-to-start";
    this.dragAnimationStartTime = null;
    if (this.cursor) {
      const x = this.lastCursorX;
      const y = this.lastCursorY;
      this.cursor.style.setProperty("--sf-cursor-transform", `translate(${x}px, ${y}px) translate(-50%, -50%)`);
      this.cursor.style.setProperty("--sf-cursor-opacity", "1");
    }
    this.resetFlashlightOverlay();
    if (this.selectionElement) {
      this.selectionElement.classList.remove("sf-selection--visible");
    }
  }
  /**
   * Resets cursor state for first animation
   */
  resetFirstAnimation() {
    this.isFirstAnimation = true;
  }
  /**
   * Cleans up resources used by the cursor manager
   */
  /**
   * Reset the CursorManager to a clean state for new playlist
   * Stops animations and clears state but keeps DOM elements for reuse
   */
  reset() {
    this.stopAnimation();
    this.currentAnimation = null;
    this.animationStartTime = null;
    this.startX = null;
    this.startY = null;
    this.targetX = null;
    this.targetY = null;
    this.currentTargetElement = null;
    this.isFirstAnimation = true;
    this.shouldShowCursor = false;
    if (this.cursor) {
      this.cursor.classList.remove("sf-cursor--visible");
    }
    this.isSelectionMode = false;
    if (this.selectionElement) {
      this.selectionElement.classList.remove("sf-selection--visible");
    }
    this.dragStartX = null;
    this.dragStartY = null;
    this.dragEndX = null;
    this.dragEndY = null;
    this.dragPhase = "move-to-start";
    this.dragAnimationStartTime = null;
    if (this.scrollRafId !== null) {
      cancelAnimationFrame(this.scrollRafId);
      this.scrollRafId = null;
    }
    this.scrollUpdatePending = false;
    this.removeScrollListenersFromParents();
    this.scrollableParents = [];
  }
  async destroy() {
    this.stopAnimation();
    if (this.boundScrollHandler) {
      window.removeEventListener("scroll", this.boundScrollHandler);
      this.boundScrollHandler = null;
    }
    this.removeScrollListenersFromParents();
    if (this.scrollRafId !== null) {
      cancelAnimationFrame(this.scrollRafId);
      this.scrollRafId = null;
    }
    this.scrollUpdatePending = false;
    if (this.cursor && this.cursor.parentNode) {
      this.cursor.remove();
      this.cursor = null;
    }
    if (this.labelElement && this.labelElement.parentNode) {
      this.labelElement.remove();
      this.labelElement = null;
    }
    this.labelText = null;
    if (this.selectionElement && this.selectionElement.parentNode) {
      this.selectionElement.remove();
      this.selectionElement = null;
    }
    if (this.flashlightOverlay && this.flashlightOverlay.parentNode) {
      this.flashlightOverlay.remove();
      this.flashlightOverlay = null;
    }
    this.isFirstAnimation = true;
    this.shouldShowCursor = false;
    this.currentTargetElement = null;
    this.isSelectionMode = false;
    if (this.targetMutationObserver) {
      this.targetMutationObserver.disconnect();
      this.targetMutationObserver = null;
    }
    document.documentElement.style.removeProperty("--sf-cursor-x");
    document.documentElement.style.removeProperty("--sf-cursor-y");
  }
  /**
   * Sets the color of the cursor SVG, selection highlight, and label
   * @param color - The color to set (hex, rgb, etc)
   */
  setColor(color) {
    if (this.cursor) {
      const svg = this.cursor.querySelector("svg");
      if (svg) {
        const path = svg.querySelector("path");
        if (path) {
          path.setAttribute("fill", color);
        }
      }
    }
    if (this.selectionElement) {
      this.selectionElement.style.setProperty("--sf-selection-color", color);
      this.selectionElement.style.setProperty("--sf-selection-bg-color", colorToRgba(color, 0));
    }
    if (this.labelElement) {
      this.labelElement.style.setProperty("--sf-cursor-label-bg", color);
      this.labelElement.style.setProperty("--sf-cursor-label-text", getContrastingTextColor(color));
    }
  }
  /**
   * Sets the label text to display beside the cursor
   * @param label - The label text to display (e.g., avatar name), or null to hide
   */
  setLabel(label) {
    var _a;
    this.labelText = label;
    if (this.labelElement) {
      if (label) {
        this.labelElement.textContent = label;
        if ((_a = this.cursor) == null ? void 0 : _a.classList.contains("sf-cursor--visible")) {
          this.labelElement.classList.add("sf-cursor-label--visible");
        }
      } else {
        this.labelElement.textContent = "";
        this.labelElement.classList.remove("sf-cursor-label--visible");
      }
    }
  }
}
const TRACKING_PARAMS = [
  // UTM parameters (Google Analytics standard)
  "utm_source",
  "utm_medium",
  "utm_campaign",
  "utm_term",
  "utm_content",
  "utm_id",
  // Google Ads
  "gclid",
  // Google Click ID
  "gclsrc",
  // Google Click Source
  "gad_source",
  // Google Ads source
  "dclid",
  // Google Display Click ID
  "gbraid",
  // Google iOS App campaign ID (for privacy)
  "wbraid",
  // Google Web-to-App campaign ID (for privacy)
  // Facebook/Meta Ads
  "fbclid",
  // Facebook Click ID
  // Microsoft Ads
  "msclkid",
  // Microsoft Click ID
  // LinkedIn Ads
  "li_fat_id",
  // LinkedIn First-Party Ad Tracking ID
  // Twitter/X Ads
  "twclid",
  // Twitter Click ID
  // TikTok Ads
  "ttclid",
  // TikTok Click ID
  // Other common tracking params
  "ref",
  // Referral parameter
  "_ga",
  // Google Analytics client ID
  "_gl"
  // Google cross-domain linker
];
function getCurrentTrackingParams() {
  const params = /* @__PURE__ */ new Map();
  try {
    const currentUrl = new URL(window.location.href);
    for (const param of TRACKING_PARAMS) {
      const value = currentUrl.searchParams.get(param);
      if (value !== null) {
        params.set(param, value);
      }
    }
    if (params.size > 0) {
      log(`urlParams: Found ${params.size} tracking params in current URL: ${Array.from(params.keys()).join(", ")}`);
    }
  } catch (error2) {
  }
  return params;
}
function appendTrackingParams(targetUrl) {
  if (!targetUrl) {
    return targetUrl;
  }
  try {
    let url;
    if (targetUrl.startsWith("/") || !targetUrl.includes("://")) {
      url = new URL(targetUrl, window.location.origin);
    } else {
      url = new URL(targetUrl);
    }
    const trackingParams = getCurrentTrackingParams();
    if (trackingParams.size === 0) {
      log("urlParams: No tracking params to append");
      return targetUrl;
    }
    let addedParams = 0;
    for (const [key, value] of trackingParams) {
      if (!url.searchParams.has(key)) {
        url.searchParams.set(key, value);
        addedParams++;
      } else {
        log(`urlParams: Skipping ${key} - already exists in target URL`);
      }
    }
    if (addedParams > 0) {
      log(`urlParams: Appended ${addedParams} tracking params to URL`);
      return url.toString();
    }
    return targetUrl;
  } catch (error2) {
    return targetUrl;
  }
}
class InteractionManager {
  constructor() {
    __publicField(this, "container", null);
    __publicField(this, "buttons", []);
    __publicField(this, "buttonContainer", null);
    __publicField(this, "scrollIndicator", null);
    __publicField(this, "storeUnsubscribe", null);
    __publicField(this, "storageManager", StorageManager.getInstance());
    /**
     * Handles the video90PercentReached event
     * @param _event - The custom event dispatched when video reaches 90%
     */
    __publicField(this, "handleVideo90PercentReached", (_event) => {
      this.showButtonsAt90Percent();
    });
  }
  /**
   * Creates interaction elements
   * @param container - The container element for interactions
   */
  create(container) {
    this.container = container;
    this.storeUnsubscribe = useSaltfishStore.subscribe((state, prevState) => {
      if (state.isMinimized !== (prevState == null ? void 0 : prevState.isMinimized)) {
        this.updateButtonPositions();
      }
    });
    container.addEventListener("video90PercentReached", this.handleVideo90PercentReached);
  }
  /**
   * Creates interactive buttons positioned within the shadow DOM
   * @param buttons - Button configurations
   */
  createButtons(buttons) {
    if (!this.container) {
      return;
    }
    this.clearButtons();
    this.buttonContainer = document.createElement("div");
    const needsScrolling = buttons.length > 4;
    let containerClass = "sf-choice-buttons-container";
    if (needsScrolling) {
      containerClass += " sf-choice-buttons-container--scrollable";
    }
    this.buttonContainer.className = containerClass;
    log(`InteractionManager: Using button layout for ${buttons.length} buttons, needsScrolling: ${needsScrolling}`);
    this.updateCenterPlayButtonPosition(buttons.length);
    this.container.appendChild(this.buttonContainer);
    if (needsScrolling) {
      this.scrollIndicator = document.createElement("div");
      this.scrollIndicator.className = "sf-scroll-indicator";
      this.scrollIndicator.innerHTML = `
        <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
          <path d="M8 12l-4-4h8l-4 4z"/>
        </svg>
      `;
      this.container.appendChild(this.scrollIndicator);
    }
    buttons.forEach((buttonConfig) => {
      const button = document.createElement("button");
      button.textContent = buttonConfig.text;
      button.dataset.buttonId = buttonConfig.id;
      button.className = `sf-choice-button sf-choice-button--${buttonConfig.action.type}`;
      if (buttonConfig.style) {
        Object.entries(buttonConfig.style).forEach(([prop, value]) => {
          button.style.setProperty(`--custom-${prop}`, String(value));
        });
      }
      button.addEventListener("click", async (event) => {
        await this.handleButtonClick(event, buttonConfig);
      });
      if (this.buttonContainer) {
        this.buttonContainer.appendChild(button);
        this.buttons.push(button);
        log(`InteractionManager: Added button '${buttonConfig.id}'`);
      }
    });
    if (needsScrolling && this.buttonContainer) {
      this.setupScrollIndicator(this.buttonContainer);
    }
    const store = getSaltfishStore();
    if (store.isMinimized) {
      this.buttonContainer.classList.add("sf-hidden");
    }
  }
  /**
   * Shows buttons with staggered animation (called when video reaches 90%)
   */
  showButtonsAt90Percent() {
    if (this.buttons.length > 0) {
      this.buttons.forEach((button, index) => {
        button.classList.add("sf-show-button");
      });
      if (this.scrollIndicator) {
        this.scrollIndicator.classList.add("sf-show-scroll-indicator");
      }
    }
  }
  /**
   * Sets up scroll detection to hide arrow when scrolled to bottom
   * @param container - The scrollable container element
   */
  setupScrollIndicator(container) {
    if (!this.scrollIndicator) return;
    const handleScroll = () => {
      if (!this.scrollIndicator) return;
      const scrollHeight = container.scrollHeight;
      const scrollTop = container.scrollTop;
      const clientHeight = container.clientHeight;
      if (scrollHeight - scrollTop - clientHeight < THRESHOLDS.SCROLL_THRESHOLD_PX) {
        this.scrollIndicator.classList.add("sf-scroll-indicator--hidden");
      } else {
        this.scrollIndicator.classList.remove("sf-scroll-indicator--hidden");
      }
    };
    container.addEventListener("scroll", handleScroll);
  }
  /**
   * Updates center play button position based on button count
   * Positions the button centered between the top of the player and the top of the first button
   * @param buttonCount - Number of buttons displayed
   */
  updateCenterPlayButtonPosition(buttonCount) {
    if (!this.container) return;
    const centerPlayButton = this.container.querySelector(".sf-player__center-play-button");
    if (centerPlayButton) {
      if (buttonCount > 0) {
        const buttonHeight = 36;
        const gap = 8;
        let containerHeight;
        if (buttonCount > 4) {
          containerHeight = 176;
        } else {
          containerHeight = buttonCount * buttonHeight + (buttonCount - 1) * gap;
        }
        centerPlayButton.style.setProperty("--sf-button-container-height", `${containerHeight}px`);
        centerPlayButton.classList.add("sf-player__center-play-button--with-buttons");
      } else {
        centerPlayButton.style.removeProperty("--sf-button-container-height");
        centerPlayButton.classList.remove("sf-player__center-play-button--with-buttons");
      }
    }
  }
  /**
   * Clears all interactive buttons
   */
  clearButtons() {
    this.updateCenterPlayButtonPosition(0);
    this.buttons.forEach((button) => {
      if (button.parentElement) {
        button.parentElement.removeChild(button);
      }
    });
    if (this.buttonContainer && this.buttonContainer.parentElement) {
      this.buttonContainer.parentElement.removeChild(this.buttonContainer);
    }
    if (this.scrollIndicator && this.scrollIndicator.parentElement) {
      this.scrollIndicator.parentElement.removeChild(this.scrollIndicator);
    }
    this.buttons = [];
    this.buttonContainer = null;
    this.scrollIndicator = null;
  }
  /**
   * Updates button visibility and positions based on player state
   */
  updateButtonPositions() {
    if (!this.buttonContainer || this.buttons.length === 0) {
      return;
    }
    const store = getSaltfishStore();
    if (store.isMinimized) {
      this.buttonContainer.classList.add("sf-hidden");
      if (this.scrollIndicator) {
        this.scrollIndicator.classList.add("sf-hidden");
      }
      return;
    } else {
      this.buttonContainer.classList.remove("sf-hidden");
      if (this.scrollIndicator) {
        this.scrollIndicator.classList.remove("sf-hidden");
      }
    }
  }
  /**
   * Handles button clicks
   * @param event - The click event
   * @param buttonConfig - The button configuration
   */
  async handleButtonClick(event, buttonConfig) {
    var _a, _b, _c, _d, _e, _f;
    log(`InteractionManager: Button click detected on button "${buttonConfig.id}"`);
    event.preventDefault();
    event.stopPropagation();
    const store = getSaltfishStore();
    switch (buttonConfig.action.type) {
      case "next":
        store.play();
        break;
      case "goto":
        if (buttonConfig.action.url) {
          log(`InteractionManager: Goto button with URL redirect - updating to step "${buttonConfig.action.target}" then redirecting to "${buttonConfig.action.url}"`);
          await this.flushAnalytics();
          const currentStore = getSaltfishStore();
          if (currentStore.manifest) {
            useSaltfishStore.setState((state) => {
              state.currentStepId = buttonConfig.action.target;
              if (state.manifest) {
                state.progress[state.manifest.id] = {
                  ...state.progress[state.manifest.id],
                  lastStepId: buttonConfig.action.target,
                  lastProgressAt: Date.now(),
                  // Use timestamp for 6-second rule
                  lastVisited: (/* @__PURE__ */ new Date()).toISOString()
                };
              }
            });
            try {
              log(`InteractionManager: Updating backend with step ${buttonConfig.action.target} before redirect`);
              if (((_a = currentStore.config) == null ? void 0 : _a.token) && ((_b = currentStore.user) == null ? void 0 : _b.id) && !((_c = currentStore.user) == null ? void 0 : _c.__isAnonymous)) {
                const apiUrl = `https://player.saltfish.ai/clients/${currentStore.config.token}/users/${currentStore.user.id}/playlists/${currentStore.manifest.id}`;
                log(`InteractionManager: Making API call to ${apiUrl}`);
                const response = await fetch(apiUrl, {
                  method: "POST",
                  headers: {
                    "Content-Type": "application/json"
                  },
                  body: JSON.stringify({
                    status: "in_progress",
                    currentStepId: buttonConfig.action.target
                  })
                });
                if (!response.ok) {
                  log(`InteractionManager: API call failed: ${response.statusText} (${response.status})`);
                } else {
                  log(`InteractionManager: Backend update complete for step ${buttonConfig.action.target}`);
                }
              } else {
                log(`InteractionManager: Cannot update backend - user not identified or anonymous`);
              }
            } catch (error2) {
              console.error("InteractionManager: Full error:", error2);
            }
            const playlistPersistence = ((_d = currentStore.playlistOptions) == null ? void 0 : _d.persistence) ?? true;
            if (playlistPersistence) {
              const userId = (_e = currentStore.user) == null ? void 0 : _e.id;
              const updatedStore = getSaltfishStore();
              this.storageManager.setProgress(updatedStore.progress, userId);
              log(`InteractionManager: Saved progress for step ${buttonConfig.action.target} before URL redirect`);
              if ((_f = currentStore.user) == null ? void 0 : _f.__isAnonymous) {
                const anonymousData = this.storageManager.getAnonymousUserData();
                if (anonymousData) {
                  anonymousData.watchedPlaylists = anonymousData.watchedPlaylists || {};
                  anonymousData.watchedPlaylists[currentStore.manifest.id] = {
                    status: "in_progress",
                    currentStepId: buttonConfig.action.target,
                    timestamp: Date.now(),
                    // Use timestamp for consistency with checkAndResumeInProgressPlaylist
                    lastProgressAt: Date.now()
                    // Keep for backward compatibility
                  };
                  this.storageManager.setAnonymousUserData(anonymousData);
                  log(`InteractionManager: Updated anonymous user watchedPlaylists for step ${buttonConfig.action.target}`);
                }
              }
            }
          }
          const redirectUrl = appendTrackingParams(buttonConfig.action.url);
          window.location.href = redirectUrl;
        } else {
          log(`InteractionManager: Goto button clicked, going to step "${buttonConfig.action.target}"`);
          store.goToStep(buttonConfig.action.target);
        }
        break;
      case "url":
        log(`InteractionManager: URL button clicked, opening "${buttonConfig.action.target}"`);
        await this.flushAnalytics();
        if (store.currentState === "completedWaitingForInteraction") {
          store.completePlaylist();
          await new Promise((resolve) => setTimeout(resolve, TIMING.STATE_PROCESSING_DELAY_MS));
        } else {
          const isLastStep = this.isCurrentStepLast(store);
          if (isLastStep) {
            store.goToStep("completed");
            await new Promise((resolve) => setTimeout(resolve, TIMING.STATE_PROCESSING_DELAY_MS));
          }
        }
        const openUrl = appendTrackingParams(buttonConfig.action.target);
        window.open(openUrl, "_blank");
        const { SaltfishPlayer: SaltfishPlayer2 } = await Promise.resolve().then(() => SaltfishPlayer$1);
        const player = SaltfishPlayer2.getInstance();
        if (player) {
          player.destroy();
        }
        break;
      case "playlist":
        log(`InteractionManager: Playlist button clicked, starting playlist "${buttonConfig.action.target}"`);
        try {
          await this.flushAnalytics();
          log("InteractionManager: Completing current playlist before starting new one");
          store.completePlaylist();
          await new Promise((resolve) => setTimeout(resolve, TIMING.STATE_PROCESSING_DELAY_MS));
          const { SaltfishPlayer: SaltfishPlayer22 } = await Promise.resolve().then(() => SaltfishPlayer$1);
          const player2 = SaltfishPlayer22.getInstance();
          if (player2) {
            log(`InteractionManager: Starting new playlist ${buttonConfig.action.target}`);
            await player2.startPlaylist(buttonConfig.action.target);
          } else {
            log("InteractionManager: No SaltfishPlayer instance found");
          }
        } catch (error2) {
          console.error(`InteractionManager: Error starting playlist "${buttonConfig.action.target}":`, error2);
          log(`InteractionManager: Failed to start playlist ${buttonConfig.action.target}: ${error2}`);
        }
        break;
      default:
        log(`InteractionManager: Unknown button action type: "${buttonConfig.action.type}"`);
        break;
    }
    if (store.manifest) {
      const analyticsData = {
        buttonId: buttonConfig.id,
        actionType: buttonConfig.action.type,
        actionTarget: buttonConfig.action.target
      };
      log(`InteractionManager: Tracked button interaction: ${JSON.stringify(analyticsData)}`);
    }
  }
  /**
   * Checks if the current step is the last step in the playlist
   * @param store - The current store state
   * @returns true if current step is the last step, false otherwise
   */
  isCurrentStepLast(store) {
    if (!store.manifest || !store.currentStepId || !store.manifest.steps) {
      return false;
    }
    const steps = store.manifest.steps;
    const currentStepIndex = steps.findIndex((step) => step.id === store.currentStepId);
    return currentStepIndex === steps.length - 1;
  }
  /**
   * Flushes analytics events to ensure they're sent before player destruction
   */
  async flushAnalytics() {
    try {
      const { SaltfishPlayer: SaltfishPlayer2 } = await Promise.resolve().then(() => SaltfishPlayer$1);
      const player = SaltfishPlayer2.getInstance();
      const playerWithManagers = player;
      if (player && playerWithManagers.analyticsManager) {
        log("InteractionManager: Flushing analytics before URL button destruction");
        const analyticsManager = playerWithManagers.analyticsManager;
        if (typeof analyticsManager.flush === "function") {
          await analyticsManager.flush();
        }
      }
    } catch (error2) {
    }
  }
  /**
   * Resets the interaction manager to initial state for reuse
   */
  reset() {
    this.clearButtons();
    this.buttonContainer = null;
    this.scrollIndicator = null;
    if (this.container) {
      this.container.removeEventListener("video90PercentReached", this.handleVideo90PercentReached);
    }
  }
  /**
   * Cleans up resources used by the interaction manager
   */
  destroy() {
    if (this.storeUnsubscribe) {
      this.storeUnsubscribe();
      this.storeUnsubscribe = null;
    }
    if (this.container) {
      this.container.removeEventListener("video90PercentReached", this.handleVideo90PercentReached);
    }
    this.clearButtons();
    this.container = null;
  }
}
class EventSubscriberManager {
  /**
   * Creates a new event-subscribing manager instance
   * @param eventManager - Optional event manager to subscribe to events
   */
  constructor(eventManager) {
    __publicField(this, "eventManager", null);
    if (eventManager) {
      this.setEventManager(eventManager);
    }
  }
  /**
   * Sets the event manager and subscribes to relevant events
   * @param eventManager - Event manager instance
   */
  setEventManager(eventManager) {
    this.eventManager = eventManager;
    this.subscribeToEvents();
  }
}
class AnalyticsManager extends EventSubscriberManager {
  /**
   * Creates a new AnalyticsManager
   * @param eventManager - Optional event manager to subscribe to events
   */
  constructor(eventManager) {
    super(eventManager);
    __publicField(this, "config", null);
    __publicField(this, "user", null);
    __publicField(this, "eventQueue", []);
    __publicField(this, "isSending", false);
    __publicField(this, "flushInterval", null);
    __publicField(this, "sessionId", null);
    __publicField(this, "analyticsEnabled", true);
    // Default to enabled
    __publicField(this, "boundVisibilityHandler", null);
  }
  /**
   * Subscribe to relevant player events for analytics tracking
   */
  subscribeToEvents() {
    if (!this.eventManager) {
      return;
    }
    this.eventManager.on("playerPaused", (_) => {
      const store = getSaltfishStore();
      const runId = this.getRunId();
      if ((store == null ? void 0 : store.manifest) && store.currentStepId && runId) {
        this.trackEvent({
          type: "playerPaused",
          playlistId: store.manifest.id,
          stepId: store.currentStepId,
          runId,
          timestamp: Date.now()
        });
      }
    });
    this.eventManager.on("playerResumed", (_) => {
      const store = getSaltfishStore();
      const runId = this.getRunId();
      if ((store == null ? void 0 : store.manifest) && store.currentStepId && runId) {
        this.trackEvent({
          type: "playerResumed",
          playlistId: store.manifest.id,
          stepId: store.currentStepId,
          runId,
          timestamp: Date.now()
        });
      }
    });
    this.eventManager.on("stepStarted", (event) => {
      log(`AnalyticsManager: Step started event received - ${event.step.id}`);
      this.trackStepStarted(event.playlist.id, event.step.id);
    });
    this.eventManager.on("stepEnded", (event) => {
      log(`AnalyticsManager: Step ended event received - ${event.step.id}`);
      this.trackStepComplete(event.playlist.id, event.step.id);
    });
    this.eventManager.on("playlistEnded", (event) => {
      log(`AnalyticsManager: playlist ended event received - ${event.playlist.id}`);
      this.trackPlaylistComplete(event.playlist.id);
    });
    this.eventManager.on("error", (event) => {
      if (event.playlistId) {
        this.trackError(
          event.playlistId,
          event.error,
          event.stepId,
          event.errorType,
          event.videoUrl,
          event.mediaErrorCode,
          event.mediaErrorMessage,
          event.failureReason
        );
      }
    });
    this.eventManager.on("playerMinimized", (_) => {
      const store = getSaltfishStore();
      const runId = this.getRunId();
      if ((store == null ? void 0 : store.manifest) && store.currentStepId && runId) {
        this.trackEvent({
          type: "playerMinimized",
          playlistId: store.manifest.id,
          stepId: store.currentStepId,
          runId,
          timestamp: Date.now()
        });
      }
    });
    this.eventManager.on("playerMaximized", (_) => {
      const store = getSaltfishStore();
      const runId = this.getRunId();
      if ((store == null ? void 0 : store.manifest) && store.currentStepId && runId) {
        this.trackEvent({
          type: "playerMaximized",
          playlistId: store.manifest.id,
          stepId: store.currentStepId,
          runId,
          timestamp: Date.now()
        });
      }
    });
    this.eventManager.on("playlistStarted", (event) => {
      log(`AnalyticsManager: Playlist started event received - ${event.playlist.id}`);
      this.trackPlaylistStart(event.playlist.id);
    });
  }
  /**
   * Initializes the analytics manager
   * @param config - Saltfish configuration
   * @param sessionId - Unique session identifier
   */
  initialize(config, sessionId) {
    this.config = config;
    this.analyticsEnabled = config.enableAnalytics !== false;
    if (sessionId) {
      this.sessionId = sessionId;
    }
    if (this.analyticsEnabled) {
      this.flushInterval = window.setInterval(() => {
        this.flushEvents();
      }, ANALYTICS.FLUSH_INTERVAL_MS);
      this.boundVisibilityHandler = this.handleVisibilityChange.bind(this);
      document.addEventListener("visibilitychange", this.boundVisibilityHandler);
    }
  }
  /**
   * Sets the current user
   * @param user - User data
   */
  setUser(user) {
    this.user = user;
  }
  /**
   * Tracks a playlist start event
   * @param playlistId - playlist ID
   */
  trackPlaylistStart(playlistId) {
    if (!this.analyticsEnabled) {
      return;
    }
    const runId = this.getRunId();
    if (runId) {
      this.trackEvent({
        type: "playlistStart",
        playlistId,
        runId,
        timestamp: Date.now()
      });
    }
  }
  /**
   * Tracks a playlist completion event
   * @param playlistId - playlist ID
   */
  trackPlaylistComplete(playlistId) {
    if (!this.analyticsEnabled) {
      return;
    }
    const runId = this.getRunId();
    if (runId) {
      this.trackEvent({
        type: "playlistComplete",
        playlistId,
        runId,
        timestamp: Date.now()
      });
    }
  }
  /**
   * Tracks a step started event
   * @param playlistId - playlist ID
   * @param stepId - Step ID
   */
  trackStepStarted(playlistId, stepId) {
    if (!this.analyticsEnabled) {
      return;
    }
    const runId = this.getRunId();
    if (runId) {
      this.trackEvent({
        type: "stepStarted",
        playlistId,
        stepId,
        runId,
        timestamp: Date.now()
      });
    }
  }
  /**
   * Tracks a step completion event
   * @param playlistId - playlist ID
   * @param stepId - Step ID
   */
  trackStepComplete(playlistId, stepId) {
    if (!this.analyticsEnabled) {
      return;
    }
    const runId = this.getRunId();
    if (runId) {
      this.trackEvent({
        type: "stepComplete",
        playlistId,
        stepId,
        runId,
        timestamp: Date.now()
      });
    }
  }
  /**
   * Tracks an interaction event
   * @param playlistId - playlist ID
   * @param stepId - Step ID
   * @param interactionData - Interaction data
   */
  trackInteraction(playlistId, stepId, interactionData) {
    if (!this.analyticsEnabled) {
      return;
    }
    const runId = this.getRunId();
    if (runId) {
      this.trackEvent({
        type: "interaction",
        playlistId,
        stepId,
        runId,
        timestamp: Date.now(),
        data: interactionData
      });
    }
  }
  /**
   * Tracks an error event
   * @param playlistId - playlist ID
   * @param error - Error object
   * @param stepId - Optional step ID
   * @param errorType - Optional error type/category (e.g., 'playlist', 'video', 'network', 'initialization')
   * @param videoUrl - Optional video URL that failed
   * @param mediaErrorCode - Optional browser MediaError code
   * @param mediaErrorMessage - Optional browser MediaError message
   * @param failureReason - Optional failure reason categorization
   */
  trackError(playlistId, error2, stepId, errorType, videoUrl, mediaErrorCode, mediaErrorMessage, failureReason) {
    if (!this.analyticsEnabled) {
      return;
    }
    const runId = this.getRunId();
    if (runId) {
      this.trackEvent({
        type: "error",
        playlistId,
        stepId,
        runId,
        timestamp: Date.now(),
        data: {
          message: error2.message,
          stack: error2.stack,
          errorType: errorType || "unknown",
          videoUrl: videoUrl || null,
          mediaErrorCode: mediaErrorCode || null,
          mediaErrorMessage: mediaErrorMessage || null,
          failureReason: failureReason || "unknown"
        }
      });
    }
  }
  /**
   * Tracks a generic event
   * @param event - Event data
   */
  trackEvent(event) {
    if (!this.analyticsEnabled) {
      return;
    }
    this.eventQueue.push(event);
    if (this.eventQueue.length >= 10) {
      this.flushEvents();
    }
  }
  /**
   * Manually flush queued events immediately
   * This is useful for ensuring events are sent before player destruction
   */
  async flush() {
    await this.flushEvents();
  }
  /**
   * Sends queued events to the backend
   */
  async flushEvents() {
    if (this.isSending || this.eventQueue.length === 0 || !this.config || !this.analyticsEnabled) {
      return;
    }
    this.isSending = true;
    try {
      const events = [...this.eventQueue];
      this.eventQueue = [];
      const payload = {
        token: this.config.token,
        sessionId: this.sessionId,
        user: this.user,
        events
      };
      const response = await fetch("https://player.saltfish.ai/analytics", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(payload)
      });
      if (!response.ok) {
        this.eventQueue = [...events, ...this.eventQueue];
        throw new Error(`Failed to send analytics events: ${response.statusText}`);
      }
    } catch (error2) {
      console.error("Failed to send analytics events:", error2);
    } finally {
      this.isSending = false;
    }
  }
  /**
   * Handles visibility change events to flush analytics when page is hidden
   * Uses sendBeacon for reliable delivery during page unload
   */
  handleVisibilityChange() {
    if (document.visibilityState === "hidden") {
      this.flushWithBeacon();
    }
  }
  /**
   * Flushes events using sendBeacon API for reliable delivery during page unload
   * sendBeacon is fire-and-forget and survives page navigation/close
   */
  flushWithBeacon() {
    if (this.eventQueue.length === 0 || !this.config || !this.analyticsEnabled) {
      return;
    }
    const events = [...this.eventQueue];
    this.eventQueue = [];
    const payload = {
      token: this.config.token,
      sessionId: this.sessionId,
      user: this.user,
      events
    };
    try {
      fetch("https://player.saltfish.ai/analytics", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(payload),
        keepalive: true
      });
    } catch (error2) {
      console.warn("AnalyticsManager: fetch keepalive failed, events returned to queue");
      this.eventQueue = [...events, ...this.eventQueue];
    }
  }
  /**
   * Cleans up resources used by the analytics manager
   */
  destroy() {
    if (this.analyticsEnabled) {
      this.flushEvents();
    }
    if (this.flushInterval !== null) {
      clearInterval(this.flushInterval);
      this.flushInterval = null;
    }
    if (this.boundVisibilityHandler) {
      document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
      this.boundVisibilityHandler = null;
    }
    if (this.eventManager) {
      this.eventManager = null;
    }
    this.config = null;
    this.user = null;
    this.eventQueue = [];
    this.sessionId = null;
    this.analyticsEnabled = true;
  }
  /**
   * Gets the current runId from the player
   * @returns The current runId or null if not available
   */
  getRunId() {
    try {
      const player = SaltfishPlayer.getInstance();
      return player.getRunId();
    } catch (error2) {
      return null;
    }
  }
}
class SessionManager {
  constructor(storageManager2) {
    __publicField(this, "sessionId");
    __publicField(this, "currentRunId", null);
    __publicField(this, "storageManager");
    this.storageManager = storageManager2 || StorageManager.getInstance();
    this.sessionId = this.getOrCreateSession();
    log(`SessionManager: Initialized with sessionId: ${this.sessionId}`);
  }
  /**
   * Gets or creates a persistent session ID
   * @returns The current session ID
   */
  getOrCreateSession() {
    if (typeof window === "undefined") {
      return this.generateUniqueId();
    }
    const sessionData = this.storageManager.getSession();
    if (sessionData) {
      const now = Date.now();
      if (now - sessionData.lastActivity < TIMING.SESSION_EXPIRY) {
        log(`SessionManager: Using existing session: ${sessionData.sessionId}`);
        this.updateSessionActivity(sessionData.sessionId);
        return sessionData.sessionId;
      }
    }
    const newSessionId = this.generateUniqueId();
    this.updateSessionActivity(newSessionId);
    return newSessionId;
  }
  /**
   * Updates the session activity timestamp
   * @param sessionId - The session ID to update
   */
  updateSessionActivity(sessionId) {
    if (typeof window === "undefined") {
      return;
    }
    const sessionData = {
      sessionId,
      lastActivity: Date.now()
    };
    this.storageManager.setSession(sessionData);
  }
  /**
   * Generates a unique ID using UUID v4 format
   * @returns A unique ID string
   */
  generateUniqueId() {
    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
      const r = Math.random() * 16 | 0;
      const v = c === "x" ? r : r & 3 | 8;
      return v.toString(16);
    });
  }
  /**
   * Gets the current session ID
   * @returns The current session ID
   */
  getSessionId() {
    this.updateSessionActivity(this.sessionId);
    return this.sessionId;
  }
  /**
   * Starts a new run and returns the run ID
   * @returns A new unique run ID
   */
  startNewRun() {
    this.currentRunId = this.generateUniqueId();
    log(`SessionManager: Started new run: ${this.currentRunId}`);
    this.updateSessionActivity(this.sessionId);
    return this.currentRunId;
  }
  /**
   * Gets the current run ID
   * @returns The current run ID or null if no run is active
   */
  getCurrentRunId() {
    return this.currentRunId;
  }
  /**
   * Ends the current run
   */
  endCurrentRun() {
    if (this.currentRunId) {
      log(`SessionManager: Ended run: ${this.currentRunId}`);
      this.currentRunId = null;
    }
  }
  /**
   * Forces session expiry (for testing or manual logout)
   */
  expireSession() {
    if (typeof window !== "undefined") {
      this.storageManager.clearSession();
      log(`SessionManager: Manually expired session: ${this.sessionId}`);
    }
    this.sessionId = this.generateUniqueId();
    this.updateSessionActivity(this.sessionId);
    this.currentRunId = null;
    log(`SessionManager: Created new session after manual expiry: ${this.sessionId}`);
  }
  /**
   * Cleans up resources
   */
  destroy() {
    this.updateSessionActivity(this.sessionId);
    this.endCurrentRun();
  }
}
class TransitionManager {
  constructor() {
    // Track active transition listeners to avoid duplicates and ensure proper cleanup
    __publicField(this, "activeTransitions", /* @__PURE__ */ new Map());
    // Track current transition state
    __publicField(this, "waitingForInteraction", false);
    // Track when StateMachineActionHandler is validating to avoid race conditions
    __publicField(this, "isStateMachineValidating", false);
    // Reference to TriggerManager for coordinating playlist triggers
    __publicField(this, "triggerManager", null);
    // beforeunload handler for cross-page URL transitions
    __publicField(this, "beforeUnloadHandler", null);
    /**
     * Handles URL changes by checking active URL path transitions and playlist triggers
     */
    __publicField(this, "handleURLChange", () => {
      if (this.isStateMachineValidating) {
        return;
      }
      const urlPathTransitions = Array.from(this.activeTransitions.entries()).filter(([_, transition]) => {
        var _a;
        return ((_a = transition.data) == null ? void 0 : _a.type) === "url-path";
      });
      let hasValidTransition = false;
      if (urlPathTransitions.length > 0) {
        for (const [_, transition] of urlPathTransitions) {
          if (transition.data && this.isURLPathMatch(transition.data.pattern)) {
            hasValidTransition = true;
            break;
          }
        }
      }
      if (!hasValidTransition) {
        const isValidUrl = this.validateCurrentStepUrl();
        if (!isValidUrl) {
          return;
        }
      }
      if (this.triggerManager) {
        this.triggerManager.evaluateAllTriggers();
      }
      if (hasValidTransition) {
        for (const [_, transition] of urlPathTransitions) {
          if (!transition.data) {
            continue;
          }
          const { pattern, nextStepId } = transition.data;
          if (this.isURLPathMatch(pattern)) {
            StorageManager.getInstance().clearPendingNavigation();
            this.triggerTransition(nextStepId);
            break;
          }
        }
      }
    });
    window.addEventListener("popstate", this.handleURLChange);
    this.monitorHistoryChanges();
  }
  /**
   * Sets the TriggerManager reference for coordinating playlist triggers
   * @param triggerManager - The TriggerManager instance
   */
  setTriggerManager(triggerManager) {
    this.triggerManager = triggerManager;
  }
  /**
   * Monitors history pushState and replaceState methods to detect SPA navigation
   */
  monitorHistoryChanges() {
    const originalPushState = history.pushState;
    const originalReplaceState = history.replaceState;
    history.pushState = (...args) => {
      originalPushState.apply(history, args);
      this.handleURLChange();
    };
    history.replaceState = (...args) => {
      originalReplaceState.apply(history, args);
      this.handleURLChange();
    };
  }
  /**
   * Sets up transitions for a step
   * @param step - The step to set up transitions for
   * @param triggerImmediately - Whether to immediately trigger non-interaction transitions
   * @param skipTimeouts - Whether to skip setting up timeout transitions (useful for manual policy steps)
   */
  setupTransitions(step, triggerImmediately = false, skipTimeouts = false) {
    this.cleanupTransitions();
    log(`TransitionManager: Setting up transitions for step ${step.id}${skipTimeouts ? " (skipping timeouts)" : ""}`);
    step.transitions.forEach((transition) => {
      switch (transition.type) {
        case "dom-click":
          this.setupDOMClickTransition(transition);
          break;
        case "timeout":
          if (!skipTimeouts) {
            this.setupTimeoutTransition(transition, triggerImmediately);
          }
          break;
        case "url-path":
          this.setupURLPathTransition(transition);
          break;
        case "dom-element-visible":
          this.setupDOMElementVisibleTransition(transition);
          break;
        default:
          log(`TransitionManager: Unsupported transition type: ${transition.type}`);
      }
    });
  }
  /**
   * Sets up DOM click transitions
   * @param transition - The transition configuration
   */
  setupDOMClickTransition(transition) {
    if (!transition.target) {
      return;
    }
    const selector = transition.target;
    const nextStepId = transition.nextStep;
    const transitionId = `dom-click-${selector}-${Date.now()}`;
    const handlers = /* @__PURE__ */ new Map();
    let mutationObserver = null;
    const addClickHandlersToElements = (elements) => {
      if (elements.length === 0) {
        return;
      }
      log(`TransitionManager: Found ${elements.length} elements matching selector '${selector}'`);
      elements.forEach((element) => {
        if (handlers.has(element)) {
          return;
        }
        const handler = async (event) => {
          const target = event.target;
          const willCauseRefresh = this.willCauseHardRefresh(target);
          if (willCauseRefresh) {
            event.preventDefault();
            event.stopImmediatePropagation();
            const success = await this.triggerTransitionWithProgress(nextStepId);
            if (success) {
              this.reDispatchClick(event);
            }
          } else {
            this.triggerTransition(nextStepId);
          }
        };
        handlers.set(element, handler);
        element.addEventListener("click", handler, { capture: true });
      });
    };
    const initialElements = transition.expectedElement || transition.expectedSize ? findAllValidElements(selector, transition.expectedElement, transition.expectedSize) : Array.from(document.querySelectorAll(selector));
    addClickHandlersToElements(initialElements);
    mutationObserver = new MutationObserver((mutationsList) => {
      for (const mutation of mutationsList) {
        if (mutation.type === "childList") {
          mutation.addedNodes.forEach((node) => {
            if (node.nodeType === Node.ELEMENT_NODE) {
              const elementNode = node;
              if (elementNode.matches(selector)) {
                if (!transition.expectedElement && !transition.expectedSize || isElementValid(elementNode, transition.expectedElement, transition.expectedSize)) {
                  addClickHandlersToElements([elementNode]);
                }
              }
              const matchingDescendants = elementNode.querySelectorAll(selector);
              if (matchingDescendants.length > 0) {
                const validDescendants = transition.expectedElement || transition.expectedSize ? Array.from(matchingDescendants).filter((el) => isElementValid(el, transition.expectedElement, transition.expectedSize)) : Array.from(matchingDescendants);
                if (validDescendants.length > 0) {
                  log(`TransitionManager: Found ${validDescendants.length} valid descendants matching '${selector}'`);
                  addClickHandlersToElements(validDescendants);
                }
              }
            }
          });
        }
      }
    });
    mutationObserver.observe(document.body, { childList: true, subtree: true });
    this.activeTransitions.set(transitionId, {
      handlers,
      cleanup: () => {
        handlers.forEach((handler, element) => {
          element.removeEventListener("click", handler, { capture: true });
        });
        handlers.clear();
        if (mutationObserver) {
          mutationObserver.disconnect();
          mutationObserver = null;
        }
      },
      data: {
        type: "dom-click",
        pattern: selector,
        nextStepId
      }
    });
  }
  /**
   * Sets up timeout transitions
   * @param transition - The transition configuration
   * @param triggerImmediately - Whether to trigger immediately
   */
  setupTimeoutTransition(transition, triggerImmediately) {
    const nextStepId = transition.nextStep;
    const timeout = transition.timeout || 0;
    const transitionId = `timeout-${timeout}-${Date.now()}`;
    let timeoutId = null;
    if (triggerImmediately) {
      this.triggerTransition(nextStepId);
    } else {
      timeoutId = window.setTimeout(() => {
        this.triggerTransition(nextStepId);
      }, timeout);
    }
    this.activeTransitions.set(transitionId, {
      handlers: /* @__PURE__ */ new Map(),
      // No DOM handlers for timeout transitions
      cleanup: () => {
        if (timeoutId !== null) {
          clearTimeout(timeoutId);
          timeoutId = null;
        }
      },
      data: {
        type: "timeout",
        pattern: "",
        nextStepId
      }
    });
  }
  /**
   * Sets up URL path transitions
   * @param transition - The transition configuration
   */
  setupURLPathTransition(transition) {
    if (!transition.target) {
      return;
    }
    const pathPattern = transition.target;
    const nextStepId = transition.nextStep;
    this.savePendingNavigation(pathPattern, nextStepId);
    this.setupBeforeUnloadHandler(pathPattern, nextStepId);
    const initialMatch = this.isURLPathMatch(pathPattern);
    if (initialMatch) {
      this.triggerTransition(nextStepId);
      const maxRetries = 5;
      let retries = 0;
      const retryTransition = () => {
        if (retries >= maxRetries) {
          return;
        }
        retries++;
        const currentStore = getSaltfishStore();
        if (currentStore.currentState === "waitingForInteraction" || currentStore.currentState === "playing") {
          log(`TransitionManager: State now compatible (${currentStore.currentState}), triggering transition to '${nextStepId}'`);
          this.triggerTransition(nextStepId);
        } else if (retries < maxRetries) {
          log(`TransitionManager: State still incompatible (${currentStore.currentState}), will retry`);
          setTimeout(retryTransition, TIMING.RETRY_DELAY_MS);
        }
      };
      setTimeout(retryTransition, TIMING.RETRY_DELAY_MS);
    }
    const transitionId = `url-path-${Date.now()}`;
    const intervalId = window.setInterval(() => {
      const match = this.isURLPathMatch(pathPattern);
      if (match) {
        clearInterval(intervalId);
        StorageManager.getInstance().clearPendingNavigation();
        this.triggerTransition(nextStepId);
      }
    }, TIMING.URL_PATH_CHECK_INTERVAL_MS);
    this.activeTransitions.set(transitionId, {
      handlers: /* @__PURE__ */ new Map(),
      // No DOM handlers for URL path transitions
      cleanup: () => {
        clearInterval(intervalId);
      },
      data: {
        type: "url-path",
        pattern: pathPattern,
        nextStepId
      }
    });
  }
  /**
   * Saves pending navigation data for cross-page URL transitions
   * This enables resuming from the correct step after a hard page refresh
   * @param urlPattern - The URL pattern to match
   * @param nextStepId - The step ID to navigate to
   */
  savePendingNavigation(urlPattern, nextStepId) {
    const store = getSaltfishStore();
    if (!store.manifest) {
      return;
    }
    const storageManager2 = StorageManager.getInstance();
    storageManager2.setPendingNavigation({
      playlistId: store.manifest.id,
      nextStepId,
      urlPattern,
      timestamp: Date.now()
    });
  }
  /**
   * Sets up beforeunload handler as backup for cross-page URL transitions
   * This ensures pending navigation is saved even if the page unloads unexpectedly
   * @param urlPattern - The URL pattern to match
   * @param nextStepId - The step ID to navigate to
   */
  setupBeforeUnloadHandler(urlPattern, nextStepId) {
    this.removeBeforeUnloadHandler();
    this.beforeUnloadHandler = () => {
      this.savePendingNavigation(urlPattern, nextStepId);
    };
    window.addEventListener("beforeunload", this.beforeUnloadHandler);
  }
  /**
   * Removes the beforeunload handler if it exists
   */
  removeBeforeUnloadHandler() {
    if (this.beforeUnloadHandler) {
      window.removeEventListener("beforeunload", this.beforeUnloadHandler);
      this.beforeUnloadHandler = null;
    }
  }
  /**
   * Checks if the current URL path matches a pattern
   */
  isURLPathMatch(pattern) {
    if (!pattern) {
      return false;
    }
    const currentUrl = stripShareIdFromUrl(window.location.href);
    const currentPath = window.location.pathname;
    const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const regexPattern = escapedPattern.replace(/\\\*/g, ".*");
    const regex = new RegExp(regexPattern);
    const fullUrlMatch = regex.test(currentUrl);
    const pathMatch = regex.test(currentPath);
    return fullUrlMatch || pathMatch;
  }
  /**
   * Handles URL requirement validation failure by exiting and closing the playlist
   * @param stepId - The step ID that failed validation
   * @param urlRequirement - The URL requirement that was not met
   */
  handleUrlRequirementFailure(stepId, urlRequirement) {
    log(`TransitionManager: Expected pattern: '${urlRequirement.pattern}' (matchType: ${urlRequirement.matchType})`);
    this.cleanupTransitions();
    const saltfishPlayer = window._saltfishPlayer;
    if (saltfishPlayer && typeof saltfishPlayer.destroy === "function") {
      saltfishPlayer.destroy();
    }
  }
  /**
   * Validates the URL requirement for the current step (immediate, no retries)
   * Used for external navigation detection - should close immediately if URL doesn't match
   * @returns boolean - true if validation passes or no requirement exists, false if validation fails
   */
  validateCurrentStepUrl() {
    const store = getSaltfishStore();
    const manifest = store.manifest;
    const currentStepId = store.currentStepId;
    if (!manifest || !currentStepId) {
      return true;
    }
    const currentStep = manifest.steps.find((step) => step.id === currentStepId);
    if (!currentStep || !currentStep.urlRequirement) {
      return true;
    }
    const isValid = validateUrlRequirement(currentStep.urlRequirement);
    if (!isValid) {
      this.handleUrlRequirementFailure(currentStepId, currentStep.urlRequirement);
    }
    return isValid;
  }
  /**
   * Triggers a transition to a new step
   * @param nextStepId - The ID of the step to transition to
   */
  triggerTransition(nextStepId) {
    var _a;
    const store = getSaltfishStore();
    const currentState = store.currentState;
    store.currentStepId;
    const isMinimized = store.isMinimized;
    if (currentState !== "playing" && currentState !== "waitingForInteraction") {
      return;
    }
    if (isMinimized) {
      return;
    }
    this.cleanupTransitions();
    if (store.goToStep) {
      store.goToStep(nextStepId);
    } else {
      (_a = store.goToStep) == null ? void 0 : _a.call(store, nextStepId);
    }
  }
  /**
   * Triggers a transition with progress coordination to ensure state is saved before page navigation
   * @param nextStepId - The ID of the step to transition to
   * @returns Promise<boolean> - True if transition was successful, false otherwise
   */
  async triggerTransitionWithProgress(nextStepId) {
    const store = getSaltfishStore();
    const currentState = store.currentState;
    store.currentStepId;
    const isMinimized = store.isMinimized;
    if (currentState !== "playing" && currentState !== "waitingForInteraction") {
      return false;
    }
    if (isMinimized) {
      return false;
    }
    try {
      log(`TransitionManager: Conditions met, transitioning to step '${nextStepId}'`);
      this.triggerTransition(nextStepId);
      await new Promise((resolve) => setTimeout(resolve, TIMING.STATE_PROCESSING_DELAY_MS));
      log(`TransitionManager: Transition to '${nextStepId}' completed successfully`);
      return true;
    } catch (error2) {
      return false;
    }
  }
  /**
   * Cleans up all active transitions
   */
  cleanupTransitions() {
    this.activeTransitions.forEach((transition) => {
      transition.cleanup();
    });
    this.activeTransitions.clear();
    this.waitingForInteraction = false;
    this.removeBeforeUnloadHandler();
  }
  /**
   * Sets the waiting for interaction state
   * @param isWaiting - Whether the player is waiting for interaction
   */
  setWaitingForInteraction(isWaiting) {
    this.waitingForInteraction = isWaiting;
  }
  /**
   * Checks if the player is waiting for interaction
   * @returns Whether the player is waiting for interaction
   */
  isWaitingForInteraction() {
    return this.waitingForInteraction;
  }
  /**
   * Resets the transition manager to initial state for reuse
   */
  reset() {
    this.cleanupTransitions();
  }
  /**
   * Marks the start of StateMachineActionHandler validation to prevent race conditions
   * TransitionManager will skip validation while this flag is set
   */
  startStateMachineValidation() {
    this.isStateMachineValidating = true;
  }
  /**
   * Marks the end of StateMachineActionHandler validation
   */
  endStateMachineValidation() {
    this.isStateMachineValidating = false;
  }
  /**
   * Destroys the transition manager and cleans up resources
   */
  destroy() {
    this.cleanupTransitions();
    window.removeEventListener("popstate", this.handleURLChange);
  }
  /**
   * Sets up DOM element visible transitions
   * @param transition - The transition configuration
   */
  setupDOMElementVisibleTransition(transition) {
    if (!transition.target) {
      return;
    }
    const selector = transition.target;
    const nextStepId = transition.nextStep;
    const transitionId = `dom-visible-${selector}-${Date.now()}`;
    let intersectionObserver = null;
    let mutationObserver = null;
    let targetElement = null;
    let periodicCheck = null;
    const cleanup = () => {
      if (intersectionObserver) {
        intersectionObserver.disconnect();
        intersectionObserver = null;
      }
      if (mutationObserver) {
        mutationObserver.disconnect();
        mutationObserver = null;
      }
      if (periodicCheck) {
        clearInterval(periodicCheck);
        periodicCheck = null;
      }
      targetElement = null;
    };
    const setupIntersectionObserver = (element) => {
      if (intersectionObserver) {
        return;
      }
      targetElement = element;
      intersectionObserver = new IntersectionObserver(
        (entries) => {
          entries.forEach((entry) => {
            var _a;
            log(`TransitionManager: IntersectionObserver callback for '${selector}': isIntersecting=${entry.isIntersecting}, ratio=${entry.intersectionRatio.toFixed(2)}, target=${entry.target.outerHTML.substring(0, 100)}...`);
            if (entry.isIntersecting && entry.target === targetElement) {
              const styles = window.getComputedStyle(entry.target);
              const isReallyVisible = styles.opacity !== "0" && styles.display !== "none" && styles.visibility === "visible" && styles.pointerEvents !== "none";
              if (!isReallyVisible) {
                return;
              }
              this.triggerTransition(nextStepId);
              (_a = this.activeTransitions.get(transitionId)) == null ? void 0 : _a.cleanup();
              this.activeTransitions.delete(transitionId);
            } else if (entry.target === targetElement) {
              const rect = entry.target.getBoundingClientRect();
              log(`TransitionManager: Element '${selector}' reported as NOT intersecting. BoundingClientRect: top=${rect.top.toFixed(0)}, left=${rect.left.toFixed(0)}, bottom=${rect.bottom.toFixed(0)}, right=${rect.right.toFixed(0)}, width=${rect.width.toFixed(0)}, height=${rect.height.toFixed(0)}`);
            }
          });
        },
        {
          root: null,
          // Explicitly use viewport as root
          rootMargin: "0px",
          // Ensure no margins are affecting detection
          threshold: 0
          // Trigger if even 1px is visible
        }
      );
      intersectionObserver.observe(element);
      if (mutationObserver) {
        mutationObserver.disconnect();
        mutationObserver = null;
      }
    };
    const initialElement = transition.expectedElement || transition.expectedSize ? findValidElement(selector, transition.expectedElement, transition.expectedSize) : document.querySelector(selector);
    if (initialElement) {
      setupIntersectionObserver(initialElement);
    } else {
      mutationObserver = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
          if (mutation.type === "childList") {
            mutation.addedNodes.forEach((node) => {
              if (node.nodeType === Node.ELEMENT_NODE) {
                const elementNode = node;
                if (elementNode.matches(selector)) {
                  if (!transition.expectedElement && !transition.expectedSize || isElementValid(elementNode, transition.expectedElement, transition.expectedSize)) {
                    setupIntersectionObserver(elementNode);
                    return;
                  }
                }
                const matchingDescendant = transition.expectedElement || transition.expectedSize ? findValidElement(selector, transition.expectedElement, transition.expectedSize) : elementNode.querySelector(selector);
                if (matchingDescendant) {
                  setupIntersectionObserver(matchingDescendant);
                  return;
                }
              }
            });
          }
          if (!mutationObserver) {
            break;
          }
        }
      });
      mutationObserver.observe(document.body, { childList: true, subtree: true });
      periodicCheck = window.setInterval(() => {
        if (!mutationObserver) {
          if (periodicCheck) {
            clearInterval(periodicCheck);
          }
          return;
        }
        const element = transition.expectedElement || transition.expectedSize ? findValidElement(selector, transition.expectedElement, transition.expectedSize) : document.querySelector(selector);
        if (element && element.offsetWidth > 0 && element.offsetHeight > 0) {
          if (periodicCheck) {
            clearInterval(periodicCheck);
          }
          periodicCheck = null;
          setupIntersectionObserver(element);
        }
      }, 1e3);
    }
    this.activeTransitions.set(transitionId, {
      handlers: /* @__PURE__ */ new Map(),
      // No direct event handlers needed here
      cleanup,
      data: {
        type: "dom-element-visible",
        pattern: selector,
        nextStepId
      }
    });
  }
  /**
   * Re-dispatches a click event to allow original page behavior after our transition logic completes
   * @param originalEvent - The original click event that was intercepted
   */
  reDispatchClick(originalEvent) {
    const target = originalEvent.target;
    if (!target || !(target instanceof HTMLElement)) {
      return;
    }
    try {
      setTimeout(() => {
        log(`TransitionManager: Re-dispatching click event on ${target.tagName}${target.id ? "#" + target.id : ""}${target.className ? "." + target.className.split(" ").join(".") : ""}`);
        const newEvent = new MouseEvent("click", {
          bubbles: true,
          cancelable: true,
          view: window,
          button: originalEvent.button || 0,
          buttons: originalEvent.buttons || 1,
          clientX: originalEvent.clientX || 0,
          clientY: originalEvent.clientY || 0,
          ctrlKey: originalEvent.ctrlKey || false,
          shiftKey: originalEvent.shiftKey || false,
          altKey: originalEvent.altKey || false,
          metaKey: originalEvent.metaKey || false
        });
        target.dispatchEvent(newEvent);
        log("TransitionManager: Click event re-dispatched successfully");
      }, 10);
    } catch (error2) {
    }
  }
  /**
   * Determines if clicking an element will likely cause a hard page refresh
   * @param element - The target element that was clicked
   * @returns true if the click will likely cause a hard refresh, false otherwise
   */
  willCauseHardRefresh(element) {
    try {
      if (element.tagName === "A" && element.hasAttribute("href")) {
        const href = element.getAttribute("href");
        if (!href) return false;
        const link = new URL(href, window.location.href);
        const current2 = new URL(window.location.href);
        if (link.origin !== current2.origin) {
          log(`TransitionManager: Detected cross-origin link: ${link.origin} vs ${current2.origin}`);
          return true;
        }
        const target = element.getAttribute("target");
        if (target && target !== "_self") {
          log(`TransitionManager: Detected link with target: ${target}`);
          return true;
        }
        if (link.protocol === "mailto:" || link.protocol === "tel:" || link.protocol === "sms:" || link.protocol === "javascript:") {
          log(`TransitionManager: Detected special protocol: ${link.protocol}`);
          return true;
        }
        log(`TransitionManager: Same-origin link detected, assuming SPA navigation`);
        return false;
      }
      const form = element.closest("form");
      if (form && form.hasAttribute("action")) {
        const action = form.getAttribute("action");
        if (action && action !== "#" && action !== "") {
          log(`TransitionManager: Form with action detected, but assuming modern handling`);
          return false;
        }
      }
      log(`TransitionManager: Normal element interaction detected: ${element.tagName}`);
      return false;
    } catch (error2) {
      return false;
    }
  }
}
class TriggerManager {
  // Track which elements are currently visible
  constructor() {
    __publicField(this, "triggeredPlaylists", []);
    __publicField(this, "triggeredPlaylistsSet", /* @__PURE__ */ new Set());
    // Track which playlists have been triggered this session
    __publicField(this, "isMonitoring", false);
    __publicField(this, "elementClickedListeners", /* @__PURE__ */ new Map());
    __publicField(this, "clickedElements", /* @__PURE__ */ new Set());
    // Track which elements have been clicked
    __publicField(this, "elementVisibleObservers", /* @__PURE__ */ new Map());
    __publicField(this, "visibleElements", /* @__PURE__ */ new Set());
  }
  /**
   * Registers playlists with triggers and their trigger configurations
   * @param playlists - List of all playlists from backend
   */
  registerTriggers(playlists) {
    this.triggeredPlaylists = playlists.filter((playlist) => {
      const hasTriggers = playlist.hasTriggers ?? playlist.autoStart ?? false;
      return hasTriggers && playlist.triggers;
    });
    log(`TriggerManager: Registered ${this.triggeredPlaylists.length} playlists with triggers`);
    this.triggeredPlaylists.forEach((playlist) => {
      var _a, _b, _c, _d;
      log(`TriggerManager: Registered trigger for playlist ${playlist.id} - URL: ${(_a = playlist.triggers) == null ? void 0 : _a.url}, ElementClick: ${(_b = playlist.triggers) == null ? void 0 : _b.elementClicked}, ElementVisible: ${(_c = playlist.triggers) == null ? void 0 : _c.elementVisible}, MaxVisits: ${(_d = playlist.triggers) == null ? void 0 : _d.maxVisits}`);
    });
    this.setupElementClickListeners();
    this.setupElementVisibleObservers();
  }
  /**
   * Starts monitoring for trigger conditions
   */
  startMonitoring() {
    if (this.isMonitoring) {
      return;
    }
    this.isMonitoring = true;
    this.evaluateAllTriggers();
  }
  /**
   * Stops monitoring for trigger conditions
   */
  stopMonitoring() {
    this.isMonitoring = false;
  }
  /**
   * Evaluates all registered triggers against current conditions
   * Called by TransitionManager when URL changes occur
   * Stops after the first playlist is triggered to prevent multiple playlists from conflicting
   */
  evaluateAllTriggers() {
    if (!this.isMonitoring || this.triggeredPlaylists.length === 0) {
      return;
    }
    for (const playlist of this.triggeredPlaylists) {
      const wasTriggered = this.evaluatePlaylistTrigger(playlist);
      if (wasTriggered) {
        log(`TriggerManager: Playlist ${playlist.id} was triggered, stopping further evaluations`);
        break;
      }
    }
  }
  /**
   * Evaluates triggers for a specific playlist
   * @param playlist - The playlist to evaluate triggers for
   * @returns true if the playlist was triggered, false otherwise
   */
  evaluatePlaylistTrigger(playlist) {
    if (!playlist.triggers) {
      return false;
    }
    const { triggers } = playlist;
    const playlistId = playlist.id;
    if (this.triggeredPlaylistsSet.has(playlistId)) {
      return false;
    }
    const store = getSaltfishStore();
    if (!store.user) {
      return false;
    }
    const conditions = [];
    const watchedPlaylists = this.getWatchedPlaylists();
    const maxVisitsCondition = this.evaluateMaxVisitsCondition(triggers.maxVisits, playlistId, watchedPlaylists);
    conditions.push(maxVisitsCondition);
    const urlCondition = this.evaluateURLCondition(triggers);
    conditions.push(urlCondition);
    log(`TriggerManager: URL condition for playlist ${playlistId}: ${urlCondition} (pattern: ${triggers.url})`);
    const playlistSeenCondition = this.evaluatePlaylistSeenCondition(triggers.playlistSeen, watchedPlaylists);
    conditions.push(playlistSeenCondition);
    log(`TriggerManager: PlaylistSeen condition for playlist ${playlistId}: ${playlistSeenCondition} (required: ${JSON.stringify(triggers.playlistSeen)})`);
    const playlistNotSeenCondition = this.evaluatePlaylistNotSeenCondition(triggers.playlistNotSeen, watchedPlaylists);
    conditions.push(playlistNotSeenCondition);
    log(`TriggerManager: PlaylistNotSeen condition for playlist ${playlistId}: ${playlistNotSeenCondition} (forbidden: ${JSON.stringify(triggers.playlistNotSeen)})`);
    const elementClickedCondition = this.evaluateElementClickCondition(triggers.elementClicked);
    conditions.push(elementClickedCondition);
    log(`TriggerManager: ElementClick condition for playlist ${playlistId}: ${elementClickedCondition} (selector: ${triggers.elementClicked})`);
    const elementVisibleCondition = this.evaluateElementVisibleCondition(triggers.elementVisible);
    conditions.push(elementVisibleCondition);
    log(`TriggerManager: ElementVisible condition for playlist ${playlistId}: ${elementVisibleCondition} (selector: ${triggers.elementVisible})`);
    const userAttributesCondition = this.evaluateUserAttributesCondition(triggers.userAttributes);
    conditions.push(userAttributesCondition);
    log(`TriggerManager: UserAttributes condition for playlist ${playlistId}: ${userAttributesCondition} (conditions: ${JSON.stringify(triggers.userAttributes)})`);
    const shouldTrigger = this.applyOperators(conditions, triggers.operators);
    log(`TriggerManager: Final evaluation for playlist ${playlistId}: ${shouldTrigger} (operator: ${triggers.operators.join(", ")})`);
    if (shouldTrigger) {
      this.triggerPlaylist(playlistId);
      return true;
    }
    return false;
  }
  /**
   * Gets watched playlists from backend userData or localStorage (for anonymous users)
   * @returns WatchedPlaylists object
   */
  getWatchedPlaylists() {
    var _a, _b;
    const store = getSaltfishStore();
    if (((_a = store.userData) == null ? void 0 : _a.watchedPlaylists) && Object.keys(store.userData.watchedPlaylists).length > 0) {
      return store.userData.watchedPlaylists;
    }
    const storageManager2 = StorageManager.getInstance();
    const progressData = storageManager2.getProgress((_b = store.user) == null ? void 0 : _b.id);
    if (!progressData) {
      return {};
    }
    const watchedPlaylists = {};
    for (const playlistId of Object.keys(progressData)) {
      watchedPlaylists[playlistId] = {
        status: "in_progress",
        timestamp: Date.now()
      };
    }
    log(`TriggerManager: Built watchedPlaylists from localStorage: ${JSON.stringify(Object.keys(watchedPlaylists))}`);
    return watchedPlaylists;
  }
  /**
   * Evaluates the 'maxVisits' condition for a playlist
   * @param maxVisits - Maximum number of times user can see this playlist (null = unlimited)
   * @param playlistId - The playlist ID to check
   * @param watchedPlaylists - User's watched playlists data
   */
  evaluateMaxVisitsCondition(maxVisits, playlistId, watchedPlaylists) {
    if (maxVisits === null) {
      return true;
    }
    const playlistData = watchedPlaylists && watchedPlaylists[playlistId];
    const visitCount = (playlistData == null ? void 0 : playlistData.visitCount) ?? ((playlistData == null ? void 0 : playlistData.status) === "completed" || (playlistData == null ? void 0 : playlistData.status) === "dismissed" ? 1 : 0);
    return visitCount < maxVisits;
  }
  /**
   * Normalizes a URL by removing trailing slash (unless it's the root path)
   * @param url - The URL to normalize
   */
  normalizeUrl(url) {
    if (url.endsWith("/") && url.lastIndexOf("/") > url.indexOf("://") + 2) {
      return url.slice(0, -1);
    }
    return url;
  }
  /**
   * Evaluates the URL condition for a playlist
   * @param triggers - The trigger configuration containing URL pattern and match type
   */
  evaluateURLCondition(triggers) {
    let pattern = triggers.url;
    if (!pattern) {
      return true;
    }
    const currentUrl = this.normalizeUrl(stripShareIdFromUrl(window.location.href.split("#")[0]));
    const currentPath = window.location.pathname;
    if (triggers.urlMatchType === "regex") {
      try {
        const regex2 = new RegExp(pattern);
        const fullUrlMatch2 = regex2.test(currentUrl);
        const pathMatch2 = regex2.test(currentPath);
        log(`TriggerManager: URL match check - pattern: '${pattern}', matchType: 'regex', currentUrl: '${currentUrl}', currentPath: '${currentPath}', match: ${fullUrlMatch2 || pathMatch2}`);
        return fullUrlMatch2 || pathMatch2;
      } catch (error2) {
        return false;
      }
    }
    if (triggers.urlMatchType === "contains" && !pattern.includes("*")) {
      pattern = "*" + pattern + "*";
    }
    const hasWildcards = pattern.includes("*");
    if (!hasWildcards && (!triggers.urlMatchType || triggers.urlMatchType === "exact" || triggers.urlMatchType === "contains")) {
      pattern = this.normalizeUrl(pattern);
    }
    const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    let regexPattern = escapedPattern.replace(/\\\*/g, ".*");
    if (!hasWildcards) {
      regexPattern = "^" + regexPattern + "$";
    }
    const regex = new RegExp(regexPattern);
    const fullUrlMatch = regex.test(currentUrl);
    const pathMatch = regex.test(currentPath);
    log(`TriggerManager: URL match check - pattern: '${triggers.url}', matchType: '${triggers.urlMatchType || "exact"}', regex: '${regexPattern}', currentUrl: '${currentUrl}', currentPath: '${currentPath}', match: ${fullUrlMatch || pathMatch}`);
    return fullUrlMatch || pathMatch;
  }
  /**
   * Evaluates the playlistSeen condition
   * User must have seen ALL specified playlists
   * @param requiredPlaylists - Array of playlist IDs that user must have seen
   * @param watchedPlaylists - User's watched playlists data
   */
  evaluatePlaylistSeenCondition(requiredPlaylists, watchedPlaylists) {
    if (!requiredPlaylists || requiredPlaylists.length === 0) {
      return true;
    }
    if (!watchedPlaylists) {
      return false;
    }
    for (const playlistId of requiredPlaylists) {
      const playlistData = watchedPlaylists[playlistId];
      if (!playlistData || playlistData.status !== "completed" && playlistData.status !== "in_progress") {
        return false;
      }
    }
    return true;
  }
  /**
   * Evaluates the playlistNotSeen condition
   * User must NOT have seen ANY of the specified playlists
   * @param forbiddenPlaylists - Array of playlist IDs that user must not have seen
   * @param watchedPlaylists - User's watched playlists data
   */
  evaluatePlaylistNotSeenCondition(forbiddenPlaylists, watchedPlaylists) {
    if (!forbiddenPlaylists || forbiddenPlaylists.length === 0) {
      return true;
    }
    if (!watchedPlaylists) {
      return true;
    }
    for (const playlistId of forbiddenPlaylists) {
      const playlistData = watchedPlaylists[playlistId];
      if (playlistData && (playlistData.status === "completed" || playlistData.status === "in_progress")) {
        return false;
      }
    }
    return true;
  }
  /**
   * Evaluates the elementClicked condition for a playlist
   * @param selector - CSS selector for element that should trigger the playlist (null = no element condition)
   */
  evaluateElementClickCondition(selector) {
    if (!selector) {
      return true;
    }
    const wasClicked = this.clickedElements.has(selector);
    return wasClicked;
  }
  /**
   * Evaluates the elementVisible condition for a playlist
   * @param selector - CSS selector for element that should trigger the playlist when visible (null = no element condition)
   */
  evaluateElementVisibleCondition(selector) {
    if (!selector) {
      return true;
    }
    const isVisible = this.visibleElements.has(selector);
    return isVisible;
  }
  /**
   * Evaluates user attribute conditions for a playlist
   * All conditions must be met (AND logic)
   * @param conditions - Array of user attribute conditions to evaluate
   */
  evaluateUserAttributesCondition(conditions) {
    if (!conditions || conditions.length === 0) {
      return true;
    }
    const store = getSaltfishStore();
    const user = store.user;
    if (!user) {
      return false;
    }
    for (const condition of conditions) {
      const { attributeKey, attributeType, operator, value: expectedValue } = condition;
      const userValue = user[attributeKey];
      if (userValue === void 0 || userValue === null) {
        return false;
      }
      const result = this.compareValues(userValue, expectedValue, attributeType, operator);
      if (!result) {
        return false;
      }
    }
    return true;
  }
  /**
   * Compares two values based on attribute type and operator
   * @param userValue - The user's actual value
   * @param expectedValue - The expected value from trigger condition (always a string)
   * @param attributeType - The data type for comparison
   * @param operator - The comparison operator
   */
  compareValues(userValue, expectedValue, attributeType, operator) {
    try {
      switch (attributeType) {
        case "string": {
          const userStr = String(userValue);
          const expectedStr = expectedValue;
          return this.applyOperator(userStr, expectedStr, operator);
        }
        case "boolean": {
          const userBool = typeof userValue === "boolean" ? userValue : String(userValue).toLowerCase() === "true";
          const expectedBool = expectedValue.toLowerCase() === "true";
          if (operator === "equals") return userBool === expectedBool;
          if (operator === "notEquals") return userBool !== expectedBool;
          return false;
        }
        case "int": {
          const userNum = typeof userValue === "number" ? userValue : parseFloat(String(userValue));
          const expectedNum = parseFloat(expectedValue);
          if (isNaN(userNum) || isNaN(expectedNum)) {
            log(`TriggerManager: Invalid number comparison - userValue: ${userValue}, expected: ${expectedValue}`);
            return false;
          }
          return this.applyOperator(userNum, expectedNum, operator);
        }
        case "date": {
          const userDate = userValue instanceof Date ? userValue : new Date(String(userValue));
          const expectedDate = new Date(expectedValue);
          if (isNaN(userDate.getTime()) || isNaN(expectedDate.getTime())) {
            log(`TriggerManager: Invalid date comparison - userValue: ${userValue}, expected: ${expectedValue}`);
            return false;
          }
          return this.applyOperator(userDate.getTime(), expectedDate.getTime(), operator);
        }
        default:
          log(`TriggerManager: Unknown attribute type: ${attributeType}`);
          return false;
      }
    } catch (error2) {
      return false;
    }
  }
  /**
   * Applies the comparison operator to two values
   */
  applyOperator(userValue, expectedValue, operator) {
    switch (operator) {
      case "equals":
        return userValue === expectedValue;
      case "notEquals":
        return userValue !== expectedValue;
      case "greaterThan":
        return userValue > expectedValue;
      case "lessThan":
        return userValue < expectedValue;
      default:
        return false;
    }
  }
  /**
   * Sets up click event listeners for all playlists with elementClicked triggers
   */
  setupElementClickListeners() {
    this.clearElementClickListeners();
    this.triggeredPlaylists.forEach((playlist) => {
      var _a;
      if ((_a = playlist.triggers) == null ? void 0 : _a.elementClicked) {
        this.setupElementClickListener(
          playlist.id,
          playlist.triggers.elementClicked,
          playlist.triggers.elementClickedExpectedElement,
          playlist.triggers.elementClickedExpectedSize
        );
      }
    });
  }
  /**
   * Sets up a click event listener for a specific playlist and selector
   * @param playlistId - The playlist ID
   * @param selector - CSS selector for the target element
   * @param expectedElement - Optional expected tag+text for validation
   * @param expectedSize - Optional expected size for validation
   */
  setupElementClickListener(playlistId, selector, expectedElement, expectedSize) {
    try {
      const element = expectedElement || expectedSize ? findValidElement(selector, expectedElement, expectedSize) : document.querySelector(selector);
      if (!element) {
        log(`TriggerManager: Element not found for selector '${selector}' (playlist: ${playlistId})`);
        return;
      }
      const playlist = this.triggeredPlaylists.find((p) => p.id === playlistId);
      if (!playlist) {
        log(`TriggerManager: Playlist not found for ID '${playlistId}'`);
        return;
      }
      const listener = (event) => {
        log(`TriggerManager: Element click detected for selector '${selector}' (playlist: ${playlistId})`);
        event.preventDefault();
        event.stopPropagation();
        this.clickedElements.add(selector);
        this.evaluateAllTriggers();
      };
      element.addEventListener("click", listener);
      const listenerId = `${playlistId}-${selector}`;
      this.elementClickedListeners.set(listenerId, {
        element,
        listener,
        selector
      });
      log(`TriggerManager: Set up click listener for playlist ${playlistId} on selector '${selector}'`);
    } catch (error2) {
    }
  }
  /**
   * Clears all element click event listeners
   */
  clearElementClickListeners() {
    this.elementClickedListeners.forEach(({ element, listener }) => {
      try {
        element.removeEventListener("click", listener);
      } catch (error2) {
        console.error("TriggerManager: Error removing element click listener:", error2);
      }
    });
    this.elementClickedListeners.clear();
  }
  /**
   * Sets up visibility observers for all playlists with elementVisible triggers
   */
  setupElementVisibleObservers() {
    this.clearElementVisibleObservers();
    this.triggeredPlaylists.forEach((playlist) => {
      var _a;
      if ((_a = playlist.triggers) == null ? void 0 : _a.elementVisible) {
        this.setupElementVisibleObserver(
          playlist.id,
          playlist.triggers.elementVisible,
          playlist.triggers.elementVisibleExpectedElement,
          playlist.triggers.elementVisibleExpectedSize
        );
      }
    });
  }
  /**
   * Sets up a visibility observer for a specific playlist and selector
   * @param playlistId - The playlist ID
   * @param selector - CSS selector for the target element
   * @param expectedElement - Optional expected tag+text for validation
   * @param expectedSize - Optional expected size for validation
   */
  setupElementVisibleObserver(playlistId, selector, expectedElement, expectedSize) {
    try {
      const observerId = `${playlistId}-${selector}`;
      if (this.elementVisibleObservers.has(observerId)) {
        log(`TriggerManager: Observer already exists for selector '${selector}' (playlist: ${playlistId})`);
        return;
      }
      const setupIntersectionObserver = (element) => {
        const intersectionObserver = new IntersectionObserver(
          (entries) => {
            entries.forEach((entry) => {
              log(`TriggerManager: IntersectionObserver callback for '${selector}': isIntersecting=${entry.isIntersecting}, ratio=${entry.intersectionRatio.toFixed(2)}`);
              if (entry.isIntersecting) {
                const styles = window.getComputedStyle(entry.target);
                const isReallyVisible = styles.opacity !== "0" && styles.display !== "none" && styles.visibility === "visible" && styles.pointerEvents !== "none";
                if (!isReallyVisible) {
                  log(`TriggerManager: Element matching '${selector}' is intersecting but visually hidden`);
                  return;
                }
                log(`TriggerManager: Element visibility detected for selector '${selector}' (playlist: ${playlistId})`);
                this.visibleElements.add(selector);
                this.evaluateAllTriggers();
              } else {
                if (this.visibleElements.has(selector)) {
                  log(`TriggerManager: Element no longer visible for selector '${selector}' (playlist: ${playlistId})`);
                  this.visibleElements.delete(selector);
                }
              }
            });
          },
          {
            root: null,
            // Use viewport as root
            rootMargin: "0px",
            threshold: 0
            // Trigger if even 1px is visible
          }
        );
        intersectionObserver.observe(element);
        log(`TriggerManager: IntersectionObserver watching element matching '${selector}'`);
        const existingEntry = this.elementVisibleObservers.get(observerId);
        if (existingEntry) {
          if (existingEntry.mutationObserver) {
            existingEntry.mutationObserver.disconnect();
          }
          this.elementVisibleObservers.set(observerId, {
            observer: intersectionObserver,
            mutationObserver: null,
            selector
          });
        } else {
          this.elementVisibleObservers.set(observerId, {
            observer: intersectionObserver,
            mutationObserver: null,
            selector
          });
        }
      };
      const initialElement = expectedElement || expectedSize ? findValidElement(selector, expectedElement, expectedSize) : document.querySelector(selector);
      if (initialElement) {
        log(`TriggerManager: Found element matching '${selector}' immediately`);
        setupIntersectionObserver(initialElement);
      } else {
        log(`TriggerManager: Element not found for selector '${selector}', setting up MutationObserver`);
        const mutationObserver = new MutationObserver((mutationsList) => {
          for (const mutation of mutationsList) {
            if (mutation.type === "childList") {
              mutation.addedNodes.forEach((node) => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                  const elementNode = node;
                  if (elementNode.matches(selector)) {
                    if (!expectedElement && !expectedSize || isElementValid(elementNode, expectedElement, expectedSize)) {
                      log(`TriggerManager: Added node matches '${selector}'`);
                      setupIntersectionObserver(elementNode);
                    }
                  } else {
                    const matchingDescendant = expectedElement || expectedSize ? findValidElement(selector, expectedElement, expectedSize) : elementNode.querySelector(selector);
                    if (matchingDescendant) {
                      log(`TriggerManager: Found descendant matching '${selector}'`);
                      setupIntersectionObserver(matchingDescendant);
                    }
                  }
                }
              });
            }
          }
        });
        mutationObserver.observe(document.body, { childList: true, subtree: true });
        log("TriggerManager: MutationObserver watching document body for element additions");
        this.elementVisibleObservers.set(observerId, {
          observer: null,
          // Will be replaced when element is found
          mutationObserver,
          selector
        });
      }
      log(`TriggerManager: Set up visibility observer for playlist ${playlistId} on selector '${selector}'`);
    } catch (error2) {
    }
  }
  /**
   * Clears all element visibility observers
   */
  clearElementVisibleObservers() {
    this.elementVisibleObservers.forEach(({ observer, mutationObserver }) => {
      try {
        if (observer) {
          observer.disconnect();
        }
        if (mutationObserver) {
          mutationObserver.disconnect();
        }
      } catch (error2) {
        console.error("TriggerManager: Error removing element visibility observer:", error2);
      }
    });
    this.elementVisibleObservers.clear();
  }
  /**
   * Applies logical operators to combine multiple conditions
   * @param conditions - Array of boolean conditions to combine
   * @param operators - Array of operators ("AND" or "OR")
   */
  applyOperators(conditions, operators) {
    if (conditions.length === 0) {
      return false;
    }
    if (conditions.length === 1) {
      return conditions[0];
    }
    if (!operators || operators.length === 0) {
      return conditions.every((condition) => condition);
    }
    if (operators.includes("OR")) {
      return conditions.some((condition) => condition);
    }
    return conditions.every((condition) => condition);
  }
  /**
   * Triggers a playlist to start
   * @param playlistId - ID of the playlist to trigger
   */
  async triggerPlaylist(playlistId) {
    this.triggeredPlaylistsSet.add(playlistId);
    try {
      const saltfishPlayer = window._saltfishPlayer;
      if (saltfishPlayer && typeof saltfishPlayer.startPlaylist === "function") {
        await saltfishPlayer.startPlaylist(playlistId, { _triggeredByTriggerManager: true });
        log(`TriggerManager: Successfully triggered playlist ${playlistId}`);
      } else {
        log("TriggerManager: Error - SaltfishPlayer instance not found or startPlaylist method not available");
      }
    } catch (error2) {
      this.triggeredPlaylistsSet.delete(playlistId);
    }
  }
  /**
   * Resets the triggered playlists tracking
   * Useful for testing or when user context changes
   */
  resetTriggeredPlaylists() {
    this.triggeredPlaylistsSet.clear();
  }
  /**
   * Gets list of playlists that have been triggered this session
   */
  getTriggeredPlaylists() {
    return Array.from(this.triggeredPlaylistsSet);
  }
  /**
   * Marks a playlist as triggered to prevent re-triggering during URL changes
   * This should be called when a playlist is started programmatically
   * @param playlistId - The playlist ID to mark as triggered
   */
  markPlaylistAsTriggered(playlistId) {
    if (!this.triggeredPlaylistsSet.has(playlistId)) {
      this.triggeredPlaylistsSet.add(playlistId);
    }
  }
  /**
   * Cleanup method to be called on destroy
   */
  destroy() {
    this.stopMonitoring();
    this.clearElementClickListeners();
    this.clearElementVisibleObservers();
    this.triggeredPlaylists = [];
    this.triggeredPlaylistsSet.clear();
    this.clickedElements.clear();
    this.visibleElements.clear();
  }
}
class ABTestManager extends EventSubscriberManager {
  /**
   * Creates a new ABTestManager
   * @param eventManager - Optional event manager to subscribe to events
   */
  constructor(eventManager) {
    super(eventManager);
  }
  /**
   * Subscribe to relevant events for A/B testing
   */
  subscribeToEvents() {
    if (!this.eventManager) {
      return;
    }
    this.eventManager.on("playlistStarted", (event) => {
      this.trackTestParticipation(event.playlist.id);
    });
  }
  /**
   * Initialize A/B tests with configurations from backend
   * @param abTests - A/B test configurations from validate-token response
   */
  initializeTests(abTests) {
    const store = getSaltfishStore();
    store.setABTests(abTests);
  }
  /**
   * Assign a user to A/B tests based on their ID and test percentages
   * @param userId - User ID for consistent assignment
   * @param existingAssignments - Existing assignments from backend (for identified users)
   * @param userListAssignments - Assignments from backend for user list tests
   * @returns Record of test assignments
   */
  assignUserToTests(userId, existingAssignments, userListAssignments) {
    var _a, _b;
    const store = getSaltfishStore();
    const abTests = store.abTests || [];
    const assignments = { ...existingAssignments };
    for (const test of abTests) {
      const testType = test.testType || "percentage";
      if (testType !== "userList" && assignments[test.id]) {
        continue;
      }
      let assigned = false;
      if (testType === "userList") {
        const isAnonymous = ((_b = (_a = store.user) == null ? void 0 : _a.userData) == null ? void 0 : _b["__isAnonymous"]) === true;
        assigned = !isAnonymous && (userListAssignments == null ? void 0 : userListAssignments[test.id]) === true;
      } else {
        assigned = this.isUserInTest(userId, test);
      }
      assignments[test.id] = {
        testId: test.id,
        assigned,
        assignedAt: Date.now()
      };
      log(`[ABTestManager.assignUserToTests] User ${userId} ${assigned ? "assigned to" : "excluded from"} ${testType} test ${test.name}`);
    }
    store.setABTestAssignments(assignments);
    return assignments;
  }
  /**
   * Determine if a user should be included in a percentage-based test using consistent hashing
   * @param userId - User ID
   * @param test - A/B test configuration
   * @returns true if user should be in test
   */
  isUserInTest(userId, test) {
    if (!test.percentage) {
      return false;
    }
    const input = `${userId}_${test.id}`;
    let hash = 0;
    for (let i = 0; i < input.length; i++) {
      const char = input.charCodeAt(i);
      hash = (hash << 5) - hash + char;
      hash = hash & hash;
    }
    const percentage = Math.abs(hash) % 100;
    return percentage < test.percentage;
  }
  /**
   * Get playlists that the user should see based on A/B test assignments
   * @param allPlaylists - All available playlists
   * @returns Filtered playlists based on A/B test assignments
   */
  getFilteredPlaylists(allPlaylists) {
    const store = getSaltfishStore();
    const abTests = store.abTests || [];
    const assignments = store.abTestAssignments || {};
    if (abTests.length === 0) {
      return allPlaylists;
    }
    const excludedPlaylistIds = /* @__PURE__ */ new Set();
    for (const test of abTests) {
      const assignment = assignments[test.id];
      if (!assignment || !assignment.assigned) {
        excludedPlaylistIds.add(test.playlistId);
        log(`[ABTestManager.getFilteredPlaylists] Excluding playlist ${test.playlistId} from test ${test.name}`);
      }
    }
    const filteredPlaylists = allPlaylists.filter(
      (playlist) => !excludedPlaylistIds.has(playlist.id)
    );
    log(`[ABTestManager.getFilteredPlaylists] Filtered ${allPlaylists.length} playlists to ${filteredPlaylists.length}`);
    return filteredPlaylists;
  }
  /**
   * Track when a user participates in an A/B test (playlist starts)
   * @param playlistId - ID of the playlist that started
   */
  trackTestParticipation(playlistId) {
    var _a;
    const store = getSaltfishStore();
    const abTests = store.abTests || [];
    const assignments = store.abTestAssignments || {};
    const test = abTests.find((t) => t.playlistId === playlistId);
    if (!test) {
      return;
    }
    const assignment = assignments[test.id];
    if (!assignment || !assignment.assigned) {
      return;
    }
    log(`[ABTestManager.trackTestParticipation] User participated in ${test.testType || "percentage"} test: ${test.name}, playlist: ${playlistId}`);
    if (this.eventManager) {
      this.eventManager.trigger("abTestParticipation", {
        testId: test.id,
        testName: test.name,
        testType: test.testType || "percentage",
        // Include test type in analytics
        playlistId,
        userId: (_a = store.user) == null ? void 0 : _a.id,
        timestamp: Date.now()
      });
    }
  }
  /**
   * Get assignments for identified users to send to backend
   * @returns A/B test assignments that should be stored in backend
   */
  getAssignmentsForBackend() {
    const store = getSaltfishStore();
    return store.abTestAssignments || {};
  }
  /**
   * Check if a specific playlist is available for the current user
   * @param playlistId - Playlist ID to check
   * @returns true if playlist is available based on A/B test assignments
   */
  isPlaylistAvailable(playlistId) {
    const store = getSaltfishStore();
    const abTests = store.abTests || [];
    const assignments = store.abTestAssignments || {};
    const test = abTests.find((t) => t.playlistId === playlistId);
    if (!test) {
      return true;
    }
    const assignment = assignments[test.id];
    return assignment && assignment.assigned;
  }
  /**
   * Get active A/B test information for analytics
   * @returns Array of active test information
   */
  getActiveTestInfo() {
    const store = getSaltfishStore();
    const abTests = store.abTests || [];
    const assignments = store.abTestAssignments || {};
    return abTests.map((test) => {
      var _a;
      return {
        testId: test.id,
        testName: test.name,
        testType: test.testType || "percentage",
        // Default to percentage for backward compatibility
        assigned: ((_a = assignments[test.id]) == null ? void 0 : _a.assigned) || false
      };
    });
  }
}
class EventManager {
  constructor() {
    __publicField(this, "listeners", /* @__PURE__ */ new Map());
  }
  /**
   * Subscribes to an event
   * @param eventName - Name of the event to subscribe to
   * @param handler - Function to call when the event is triggered
   */
  on(eventName, handler) {
    if (!this.listeners.has(eventName)) {
      this.listeners.set(eventName, /* @__PURE__ */ new Set());
    }
    this.listeners.get(eventName).add(handler);
  }
  /**
   * Unsubscribes from an event
   * @param eventName - Name of the event to unsubscribe from
   * @param handler - Handler function to remove
   * @returns true if the handler was removed, false if it wasn't found
   */
  off(eventName, handler) {
    const handlers = this.listeners.get(eventName);
    if (!handlers) {
      return false;
    }
    return handlers.delete(handler);
  }
  /**
   * Triggers an event, calling all subscribed handlers
   * @param eventName - Name of the event to trigger
   * @param payload - Data to pass to the event handlers
   */
  trigger(eventName, payload) {
    const handlers = this.listeners.get(eventName);
    if (!handlers || handlers.size === 0) {
      return;
    }
    if (!("timestamp" in payload)) {
      payload.timestamp = Date.now();
    }
    handlers.forEach((handler) => {
      try {
        handler(payload);
      } catch (error2) {
        console.error(`Error in ${eventName} event handler:`, error2);
      }
    });
  }
  /**
   * Removes all event listeners
   */
  removeAllListeners() {
    this.listeners.clear();
  }
  /**
   * Gets the count of listeners for a specific event
   * @param eventName - Name of the event
   * @returns Number of listeners for the event
   */
  getListenerCount(eventName) {
    const handlers = this.listeners.get(eventName);
    return handlers ? handlers.size : 0;
  }
}
class PlaylistLoader {
  /**
   * Loads a playlist manifest from the given path
   * @param options - Load configuration including path and options
   * @returns Promise resolving to manifest and determined start step
   */
  async loadManifest(options) {
    const { manifestPath, playlistOptions, savedProgress } = options;
    try {
      const manifest = await this.fetchManifest(manifestPath);
      this.validateManifest(manifest);
      const startStepId = this.determineStartStep(manifest, playlistOptions, savedProgress);
      log(`PlaylistLoader: Successfully loaded manifest '${manifest.id}' with start step '${startStepId}'`);
      return {
        manifest,
        startStepId
      };
    } catch (loadError) {
      const errorMessage = loadError instanceof Error ? loadError.message : "Unknown error";
      error("[PlaylistLoader] Failed to load manifest:", loadError);
      throw new Error(`Unable to load playlist manifest: ${errorMessage}`);
    }
  }
  /**
   * Fetches manifest from URL
   * @param manifestPath - Path to manifest file
   * @returns Promise resolving to manifest object
   */
  async fetchManifest(manifestPath) {
    try {
      log("[PlaylistLoader] Fetching manifest from path:", manifestPath);
      const response = await fetch(manifestPath);
      log("[PlaylistLoader] Fetch response status:", {
        status: response.status,
        statusText: response.statusText
      });
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      log("[PlaylistLoader] Parsing manifest JSON...");
      const manifest = await response.json();
      log("[PlaylistLoader] Successfully parsed manifest JSON");
      return manifest;
    } catch (fetchError) {
      error("[PlaylistLoader] Fetch/parse failed:", fetchError);
      error("[PlaylistLoader] Manifest path:", manifestPath);
      throw new Error(
        `Failed to fetch manifest from "${manifestPath}": ${fetchError instanceof Error ? fetchError.message : "Unknown error"}`
      );
    }
  }
  /**
   * Validates that the manifest has required structure
   * @param manifest - Manifest object to validate
   * @throws Error if manifest is invalid
   */
  validateManifest(manifest) {
    if (!manifest) {
      throw new Error("Manifest is null or undefined");
    }
    if (typeof manifest !== "object") {
      throw new Error("Manifest must be an object");
    }
    const manifestObj = manifest;
    const requiredFields = ["id", "startStep", "steps"];
    for (const field of requiredFields) {
      if (!(field in manifestObj)) {
        throw new Error(`Manifest missing required field: ${field}`);
      }
    }
    if (!Array.isArray(manifestObj.steps)) {
      throw new Error("Manifest steps must be an array");
    }
    const steps = manifestObj.steps;
    if (steps.length === 0) {
      throw new Error("Manifest must contain at least one step");
    }
    const startStepExists = steps.some((step) => {
      return typeof step === "object" && step !== null && "id" in step && step.id === manifestObj.startStep;
    });
    if (!startStepExists) {
      throw new Error(`Start step '${manifestObj.startStep}' not found in steps array`);
    }
    for (const stepObj of steps) {
      if (typeof stepObj !== "object" || stepObj === null) {
        throw new Error("Each step must be an object");
      }
      const step = stepObj;
      if (!step.id) {
        throw new Error("Each step must have an id");
      }
      if (!step.transitions || !Array.isArray(step.transitions)) {
        throw new Error(`Step '${step.id}' must have transitions array`);
      }
    }
  }
  /**
   * Determines which step to start from based on persistence and saved progress
   * @param manifest - The loaded playlist manifest
   * @param options - Playlist configuration options
   * @param savedProgress - Previously saved progress data
   * @returns The step ID to start from
   */
  determineStartStep(manifest, options, savedProgress) {
    const isPersistenceEnabled = options.persistence ?? manifest.isPersistent ?? true;
    const manifestIdForProgress = manifest.id;
    const wasTriggered = options._triggeredByTriggerManager === true;
    let startStepId = manifest.startStep;
    const pendingNav = this.checkPendingNavigation(manifest);
    if (pendingNav) {
      log(`PlaylistLoader: Resuming from pending URL navigation to step '${pendingNav.nextStepId}'`);
      return pendingNav.nextStepId;
    }
    if (options.startNodeId) {
      const customStep = manifest.steps.find((step) => step.id === options.startNodeId);
      if (customStep) {
        log(`PlaylistLoader: Using custom start node from options: ${options.startNodeId}`);
        startStepId = options.startNodeId;
      } else {
        log(`PlaylistLoader: Custom start node '${options.startNodeId}' not found, using default start step`);
      }
    } else if (wasTriggered) {
      startStepId = manifest.startStep;
    } else if (isPersistenceEnabled && savedProgress && savedProgress[manifestIdForProgress]) {
      const progressData = savedProgress[manifestIdForProgress];
      if (progressData.status === "completed") {
        startStepId = manifest.startStep;
      } else {
        const { isValid, ageMs } = isProgressRecent(progressData);
        if (!isValid) {
          startStepId = manifest.startStep;
        } else {
          const lastStepId = progressData.currentStepId || progressData.lastStepId;
          if (lastStepId) {
            const savedStep = manifest.steps.find((step) => step.id === lastStepId);
            if (savedStep) {
              startStepId = lastStepId;
            }
          }
        }
      }
    }
    return startStepId;
  }
  /**
   * Checks for pending navigation from a cross-page URL transition
   * This handles the case where user navigated to a new page (hard refresh)
   * and we need to resume from the step that was waiting for that URL
   * @param manifest - The loaded playlist manifest
   * @returns The pending navigation data if valid, null otherwise
   */
  checkPendingNavigation(manifest) {
    const storageManager2 = StorageManager.getInstance();
    const pending = storageManager2.getPendingNavigation();
    if (!pending) {
      return null;
    }
    if (pending.playlistId !== manifest.id) {
      log(`PlaylistLoader: Pending navigation is for different playlist (${pending.playlistId}), ignoring`);
      return null;
    }
    const ageMs = Date.now() - pending.timestamp;
    if (ageMs > TIMING.PENDING_NAVIGATION_EXPIRY) {
      storageManager2.clearPendingNavigation();
      return null;
    }
    if (!this.isURLPathMatch(pending.urlPattern)) {
      log(`PlaylistLoader: URL doesn't match pending pattern '${pending.urlPattern}', clearing`);
      storageManager2.clearPendingNavigation();
      return null;
    }
    const targetStep = manifest.steps.find((step) => step.id === pending.nextStepId);
    if (!targetStep) {
      log(`PlaylistLoader: Pending navigation target step '${pending.nextStepId}' not found in manifest, clearing`);
      storageManager2.clearPendingNavigation();
      return null;
    }
    log(`PlaylistLoader: Using pending navigation to step '${pending.nextStepId}' (${Math.round(ageMs / 1e3)}s old)`);
    storageManager2.clearPendingNavigation();
    return pending;
  }
  /**
   * Checks if the current URL path matches a pattern
   * Uses the same logic as TransitionManager for consistency
   * @param pattern - The URL pattern to match (supports wildcards)
   * @returns true if the current URL matches the pattern
   */
  isURLPathMatch(pattern) {
    if (!pattern) {
      return false;
    }
    const currentUrl = window.location.href;
    const currentPath = window.location.pathname;
    const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const regexPattern = escapedPattern.replace(/\\\*/g, ".*");
    const regex = new RegExp(regexPattern);
    const match = regex.test(currentUrl) || regex.test(currentPath);
    return match;
  }
}
class PlaylistManager extends EventSubscriberManager {
  /**
   * Creates a new PlaylistManager
   * @param eventManager - Optional event manager to subscribe to events
   * @param storageManager - Optional storage manager for localStorage operations
   */
  constructor(eventManager, storageManager2) {
    super(eventManager);
    __publicField(this, "isUpdatingWatchedPlaylists", false);
    __publicField(this, "playlistLoader");
    __publicField(this, "storageManager");
    this.playlistLoader = new PlaylistLoader();
    this.storageManager = storageManager2 || StorageManager.getInstance();
  }
  /**
   * Subscribe to relevant player events for playlist tracking
   */
  subscribeToEvents() {
    if (!this.eventManager) {
      return;
    }
    this.eventManager.on("playlistStarted", (event) => {
      log(`PlaylistManager: Playlist started event received - ${event.playlist.id}`);
      this.updateWatchedPlaylistStatus(event.playlist.id, "in_progress");
    });
    this.eventManager.on("playlistEnded", (event) => {
      log(`PlaylistManager: Playlist ended event received - ${event.playlist.id}`);
      this.updateWatchedPlaylistStatus(event.playlist.id, "completed").catch((error2) => {
        console.error(`PlaylistManager: Error in updateWatchedPlaylistStatus for playlist ${event.playlist.id}:`, error2);
      });
    });
    this.eventManager.on("playlistDismissed", (event) => {
      log(`PlaylistManager: Playlist dismissed event received - ${event.playlist.id}`);
      this.updateWatchedPlaylistStatus(event.playlist.id, "dismissed");
    });
    this.eventManager.on("stepStarted", (event) => {
      log(`PlaylistManager: Step started event received - ${event.step.id}`);
      this.updateWatchedPlaylistStatus(event.playlist.id, "in_progress", event.step.id);
    });
  }
  /**
   * Updates the watched playlist status locally and in the backend
   * @param playlistId - ID of the playlist
   * @param status - New status ('in_progress' or 'completed')
   * @param currentStepId - Optional current step ID
   */
  async updateWatchedPlaylistStatus(playlistId, status, currentStepId) {
    var _a, _b, _c, _d, _e, _f;
    if (this.isUpdatingWatchedPlaylists) {
      return;
    }
    this.isUpdatingWatchedPlaylists = true;
    try {
      const store = getSaltfishStore();
      const currentUserData = store.userData || {};
      const currentWatchedPlaylists = currentUserData.watchedPlaylists || {};
      const existingPlaylistData = currentWatchedPlaylists[playlistId];
      const previousStatus = existingPlaylistData == null ? void 0 : existingPlaylistData.status;
      const currentVisitCount = (existingPlaylistData == null ? void 0 : existingPlaylistData.visitCount) ?? 0;
      const shouldIncrementVisitCount = (status === "completed" || status === "dismissed") && previousStatus !== status;
      const updatedPlaylistData = {
        status,
        // Don't save currentStepId for completed playlists - they should restart from beginning
        currentStepId: status === "completed" ? null : currentStepId || store.currentStepId || null,
        timestamp: Date.now(),
        // Use timestamp for consistency with checkAndResumeInProgressPlaylist
        lastProgressAt: Date.now(),
        // Keep for backward compatibility
        visitCount: shouldIncrementVisitCount ? currentVisitCount + 1 : currentVisitCount
      };
      const updatedWatchedPlaylists = {
        ...currentWatchedPlaylists,
        [playlistId]: updatedPlaylistData
      };
      store.setUserData({
        ...currentUserData,
        watchedPlaylists: updatedWatchedPlaylists
      });
      log(`PlaylistManager: Updated local watched playlists for ${playlistId} with status ${status}`);
      if (!((_a = store == null ? void 0 : store.config) == null ? void 0 : _a.token) || !((_b = store == null ? void 0 : store.user) == null ? void 0 : _b.id) || ((_c = store == null ? void 0 : store.user) == null ? void 0 : _c.__isAnonymous)) {
        log(`PlaylistManager: Cannot update backend - token: ${!!((_d = store == null ? void 0 : store.config) == null ? void 0 : _d.token)}, userId: ${!!((_e = store == null ? void 0 : store.user) == null ? void 0 : _e.id)}, anonymous: ${(_f = store == null ? void 0 : store.user) == null ? void 0 : _f.__isAnonymous}`);
        this.updateAnonymousUserWatchedPlaylists(playlistId, status, currentStepId || store.currentStepId || null);
        return;
      }
      const apiUrl = `https://player.saltfish.ai/clients/${store.config.token}/users/${store.user.id}/playlists/${playlistId}`;
      log(`PlaylistManager: Making API call to update status - URL: ${apiUrl}, Status: ${status}`);
      try {
        const response = await fetch(apiUrl, {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            status,
            currentStepId: currentStepId || store.currentStepId || null
          })
        });
        if (!response.ok) {
          throw new Error(`Failed to update watched playlist status: ${response.statusText} (${response.status})`);
        }
        log(`PlaylistManager: Successfully updated watched playlist status via API for ${playlistId} to '${status}'`);
      } catch (error2) {
        console.error(`PlaylistManager: Error updating watched playlist status for ${playlistId}:`, error2);
      }
    } finally {
      this.isUpdatingWatchedPlaylists = false;
    }
  }
  /**
   * Updates anonymous user watched playlist status in localStorage
   * @param playlistId - ID of the playlist
   * @param status - New status ('in_progress', 'completed', or 'dismissed')
   * @param currentStepId - Optional current step ID
   */
  updateAnonymousUserWatchedPlaylists(playlistId, status, currentStepId) {
    if (typeof window === "undefined") {
      return;
    }
    let anonymousUserData = this.storageManager.getAnonymousUserData() || {
      userId: "anonymous",
      userData: {},
      watchedPlaylists: {},
      timestamp: Date.now()
    };
    if (!anonymousUserData.watchedPlaylists) {
      anonymousUserData.watchedPlaylists = {};
    }
    const existingPlaylistData = anonymousUserData.watchedPlaylists[playlistId];
    const previousStatus = existingPlaylistData == null ? void 0 : existingPlaylistData.status;
    const currentVisitCount = (existingPlaylistData == null ? void 0 : existingPlaylistData.visitCount) ?? 0;
    const shouldIncrementVisitCount = (status === "completed" || status === "dismissed") && previousStatus !== status;
    anonymousUserData.watchedPlaylists[playlistId] = {
      status,
      currentStepId: currentStepId || null,
      timestamp: Date.now(),
      // Use timestamp for consistency with checkAndResumeInProgressPlaylist
      lastProgressAt: Date.now(),
      // Keep for backward compatibility
      visitCount: shouldIncrementVisitCount ? currentVisitCount + 1 : currentVisitCount
    };
    anonymousUserData.timestamp = Date.now();
    this.storageManager.setAnonymousUserData(anonymousUserData);
  }
  /**
   * Loads a playlist manifest and sets up the store
   * @param playlistId - Path or identifier for the playlist manifest
   * @param options - Playlist configuration options
   */
  async load(playlistId, options) {
    var _a;
    try {
      const store = getSaltfishStore();
      if (!store) {
        throw new Error("Store not available");
      }
      const savedProgress = ((_a = store.userData) == null ? void 0 : _a.watchedPlaylists) || store.progress;
      const loadResult = await this.playlistLoader.loadManifest({
        manifestPath: playlistId,
        playlistOptions: options,
        savedProgress
      });
      const { manifest, startStepId } = loadResult;
      const firstStep = manifest.steps.find((step) => step.id === startStepId);
      store.setManifest(manifest, startStepId);
      if (firstStep) {
        store.sendStateMachineEvent({
          type: "MANIFEST_LOADED",
          step: firstStep
        });
      }
    } catch (error2) {
      const store = getSaltfishStore();
      const errorObj = error2 instanceof Error ? error2 : new Error("Unknown error loading manifest");
      if (store) {
        store.setError(errorObj);
      } else {
        console.error("[PlaylistManager] Cannot set error: Store not available", errorObj);
      }
    }
  }
  /**
   * Cleans up resources used by the playlist manager
   */
  destroy() {
    if (this.eventManager) {
      this.eventManager = null;
    }
  }
}
class MinimizeButton {
  constructor(playerElement) {
    __publicField(this, "button");
    __publicField(this, "playerElement");
    this.playerElement = playerElement;
    this.button = document.createElement("button");
    this.button.className = "sf-player__minimize-button";
    this.button.innerHTML = createCloseIcon(20);
    this.button.addEventListener("click", this.handleClick.bind(this));
    this.playerElement.appendChild(this.button);
    this.updateVisibility(getSaltfishStore().isMinimized);
  }
  handleClick() {
    const store = getSaltfishStore();
    const isMinimized = !store.isMinimized;
    if (isMinimized) {
      store.minimize();
    } else {
      store.maximize();
    }
    if (isMinimized) {
      this.minimize();
    } else {
      this.maximize();
    }
  }
  minimize() {
    this.button.innerHTML = ICON_PLUS;
  }
  maximize() {
    this.button.innerHTML = createCloseIcon(20);
  }
  updateVisibility(isMinimized) {
    if (isMinimized) {
      this.button.classList.add("sf-hidden");
    } else {
      this.button.classList.remove("sf-hidden");
    }
  }
  destroy() {
    this.button.removeEventListener("click", this.handleClick.bind(this));
    this.button.remove();
  }
}
class PlayPauseButton {
  constructor(container, videoManager) {
    __publicField(this, "playButton");
    __publicField(this, "container");
    __publicField(this, "videoManager", null);
    this.container = container;
    this.videoManager = videoManager || null;
    this.createButton();
  }
  createButton() {
    this.playButton = document.createElement("button");
    this.playButton.className = "sf-controls-container__play-button";
    this.playButton.innerHTML = ICON_PLAY;
    this.playButton.addEventListener("click", this.handlePlayClick.bind(this));
    this.container.appendChild(this.playButton);
  }
  handlePlayClick(event) {
    if (event) {
      event.stopPropagation();
    }
    const store = getSaltfishStore();
    if (store.currentState === "autoplayBlocked") {
      if (this.videoManager) {
        this.videoManager.markUserInteraction();
        this.videoManager.setMuted(false);
        const videoElement = this.videoManager.getVideoElement();
        if (videoElement) {
          videoElement.loop = false;
          videoElement.currentTime = 0;
        }
      } else {
        const rootPlayer = this.playButton.closest(".sf-player");
        if (rootPlayer) {
          const videoElement = rootPlayer.querySelector(".sf-video-container__video");
          if (videoElement) {
            videoElement.muted = false;
            videoElement.loop = false;
            videoElement.currentTime = 0;
          }
        }
      }
    } else {
      if (this.videoManager) {
        this.videoManager.markUserInteraction();
      } else {
        console.warn("PlayPauseButton: VideoManager not available, falling back to store play");
      }
    }
    if (store.currentState === "paused" || store.currentState === "waitingForInteraction" || store.currentState === "autoplayBlocked") {
      store.play();
    }
  }
  destroy() {
    this.playButton.removeEventListener("click", this.handlePlayClick.bind(this));
    this.playButton.remove();
  }
}
class ExitButton {
  constructor(playerElement) {
    __publicField(this, "button");
    __publicField(this, "playerElement");
    this.playerElement = playerElement;
    this.button = document.createElement("button");
    this.button.className = "sf-player__exit-button";
    this.button.innerHTML = ICON_CLOSE;
    this.button.addEventListener("click", this.handleClick.bind(this));
    this.playerElement.appendChild(this.button);
    this.updateVisibility(getSaltfishStore().isMinimized);
  }
  handleClick(e) {
    e.stopPropagation();
    const store = getSaltfishStore();
    store.sendStateMachineEvent({ type: "EXIT" });
  }
  updateVisibility(isMinimized) {
    if (isMinimized) {
      this.button.classList.remove("sf-hidden");
    } else {
      this.button.classList.add("sf-hidden");
    }
  }
  destroy() {
    this.button.removeEventListener("click", this.handleClick.bind(this));
    this.button.remove();
  }
}
class ErrorDisplay {
  constructor(container, options = {}) {
    __publicField(this, "errorOverlay");
    __publicField(this, "container");
    __publicField(this, "options");
    __publicField(this, "isVisible", false);
    this.container = container;
    this.options = options;
    this.createErrorDisplay();
  }
  /**
   * Creates the error display overlay with message
   */
  createErrorDisplay() {
    this.errorOverlay = document.createElement("div");
    this.errorOverlay.className = CSS_CLASSES.ERROR_DISPLAY;
    this.errorOverlay.classList.add("sf-hidden");
    const errorContent = document.createElement("div");
    errorContent.className = CSS_CLASSES.ERROR_DISPLAY_CONTENT;
    const errorMessage = document.createElement("p");
    errorMessage.className = CSS_CLASSES.ERROR_DISPLAY_MESSAGE;
    errorMessage.textContent = this.options.message || "Unable to load content";
    errorContent.appendChild(errorMessage);
    this.errorOverlay.appendChild(errorContent);
    this.container.appendChild(this.errorOverlay);
  }
  /**
   * Shows the error display with optional new message and options
   */
  show(newOptions) {
    if (newOptions) {
      this.updateOptions(newOptions);
    }
    this.errorOverlay.classList.remove("sf-hidden");
    this.isVisible = true;
    requestAnimationFrame(() => {
      this.errorOverlay.classList.add(CSS_CLASSES.ERROR_DISPLAY_VISIBLE);
    });
  }
  /**
   * Hides the error display
   */
  hide() {
    if (!this.isVisible) {
      return;
    }
    this.errorOverlay.classList.remove(CSS_CLASSES.ERROR_DISPLAY_VISIBLE);
    setTimeout(() => {
      this.errorOverlay.classList.add("sf-hidden");
      this.isVisible = false;
    }, 300);
  }
  /**
   * Updates the error display options and content
   */
  updateOptions(newOptions) {
    this.options = { ...this.options, ...newOptions };
    if (newOptions.message !== void 0) {
      const messageElement = this.errorOverlay.querySelector(`.${CSS_CLASSES.ERROR_DISPLAY_MESSAGE}`);
      if (messageElement) {
        messageElement.textContent = newOptions.message || "Unable to load content";
      }
    }
  }
  /**
   * Gets user-friendly error message based on error type
   */
  static getErrorMessage(error2, errorType) {
    const errorString = error2 instanceof Error ? error2.message : error2;
    const lowerErrorString = errorString.toLowerCase();
    if (errorType === "video" || lowerErrorString.includes("video") || lowerErrorString.includes("cannot load video")) {
      if (lowerErrorString.includes("no video url") || lowerErrorString.includes("video url provided")) {
        return "This step doesn't have a video to play";
      }
      return "Unable to load video content";
    }
    if (errorType === "network" || lowerErrorString.includes("network") || lowerErrorString.includes("fetch")) {
      return "Connection issue detected";
    }
    if (errorType === "playlist" || lowerErrorString.includes("playlist")) {
      return "Unable to load playlist";
    }
    if (errorType === "initialization" || lowerErrorString.includes("token") || lowerErrorString.includes("initialize")) {
      return "Setup issue detected";
    }
    return "Unable to load content";
  }
  /**
   * Checks if the error display is currently visible
   */
  isShowing() {
    return this.isVisible;
  }
  /**
   * Destroys the error display and cleans up resources
   */
  destroy() {
    if (this.errorOverlay) {
      this.errorOverlay.remove();
    }
  }
}
class LoadingSpinner {
  constructor(container) {
    __publicField(this, "container");
    __publicField(this, "spinnerElement", null);
    this.container = container;
  }
  /**
   * Shows the loading spinner
   */
  show() {
    if (this.spinnerElement) {
      this.spinnerElement.classList.remove("sf-hidden");
      return;
    }
    this.spinnerElement = document.createElement("div");
    this.spinnerElement.className = `${CSS_CLASSES.LOADING_SPINNER}`;
    this.spinnerElement.innerHTML = `
      <div class="sf-loading-spinner__content">
        <div class="sf-loading-spinner__icon">${ICON_LOADING_SPINNER}</div>
        <div class="sf-loading-spinner__text">Loading playlist...</div>
      </div>
    `;
    this.container.appendChild(this.spinnerElement);
  }
  /**
   * Hides the loading spinner
   */
  hide() {
    if (this.spinnerElement) {
      this.spinnerElement.classList.add("sf-hidden");
    }
  }
  /**
   * Updates the loading message
   * @param message - New loading message to display
   */
  updateMessage(message) {
    if (this.spinnerElement) {
      const textElement = this.spinnerElement.querySelector(".sf-loading-spinner__text");
      if (textElement) {
        textElement.textContent = message;
      }
    }
  }
  /**
   * Destroys the loading spinner and removes it from DOM
   */
  destroy() {
    if (this.spinnerElement) {
      this.spinnerElement.remove();
      this.spinnerElement = null;
    }
  }
}
class UIManager {
  constructor(shadowDOMManager) {
    __publicField(this, "shadowDOMManager");
    __publicField(this, "playerRoot", null);
    __publicField(this, "playerElement", null);
    __publicField(this, "minimizeButton", null);
    __publicField(this, "exitButton", null);
    __publicField(this, "playPauseButton", null);
    __publicField(this, "errorDisplay", null);
    __publicField(this, "loadingSpinner", null);
    __publicField(this, "compactLabel", null);
    // Button management properties (from ButtonManager)
    __publicField(this, "playbackButtonsVisible", false);
    __publicField(this, "playButton", null);
    __publicField(this, "centerPlayButton", null);
    __publicField(this, "videoManager", null);
    __publicField(this, "storeUnsubscribe", null);
    this.shadowDOMManager = shadowDOMManager;
  }
  /**
   * Creates the complete player UI including all DOM structure and button management
   */
  createPlayerUI(videoManager, cursorManager, interactionManager) {
    this.videoManager = videoManager;
    if (this.playerElement) {
      this.reset();
    }
    if (!this.playerRoot) {
      this.shadowDOMManager.create();
      this.playerRoot = this.shadowDOMManager.getRootElement();
      if (!this.playerRoot) {
        console.error("Failed to create player root element");
        return;
      }
    }
    this.playerElement = this.createPlayerElement(this.playerRoot);
    this.minimizeButton = new MinimizeButton(this.playerElement);
    this.exitButton = new ExitButton(this.playerElement);
    videoManager.create(this.playerElement);
    const controlsContainer = this.createControlsContainer(this.playerElement);
    this.playPauseButton = new PlayPauseButton(controlsContainer, videoManager);
    this.createSaltfishLogo(this.playerElement);
    this.setupVideoContainerClickHandler();
    cursorManager.create();
    interactionManager.create(this.playerElement);
    this.initializeButtonManagement();
    const store = getSaltfishStore();
    this.handleMinimizeStateChange(store.isMinimized);
  }
  /**
   * Sets up click handler for video container
   */
  setupVideoContainerClickHandler() {
    var _a;
    const videoContainer = (_a = this.playerElement) == null ? void 0 : _a.querySelector(".sf-video-container");
    if (videoContainer) {
      videoContainer.addEventListener("click", (event) => {
        var _a2;
        const store = getSaltfishStore();
        if (store.isMinimized) {
          event.stopPropagation();
          this.handleMinimizeClick();
          return;
        }
        const target = event.target;
        const isButton = target.tagName === "BUTTON" || target.closest("button") || target.closest(".sf-controls-container");
        if (isButton) {
          return;
        }
        const isCompactMode = (_a2 = this.playerElement) == null ? void 0 : _a2.classList.contains("sf-player--compact");
        if (store.currentState === "playing") {
          store.pause();
        } else if (isCompactMode && (store.currentState === "idleMode" || store.currentState === "autoplayBlocked" || store.currentState === "paused")) {
          this.handleAutoplayFallbackInteraction("compact bubble click");
          store.play();
        } else ;
      });
    }
  }
  /**
   * Updates the position of the player element based on store state
   */
  updatePosition() {
    var _a;
    if (!this.playerRoot || !this.playerElement) {
      console.warn("UIManager: updatePosition called but playerRoot or playerElement is null", {
        playerRoot: !!this.playerRoot,
        playerElement: !!this.playerElement
      });
      return;
    }
    const store = getSaltfishStore();
    let positionToUse = ((_a = store.playlistOptions) == null ? void 0 : _a.position) || "bottom-right";
    if (store.currentStepId && store.manifest) {
      const currentStep = store.manifest.steps.find((step) => step.id === store.currentStepId);
      if (currentStep == null ? void 0 : currentStep.position) {
        positionToUse = currentStep.position;
      }
    }
    if (positionToUse !== "bottom-left" && positionToUse !== "bottom-right") {
      console.warn(`UIManager: Invalid position "${positionToUse}", defaulting to bottom-right`);
      positionToUse = "bottom-right";
    }
    this.playerRoot.classList.remove("sf-player-root--bottom-left", "sf-player-root--bottom-right");
    if (positionToUse === "bottom-left") {
      this.playerRoot.classList.add("sf-player-root--bottom-left");
    } else {
      this.playerRoot.classList.add("sf-player-root--bottom-right");
    }
    if (store.isMinimized) {
      this.playerElement.classList.add(CSS_CLASSES.PLAYER_MINIMIZED);
    } else {
      this.playerElement.classList.remove(CSS_CLASSES.PLAYER_MINIMIZED);
    }
  }
  /**
   * Handles minimize button click
   */
  handleMinimizeClick() {
    if (!this.playerElement) {
      return;
    }
    const store = getSaltfishStore();
    const isMinimized = !store.isMinimized;
    if (isMinimized) {
      store.minimize();
    } else {
      store.maximize();
    }
    if (isMinimized) {
      this.minimizeButton.minimize();
    } else {
      this.minimizeButton.maximize();
    }
  }
  /**
   * Handles minimize state changes and updates button visibility
   */
  handleMinimizeStateChange(isMinimized) {
    if (this.exitButton) {
      this.exitButton.updateVisibility(isMinimized);
    }
    if (this.minimizeButton) {
      this.minimizeButton.updateVisibility(isMinimized);
    }
  }
  /**
   * Gets the player element
   */
  getPlayerElement() {
    return this.playerElement;
  }
  /**
   * Gets the player root element
   */
  getPlayerRoot() {
    return this.playerRoot;
  }
  /**
   * Shows an error display with the specified options
   */
  showError(error2, errorType, customOptions) {
    if (!this.playerElement) {
      console.warn("UIManager: Cannot show error display - player element not available");
      return;
    }
    const message = ErrorDisplay.getErrorMessage(error2, errorType);
    if (!this.errorDisplay) {
      this.errorDisplay = new ErrorDisplay(this.playerElement, {
        message,
        ...customOptions
      });
    }
    this.errorDisplay.show({
      message,
      ...customOptions
    });
  }
  /**
   * Hides the current error display
   */
  hideError() {
    if (this.errorDisplay) {
      this.errorDisplay.hide();
    }
  }
  /**
   * Checks if error display is currently visible
   */
  isErrorDisplayVisible() {
    var _a;
    return ((_a = this.errorDisplay) == null ? void 0 : _a.isShowing()) || false;
  }
  /**
   * Shows the loading spinner with optional message
   * @param message - Optional loading message to display
   */
  showLoading(message) {
    if (!this.playerElement) {
      console.warn("UIManager: Cannot show loading spinner - player element not available");
      return;
    }
    if (!this.loadingSpinner) {
      this.loadingSpinner = new LoadingSpinner(this.playerElement);
    }
    this.loadingSpinner.show();
    if (message) {
      this.loadingSpinner.updateMessage(message);
    }
  }
  /**
   * Hides the loading spinner
   */
  hideLoading() {
    if (this.loadingSpinner) {
      this.loadingSpinner.hide();
    }
  }
  /**
   * Updates the loading message
   * @param message - New loading message to display
   */
  updateLoadingMessage(message) {
    if (this.loadingSpinner) {
      this.loadingSpinner.updateMessage(message);
    }
  }
  /**
   * Shows the player by fading it in
   */
  showPlayer() {
    if (this.playerElement) {
      this.playerElement.classList.add("sf-player--visible");
    }
  }
  // === DOM CREATION METHODS (from PlayerView) ===
  /**
   * Creates the main player DOM structure
   * @param shadowRoot - The shadow DOM root element to append to
   * @returns The created player element
   */
  createPlayerElement(shadowRoot) {
    const playerElement = document.createElement("div");
    playerElement.className = CSS_CLASSES.PLAYER;
    shadowRoot.appendChild(playerElement);
    return playerElement;
  }
  /**
   * Creates the controls container structure
   * @param parentElement - The parent element to append the controls container to
   * @returns The created controls container element
   */
  createControlsContainer(parentElement) {
    const controlsContainer = document.createElement("div");
    controlsContainer.className = CSS_CLASSES.CONTROLS_CONTAINER;
    parentElement.appendChild(controlsContainer);
    return controlsContainer;
  }
  /**
   * Creates and adds the saltfish logo to the player
   * @param parentElement - The parent element to append the logo to
   */
  createSaltfishLogo(parentElement) {
    var _a;
    const store = getSaltfishStore();
    if (((_a = store.config) == null ? void 0 : _a.showLogo) === false) {
      return;
    }
    const logoContainer = document.createElement("div");
    logoContainer.className = CSS_CLASSES.LOGO;
    logoContainer.innerHTML = `
      <svg viewBox="0 0 41 15" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M8.42034 7.49991C8.42034 9.67502 6.65707 11.4383 4.48196 11.4383C2.30685 11.4383 4.00664 9.67502 4.00664 7.49991C4.00664 5.3248 2.30685 3.56152 4.48196 3.56152C6.65707 3.56152 8.42034 5.3248 8.42034 7.49991Z" fill="white"/>
        <path d="M3.53097 7.43198C3.53097 8.96956 2.29707 10.216 0.774969 10.216C-0.747128 10.216 0.442349 8.96956 0.442349 7.43198C0.442349 5.8944 -0.747128 4.64795 0.774969 4.64795C2.29707 4.64795 3.53097 5.8944 3.53097 7.43198Z" fill="white"/>
        <path d="M15.0603 10.628C13.8603 10.628 12.9803 9.892 12.9803 8.372L13.6043 8.292C13.6043 9.492 14.1803 10.044 15.0603 10.044C15.8603 10.044 16.3243 9.612 16.3243 8.972C16.3243 8.252 15.8363 7.988 15.0603 7.788C13.7003 7.436 13.2523 6.996 13.2523 6.196C13.2523 5.396 13.9403 4.772 14.9803 4.772C16.0203 4.772 16.6923 5.396 16.6923 6.436L16.0683 6.516C16.0683 5.796 15.6203 5.356 14.9803 5.356C14.3403 5.356 13.8763 5.636 13.8763 6.196C13.8763 6.756 14.2843 7.004 15.0603 7.204C16.3723 7.548 16.9483 8.012 16.9483 8.972C16.9483 9.932 16.2603 10.628 15.0603 10.628ZM19.182 10.628C18.542 10.628 17.83 10.228 17.83 9.428C17.83 8.548 18.462 8.212 19.494 8.076C19.966 8.012 20.638 7.956 20.414 7.38C20.27 7.004 19.734 6.956 19.494 6.956C18.854 6.956 18.534 7.236 18.534 7.716L17.91 7.636C17.91 6.756 18.774 6.372 19.494 6.372C20.214 6.372 21.022 6.692 21.022 7.652V10.5H20.438V9.7C20.262 10.22 19.798 10.628 19.182 10.628ZM18.454 9.46C18.454 9.78 18.782 10.044 19.182 10.044C19.822 10.044 20.438 9.596 20.438 8.476V8.26C20.286 8.476 19.91 8.596 19.374 8.66C18.742 8.748 18.454 9.06 18.454 9.46ZM22.3044 10.5V4.82H22.9284V10.5H22.3044ZM25.6789 10.5C24.9909 10.5 24.6469 10.204 24.6469 9.508V7.084H23.9669V6.5H24.6469V5.292L25.1909 5.212V6.5H26.2309V7.084H25.1909V9.34C25.1909 9.836 25.3829 9.916 25.7749 9.916H26.2309V10.5H25.6789ZM27.7094 6.052C27.7094 5.116 28.0934 4.82 28.7814 4.82H29.3334V5.404H28.8774C28.4454 5.404 28.2934 5.484 28.2934 6.22V6.5H29.3334V7.084H28.2934V10.5H27.7094V7.084H27.0294V6.5H27.7094V6.052ZM30.2805 10.5V6.5H30.9045V10.5H30.2805ZM30.2165 5.86V5.108H30.9685V5.86H30.2165ZM33.7678 10.628C32.8078 10.628 32.0078 10.06 32.0078 9.02L32.5918 8.94C32.5918 9.66 33.2078 10.044 33.7678 10.044C34.3278 10.044 34.7678 9.86 34.7678 9.38C34.7678 8.9 34.4558 8.772 33.9998 8.708L33.2958 8.612C32.6638 8.524 32.2318 8.092 32.2318 7.532C32.2318 6.812 32.8478 6.372 33.7278 6.372C34.6078 6.372 35.1678 6.892 35.1678 7.612L34.5838 7.692C34.5838 7.212 34.2878 6.956 33.7278 6.956C33.2478 6.956 32.8558 7.132 32.8558 7.532C32.8558 7.852 33.0718 7.996 33.5438 8.068L34.1518 8.156C34.8558 8.26 35.3918 8.66 35.3918 9.38C35.3918 10.1 34.7278 10.628 33.7678 10.628ZM38.9252 7.868C38.9252 7.148 38.5812 6.956 38.1012 6.956C37.6212 6.956 36.9812 7.276 36.9812 8.636V10.5H36.3572V4.9H36.9812V7.364C37.1092 6.812 37.5652 6.372 38.3012 6.372C39.1812 6.372 39.5492 6.988 39.5492 7.868V10.5H38.9252V7.868Z" fill="white"/>
      </svg>
    `;
    parentElement.appendChild(logoContainer);
    logoContainer.addEventListener("click", (event) => {
      var _a2;
      event.stopPropagation();
      const currentStore = getSaltfishStore();
      if (currentStore.currentState === "playing") {
        currentStore.pause();
      }
      const token = (_a2 = currentStore.config) == null ? void 0 : _a2.token;
      if (token) {
        window.open(`https://www.saltfish.ai/demos?clientId=${token}`, "_blank");
      } else {
        console.warn("UIManager: No token available, falling back to saltfish.ai homepage");
        window.open("https://www.saltfish.ai/", "_blank");
      }
    });
  }
  // === BUTTON MANAGEMENT METHODS (from ButtonManager) ===
  /**
   * Initializes button management functionality
   */
  initializeButtonManagement() {
    if (!this.playerElement || !this.videoManager) {
      console.error("UIManager: Cannot initialize button management - missing player element or video manager");
      return;
    }
    this.storeUnsubscribe = useSaltfishStore.subscribe((state) => {
      this.updatePlayPauseButton(state.currentState);
    });
    this.playButton = this.playerElement.querySelector(".sf-controls-container__play-button");
    this.centerPlayButton = this.playerElement.querySelector(".sf-player__center-play-button");
    if (!this.centerPlayButton) {
      this.createCenterPlayButton();
    }
    if (!this.playButton) {
      console.error("UIManager: Failed to find play button during initialization");
    }
    log("UIManager: Center play button state: " + (this.centerPlayButton ? "found/created" : "not found"));
  }
  /**
   * Displays or hides the playback buttons based on the given visibility state
   * @param visible - Whether the buttons should be visible
   */
  displayPlaybackButtons(visible) {
    if (this.playbackButtonsVisible === visible) {
      return;
    }
    this.playbackButtonsVisible = visible;
  }
  /**
   * Updates the play/pause button state based on the video state
   * @param state - The current video state ('playing' or 'paused')
   */
  updatePlayPauseButton(state) {
    if (this.playButton) {
      this.playButton.classList.add("sf-hidden");
    }
    if (!this.centerPlayButton && this.playerElement) {
      this.centerPlayButton = this.playerElement.querySelector(".sf-player__center-play-button");
      if (!this.centerPlayButton) {
        this.createCenterPlayButton();
      }
    }
    if (!this.centerPlayButton) {
      return;
    }
    if (state === "playing" || state === "completed") {
      this.centerPlayButton.classList.remove("sf-player__center-play-button--visible");
      this.centerPlayButton.classList.remove("sf-player__center-play-button--prominent");
    } else {
      this.centerPlayButton.classList.add("sf-player__center-play-button--visible");
      if (state === "autoplayBlocked" || state === "idleMode") {
        this.centerPlayButton.classList.add("sf-player__center-play-button--prominent");
      } else {
        this.centerPlayButton.classList.remove("sf-player__center-play-button--prominent");
      }
    }
  }
  /**
   * Handles autoplay fallback and idle mode interaction by unmuting and resetting video
   * @param source - The UI element that triggered the interaction (for logging)
   */
  handleAutoplayFallbackInteraction(source) {
    var _a;
    const store = getSaltfishStore();
    if (store.currentState !== "autoplayBlocked" && store.currentState !== "idleMode") {
      if (this.videoManager) {
        this.videoManager.markUserInteraction();
      }
      return;
    }
    store.currentState === "autoplayBlocked" ? "autoplay fallback" : "idle";
    if (this.videoManager) {
      this.videoManager.markUserInteraction();
      this.videoManager.setMuted(false);
      const videoElement = this.videoManager.getVideoElement();
      if (videoElement) {
        videoElement.loop = false;
        videoElement.currentTime = 0;
      }
    } else {
      const videoElement = (_a = this.playerElement) == null ? void 0 : _a.querySelector(".sf-video-container__video");
      if (videoElement) {
        videoElement.muted = false;
        videoElement.loop = false;
        videoElement.currentTime = 0;
      }
    }
  }
  /**
   * Creates the center play button with proper click handler
   */
  createCenterPlayButton() {
    if (!this.playerElement) {
      return;
    }
    this.centerPlayButton = document.createElement("button");
    this.centerPlayButton.className = "sf-player__center-play-button";
    this.centerPlayButton.innerHTML = `
      <svg class="sf-player__center-play-button__play-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M8 5v14l11-7z" fill="currentColor"/>
      </svg>
      <svg class="sf-player__center-play-button__pause-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" fill="currentColor"/>
      </svg>
      <svg class="sf-player__center-play-button__replay-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" fill="currentColor"/>
      </svg>
    `;
    this.playerElement.appendChild(this.centerPlayButton);
    this.centerPlayButton.addEventListener("click", (e) => {
      e.stopPropagation();
      e.preventDefault();
      const store = getSaltfishStore();
      this.handleAutoplayFallbackInteraction("center play button");
      if (store.currentState === "paused" || store.currentState === "waitingForInteraction" || store.currentState === "completedWaitingForInteraction" || store.currentState === "autoplayBlocked" || store.currentState === "idleMode" || store.currentState === "error") {
        store.play();
      } else if (store.currentState === "playing") {
        store.pause();
      }
    });
  }
  /**
   * Shows the compact label next to the player with the provided text
   */
  showCompactLabel(labelText) {
    var _a;
    if (!this.playerRoot || !labelText) {
      return;
    }
    this.hideCompactLabel();
    const store = getSaltfishStore();
    let positionToUse = ((_a = store.playlistOptions) == null ? void 0 : _a.position) || "bottom-right";
    if (store.currentStepId && store.manifest) {
      const currentStep = store.manifest.steps.find((step) => step.id === store.currentStepId);
      if (currentStep == null ? void 0 : currentStep.position) {
        positionToUse = currentStep.position;
      }
    }
    const isPlayerOnRight = positionToUse === "bottom-right";
    const labelPositionClass = isPlayerOnRight ? "sf-compact-label--left" : "sf-compact-label--right";
    this.compactLabel = document.createElement("div");
    this.compactLabel.className = `sf-compact-label ${labelPositionClass}`;
    this.compactLabel.textContent = labelText;
    this.compactLabel.style.cursor = "pointer";
    this.compactLabel.style.pointerEvents = "auto";
    this.compactLabel.addEventListener("click", (e) => {
      e.stopPropagation();
      e.preventDefault();
      const currentStore = getSaltfishStore();
      this.handleAutoplayFallbackInteraction("compact label");
      if (currentStore.currentState === "paused" || currentStore.currentState === "waitingForInteraction" || currentStore.currentState === "completedWaitingForInteraction" || currentStore.currentState === "autoplayBlocked" || currentStore.currentState === "idleMode" || currentStore.currentState === "error") {
        currentStore.play();
      }
    });
    this.playerRoot.appendChild(this.compactLabel);
  }
  /**
   * Hides and removes the compact label
   */
  hideCompactLabel() {
    if (this.compactLabel && this.compactLabel.parentNode) {
      this.compactLabel.parentNode.removeChild(this.compactLabel);
      this.compactLabel = null;
    }
  }
  /**
   * Resets the UI manager to initial state for reuse
   */
  reset() {
    if (this.minimizeButton) {
      this.minimizeButton.destroy();
      this.minimizeButton = null;
    }
    if (this.exitButton) {
      this.exitButton.destroy();
      this.exitButton = null;
    }
    if (this.playPauseButton) {
      this.playPauseButton.destroy();
      this.playPauseButton = null;
    }
    if (this.errorDisplay) {
      this.errorDisplay.destroy();
      this.errorDisplay = null;
    }
    if (this.loadingSpinner) {
      this.loadingSpinner.destroy();
      this.loadingSpinner = null;
    }
    this.hideCompactLabel();
    if (this.playerElement && this.playerElement.parentNode) {
      this.playerElement.parentNode.removeChild(this.playerElement);
    }
    this.playerRoot = null;
    this.playerElement = null;
  }
  /**
   * Enable prominent styling for the center play button (for special states)
   */
  enablePlayButtonProminent() {
    if (this.centerPlayButton) {
      this.centerPlayButton.classList.add("sf-player__center-play-button--prominent");
    }
  }
  /**
   * Disable prominent styling for the center play button
   */
  disablePlayButtonProminent() {
    if (this.centerPlayButton) {
      this.centerPlayButton.classList.remove("sf-player__center-play-button--prominent");
    }
  }
  /**
   * Destroys the UI and cleans up resources
   */
  destroy() {
    if (this.minimizeButton) {
      this.minimizeButton.destroy();
      this.minimizeButton = null;
    }
    if (this.exitButton) {
      this.exitButton.destroy();
      this.exitButton = null;
    }
    if (this.playPauseButton) {
      this.playPauseButton.destroy();
      this.playPauseButton = null;
    }
    if (this.errorDisplay) {
      this.errorDisplay.destroy();
      this.errorDisplay = null;
    }
    if (this.loadingSpinner) {
      this.loadingSpinner.destroy();
      this.loadingSpinner = null;
    }
    if (this.storeUnsubscribe) {
      this.storeUnsubscribe();
      this.storeUnsubscribe = null;
    }
    this.playbackButtonsVisible = false;
    this.playButton = null;
    this.centerPlayButton = null;
    this.videoManager = null;
    this.shadowDOMManager.remove();
    this.playerRoot = null;
    this.playerElement = null;
  }
}
const _StepTimeoutManager = class _StepTimeoutManager {
  constructor(destroyCallback) {
    __publicField(this, "currentStepId", null);
    __publicField(this, "stepTimeoutId", null);
    __publicField(this, "destroyCallback", null);
    this.destroyCallback = destroyCallback;
  }
  /**
   * Update method - called when player state changes
   */
  update(data) {
    const { currentStepId, currentState } = data;
    log(`StepTimeoutManager: State update - currentState: ${currentState}, currentStepId: ${currentStepId}, previousStepId: ${this.currentStepId}`);
    const isActiveState = currentState === "playing" || currentState === "waitingForInteraction" || currentState === "paused" || currentState === "autoplayBlocked";
    if (!isActiveState) {
      if (this.stepTimeoutId !== null) ;
      this.clearStepTimeout();
      return;
    }
    const stepChanged = currentStepId !== this.currentStepId;
    const noTimeoutRunning = this.stepTimeoutId === null;
    if (stepChanged || noTimeoutRunning) {
      if (stepChanged) {
        log(`StepTimeoutManager: Step changed from ${this.currentStepId} to ${currentStepId}`);
      }
      this.setStepTimeout(currentStepId);
    }
  }
  /**
   * Sets or resets the step timeout
   */
  setStepTimeout(stepId) {
    this.clearStepTimeout();
    if (!stepId) {
      return;
    }
    this.currentStepId = stepId;
    this.stepTimeoutId = window.setTimeout(() => {
      this.destroyPlayer();
    }, _StepTimeoutManager.STEP_TIMEOUT_MS);
  }
  /**
   * Clears the current step timeout
   */
  clearStepTimeout() {
    if (this.stepTimeoutId !== null) {
      log(`StepTimeoutManager: Clearing step timeout for step ${this.currentStepId}`);
      window.clearTimeout(this.stepTimeoutId);
      this.stepTimeoutId = null;
    }
  }
  /**
   * Set the destroy callback
   */
  setDestroyCallback(callback) {
    this.destroyCallback = callback;
  }
  /**
   * Destroys the player safely
   */
  destroyPlayer() {
    if (this.destroyCallback) {
      try {
        log("StepTimeoutManager: Calling destroy callback due to step timeout");
        this.destroyCallback();
      } catch (error2) {
      }
    }
  }
  /**
   * Resets the timeout manager
   */
  reset() {
    this.clearStepTimeout();
    this.currentStepId = null;
  }
  /**
   * Destroys the timeout manager and cleans up resources
   */
  destroy() {
    this.clearStepTimeout();
    this.currentStepId = null;
    this.destroyCallback = null;
  }
};
__publicField(_StepTimeoutManager, "STEP_TIMEOUT_MS", TIMING.STEP_TIMEOUT);
let StepTimeoutManager = _StepTimeoutManager;
class ManagerFactory {
  /**
   * Create all manager instances with proper dependencies
   */
  createManagers() {
    const storageManager2 = StorageManager.getInstance();
    const sessionManager = new SessionManager(storageManager2);
    const shadowDOMManager = new ShadowDOMManager();
    const videoManager = new VideoManager();
    const eventManager = new EventManager();
    const analyticsManager = new AnalyticsManager(eventManager);
    const playlistManager = new PlaylistManager(eventManager, storageManager2);
    const abTestManager = new ABTestManager(eventManager);
    const cursorManager = new CursorManager();
    const interactionManager = new InteractionManager();
    const transitionManager = new TransitionManager();
    const triggerManager = new TriggerManager();
    const stepTimeoutManager = new StepTimeoutManager(() => {
    });
    transitionManager.setTriggerManager(triggerManager);
    const uiManager = new UIManager(shadowDOMManager);
    const managers = {
      shadowDOMManager,
      videoManager,
      cursorManager,
      interactionManager,
      analyticsManager,
      sessionManager,
      transitionManager,
      triggerManager,
      abTestManager,
      eventManager,
      playlistManager,
      stepTimeoutManager,
      uiManager,
      storageManager: storageManager2
    };
    return managers;
  }
  /**
   * Reset all managers that support reset
   */
  resetManagers(managers) {
    try {
      if (managers.videoManager.reset) {
        managers.videoManager.reset();
      }
      if (managers.cursorManager.reset) {
        managers.cursorManager.reset();
      }
      if (managers.interactionManager.reset) {
        managers.interactionManager.reset();
      }
      if (managers.transitionManager.reset) {
        managers.transitionManager.reset();
      }
      if (managers.uiManager.reset) {
        managers.uiManager.reset();
      }
      if (managers.stepTimeoutManager.reset) {
        managers.stepTimeoutManager.reset();
      }
      log("ManagerFactory: Managers reset successfully");
    } catch (error2) {
      console.error("ManagerFactory: Error resetting managers:", error2);
    }
  }
  /**
   * Destroy all managers
   */
  destroyManagers(managers) {
    try {
      managers.transitionManager.destroy();
      managers.triggerManager.destroy();
      managers.videoManager.destroy();
      managers.cursorManager.destroy();
      managers.interactionManager.destroy();
      managers.analyticsManager.destroy();
      managers.sessionManager.destroy();
      managers.playlistManager.destroy();
      managers.stepTimeoutManager.destroy();
      managers.uiManager.destroy();
      log("ManagerFactory: All managers destroyed successfully");
    } catch (error2) {
      console.error("ManagerFactory: Error destroying managers:", error2);
    }
  }
}
const _SaltfishPlayer = class _SaltfishPlayer {
  constructor() {
    // Core services
    __publicField(this, "playerInitializationService");
    __publicField(this, "userManagementService");
    __publicField(this, "playlistOrchestrator");
    __publicField(this, "stateMachineActionHandler");
    __publicField(this, "managerOrchestrator");
    // Manager factory
    __publicField(this, "managerFactory");
    // Track initialization state
    __publicField(this, "isInitialized", false);
    if (_SaltfishPlayer.instance) {
      throw new Error("SaltfishPlayer is a singleton. Use getInstance()");
    }
    this.managerFactory = new ManagerFactory();
    const managers = this.managerFactory.createManagers();
    this.playerInitializationService = new PlayerInitializationService(managers);
    this.userManagementService = new UserManagementService(managers);
    this.playlistOrchestrator = new PlaylistOrchestrator(managers);
    this.stateMachineActionHandler = new StateMachineActionHandler(managers);
    this.managerOrchestrator = new ManagerOrchestrator(managers);
    this.userManagementService.setPlayerInitializationService(this.playerInitializationService);
    this.playerInitializationService.setUserManagementService(this.userManagementService);
    this.playerInitializationService.setPlaylistOrchestrator(this.playlistOrchestrator);
    this.playlistOrchestrator.setUserManagementService(this.userManagementService);
    this.playlistOrchestrator.setManagerOrchestrator(this.managerOrchestrator);
    this.playlistOrchestrator.setPlayerInitializationService(this.playerInitializationService);
    this.playlistOrchestrator.setStateMachineActionHandler(this.stateMachineActionHandler);
    this.stateMachineActionHandler.setDestroyCallback(() => this.destroy());
    managers.stepTimeoutManager.setDestroyCallback(() => this.destroy());
    useSaltfishStore.subscribe(() => this.managerOrchestrator.handleStoreChanges());
    this.stateMachineActionHandler.registerStateMachineActions();
  }
  /**
   * Gets the singleton instance of the Saltfish playlist Player
   */
  static getInstance() {
    if (!_SaltfishPlayer.instance) {
      _SaltfishPlayer.instance = new _SaltfishPlayer();
    }
    return _SaltfishPlayer.instance;
  }
  /**
   * Get session ID from SessionManager
   */
  getSessionId() {
    const managers = this.managerOrchestrator.getManagers();
    return managers.sessionManager.getSessionId();
  }
  /**
   * Get run ID from SessionManager
   */
  getRunId() {
    const managers = this.managerOrchestrator.getManagers();
    return managers.sessionManager.getCurrentRunId();
  }
  /**
   * Initialize the player with configuration
   */
  async initialize(config) {
    if (this.isInitialized) {
      console.warn("Saltfish playlist Player is already initialized");
      return;
    }
    try {
      await this.playerInitializationService.initialize(config);
      this.isInitialized = true;
      this.managerOrchestrator.setInitialized(true);
      if (typeof window !== "undefined") {
        window._saltfishPlayer = this;
        window._cursorManager = this.getManagers().cursorManager;
      }
      log("SaltfishPlayer: Initialization completed successfully");
    } catch (error2) {
      throw error2;
    }
  }
  /**
   * Identify a user with ID and optional data
   */
  identifyUser(userId, userData) {
    this.userManagementService.identifyUser(userId, userData);
  }
  /**
   * Identify user anonymously with optional data
   */
  async identifyAnonymous(userData) {
    await this.userManagementService.identifyAnonymous(userData);
  }
  /**
   * Start a playlist with given options
   */
  async startPlaylist(playlistId, options) {
    await this.playlistOrchestrator.startPlaylist(playlistId, options);
  }
  /**
   * Reset current playlist to initial state
   */
  resetPlaylist() {
    this.playlistOrchestrator.resetPlaylist();
  }
  /**
   * Destroy the player and clean up all resources
   */
  destroy() {
    if (!this.isInitialized) {
      console.warn("Saltfish playlist Player is not initialized");
      return;
    }
    try {
      log("SaltfishPlayer: Starting destruction process");
      this.managerOrchestrator.destroyAll();
      this.isInitialized = false;
      log("SaltfishPlayer: Destruction completed successfully");
    } catch (error2) {
      console.error("SaltfishPlayer: Error during destruction:", error2);
      this.isInitialized = false;
    }
  }
  /**
   * Add event listener
   */
  on(eventName, listener) {
    const managers = this.managerOrchestrator.getManagers();
    managers.eventManager.on(eventName, listener);
  }
  /**
   * Remove event listener
   */
  off(eventName, listener) {
    const managers = this.managerOrchestrator.getManagers();
    return managers.eventManager.off(eventName, listener);
  }
  // ===== LEGACY COMPATIBILITY METHODS =====
  // These properties/methods are kept for backwards compatibility with existing code
  /**
   * Get VideoManager instance for backwards compatibility
   */
  get videoManager() {
    const managers = this.managerOrchestrator.getManagers();
    return managers.videoManager;
  }
  /**
   * Get all managers for debugging/testing purposes
   */
  getManagers() {
    return this.managerOrchestrator.getManagers();
  }
  /**
   * Get services for debugging/testing purposes
   */
  getServices() {
    return {
      playerInitializationService: this.playerInitializationService,
      userManagementService: this.userManagementService,
      playlistOrchestrator: this.playlistOrchestrator,
      stateMachineActionHandler: this.stateMachineActionHandler,
      managerOrchestrator: this.managerOrchestrator
    };
  }
};
__publicField(_SaltfishPlayer, "instance", null);
let SaltfishPlayer = _SaltfishPlayer;
const SaltfishPlayer$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
  __proto__: null,
  SaltfishPlayer
}, Symbol.toStringTag, { value: "Module" }));
const version = "0.3.75";
const packageJson = {
  version
};
function createAPI() {
  const player = SaltfishPlayer.getInstance();
  let isInitializing = false;
  let isInitialized = false;
  let initPromise = null;
  const commandQueue = [];
  const processQueue = async () => {
    if (commandQueue.length === 0) {
      return;
    }
    while (commandQueue.length > 0) {
      const command = commandQueue.shift();
      if (command) {
        try {
          await command();
        } catch (err) {
          error("Error executing queued command:", err);
        }
      }
    }
  };
  const api = {
    init: (token) => {
      if (isInitialized) {
        info("Saltfish already initialized");
        return Promise.resolve();
      }
      if (isInitializing && initPromise) {
        return initPromise;
      }
      const config = typeof token === "string" ? { token } : token;
      const fullConfig = {
        enableAnalytics: true,
        // Default to true
        ...config
      };
      info(`Saltfish initialized: analytics=${fullConfig.enableAnalytics}`);
      isInitializing = true;
      initPromise = player.initialize(fullConfig).then(() => {
        isInitialized = true;
        isInitializing = false;
        return processQueue();
      }).catch((error2) => {
        isInitializing = false;
        error2("Saltfish initialization failed:", error2);
        throw error2;
      });
      return initPromise;
    },
    identify: (userId, userData) => {
      if (!isInitialized && isInitializing) {
        commandQueue.push(async () => {
          player.identifyUser(userId, userData);
        });
        return;
      }
      player.identifyUser(userId, userData);
    },
    identifyAnonymous: (userData) => {
      if (!isInitialized && isInitializing) {
        commandQueue.push(async () => {
          player.identifyAnonymous(userData);
        });
        return;
      }
      player.identifyAnonymous(userData);
    },
    startPlaylist: (playlistId, options) => {
      if (!isInitialized && isInitializing) {
        return new Promise((resolve, reject) => {
          commandQueue.push(async () => {
            try {
              await player.startPlaylist(playlistId, options);
              resolve();
            } catch (error2) {
              reject(error2);
            }
          });
          if (initPromise) {
            initPromise.catch(reject);
          }
        });
      }
      return player.startPlaylist(playlistId, options);
    },
    on: (eventName, listener) => {
      player.on(eventName, listener);
    },
    off: (eventName, listener) => {
      return player.off(eventName, listener);
    },
    resetPlaylist: () => {
      if (!isInitialized && !isInitializing) {
        warn("Cannot reset playlist - Saltfish not initialized");
        return;
      }
      if (!isInitialized && isInitializing) {
        commandQueue.push(async () => {
          player.resetPlaylist();
        });
        return;
      }
      player.resetPlaylist();
    },
    destroy: () => {
      if (!isInitialized && !isInitializing) {
        warn("Cannot destroy - Saltfish not initialized");
        return;
      }
      if (!isInitializing) {
        isInitialized = false;
        isInitializing = false;
        initPromise = null;
        commandQueue.length = 0;
      }
      player.destroy();
      isInitialized = false;
      isInitializing = false;
      initPromise = null;
    },
    getSessionId: () => {
      return player.getSessionId();
    },
    getRunId: () => {
      return player.getRunId();
    },
    version: () => {
      return packageJson.version;
    }
  };
  api.__dev__ = {
    getDeviceInfo: () => {
      return DeviceDetector.getDeviceInfo();
    }
  };
  return api;
}
const saltfish = createAPI();
if (typeof window !== "undefined" && typeof document !== "undefined") {
  if (!document.querySelector('meta[name="saltfish-player-loaded"]')) {
    const meta = document.createElement("meta");
    meta.name = "saltfish-player-loaded";
    meta.content = "true";
    document.head.appendChild(meta);
  }
}
if (typeof window !== "undefined") {
  window.saltfish = saltfish;
}
export {
  saltfish as default
};