/**
 * OTS Signage Modern Theme - Client-Side Utilities
 * Sidebar toggle, dropdown menus, and UI interactions
 */

(function() {
  'use strict';

  // Apply saved or system-preferred theme as early as possible to avoid
  // a flash from dark -> light when navigating between pages.
  (function() {
    try {
      var stored = localStorage.getItem('ots-theme-mode');
      var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
      var initial = stored || (prefersLight ? 'light' : 'light');
      if (initial === 'light') {
        document.documentElement.classList.add('ots-light-mode');
        if (document.body) document.body.classList.add('ots-light-mode');
      } else {
        document.documentElement.classList.remove('ots-light-mode');
        if (document.body) document.body.classList.remove('ots-light-mode');
      }
    } catch (err) {
      // ignore failures (e.g. localStorage unavailable)
    }
  })();

  const STORAGE_KEYS = {
    sidebarCollapsed: 'otsTheme:sidebarCollapsed'
  };

  /**
   * Sidebar width is now handled purely by CSS classes (.ots-sidebar-collapsed).
   * This function is kept as a no-op for backward compatibility.
   */
  function updateSidebarWidth() {
    const sidebar = document.querySelector('.ots-sidebar');
    if (!sidebar) return;
    const w = sidebar.offsetWidth;
    document.documentElement.style.setProperty('--ots-sidebar-actual-width', w + 'px');
  }

  /**
   * Measure the sidebar header bottom and set the top padding of the nav list
   * so nav items always begin below the header (logo + buttons).
   */
  function updateSidebarNavOffset() {
    /* No-op: sidebar uses flex-direction:column so the header and
       nav content are separate flex children that never overlap.
       Previously this set padding-top:~72px which created a huge gap. */
    var nav = document.querySelector('.ots-sidebar .sidebar-nav, .ots-sidebar .ots-sidebar-nav');
    if (nav) {
      try { nav.style.removeProperty('padding-top'); } catch(e) { nav.style.paddingTop = ''; }
    }
  }

  /**
   * Detect whether the playlist/layout editor modal is open and toggle
   * body.ots-playlist-editor-active accordingly.  Because the editor is
   * loaded via AJAX into #editor-container, a one-shot check at page-load
   * is not enough – we use a MutationObserver that watches for DOM changes.
   */
  function updatePlaylistEditorBackground() {
    var isActive = !!document.querySelector('.editor-modal, #playlist-editor, #layout-editor');
    document.body.classList.toggle('ots-playlist-editor-active', isActive);
  }

  /* Start a MutationObserver that fires updatePlaylistEditorBackground
     whenever children are added to or removed from the page. */
  (function initEditorObserver() {
    // Run once immediately
    updatePlaylistEditorBackground();

    var target = document.body;
    if (!target) return;

    var editorObs = new MutationObserver(function() {
      updatePlaylistEditorBackground();
    });
    editorObs.observe(target, { childList: true, subtree: true });

    // Store reference for cleanup
    window._otsEditorObs = editorObs;
  })();

    /**
     * DISABLED: Cleanup function to remove inline styles that were forcing incorrect margins
     * The sidebar layout is now controlled entirely by CSS variables and margin-left.
     */
    function updateSidebarGap() {
      // This function is intentionally left minimal.
      // Spacing is now handled by CSS: .ots-main { margin-left: var(--ots-sidebar-width) }
      // Removing any inline margin-left or padding-left that may have been set previously
      const targets = [
        document.getElementById('page-wrapper'),
        document.querySelector('.ots-main'),
        document.getElementById('content-wrapper'),
        document.querySelector('#content')
      ].filter(Boolean);

      targets.forEach(pageWrapper => {
        try {
          pageWrapper.style.removeProperty('margin-left');
          pageWrapper.style.removeProperty('padding-left');
        } catch (err) {
          pageWrapper.style.marginLeft = '';
          pageWrapper.style.paddingLeft = '';
        }
        // Also remove from common child wrappers
        try {
          const inner = pageWrapper.querySelector('.page-content') || pageWrapper.querySelector('.ots-content') || pageWrapper.querySelector('.container');
          if (inner) {
            inner.style.removeProperty('padding-left');
          }
        } catch (err) {}
      });
    }

  function debounce(fn, wait) {
    let t;
    return function () {
      clearTimeout(t);
      t = setTimeout(() => fn.apply(this, arguments), wait);
    };
  }

  /**
   * Reflect sidebar open/collapsed state on the document body
   */
  function updateSidebarStateClass() {
    const sidebar = document.querySelector('.ots-sidebar');
    if (!sidebar) return;
    const body = document.body;
    const isCollapsed = sidebar.classList.contains('collapsed');
    if (!isCollapsed) {
      body.classList.add('ots-sidebar-open');
    } else {
      body.classList.remove('ots-sidebar-open');
    }
  }

  /**
   * Initialize sidebar toggle functionality
   */
  function initSidebarToggle() {
    const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
    const sidebar = document.querySelector('.ots-sidebar');
    const collapseBtn = document.querySelector('.sidebar-collapse-btn-visible');
    const expandBtn = document.querySelector('.sidebar-expand-btn');
    const body = document.body;

    if (!sidebar) return;

    // Mobile-aware toggle: add backdrop, aria-expanded, and focus management
    if (toggleBtn) {
      let lastFocus = null;
      function ensureBackdrop() {
        let bd = document.querySelector('.ots-backdrop');
        if (!bd) {
          bd = document.createElement('div');
          bd.className = 'ots-backdrop';
          bd.addEventListener('click', function() {
            sidebar.classList.remove('active');
            bd.classList.remove('show');
            toggleBtn.setAttribute('aria-expanded', 'false');
            if (lastFocus) lastFocus.focus();
          });
          document.body.appendChild(bd);
        }
        return bd;
      }

      toggleBtn.setAttribute('role', 'button');
      toggleBtn.setAttribute('aria-controls', 'ots-sidebar');
      toggleBtn.setAttribute('aria-expanded', 'false');

      toggleBtn.addEventListener('click', function(e) {
        e.preventDefault();
        const isNowActive = !sidebar.classList.contains('active');
        sidebar.classList.toggle('active');
        // On small screens show backdrop and manage focus
        if (window.innerWidth <= 768) {
          const bd = ensureBackdrop();
          if (isNowActive) {
            bd.classList.add('show');
            toggleBtn.setAttribute('aria-expanded', 'true');
            lastFocus = document.activeElement;
            const firstFocusable = sidebar.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
            if (firstFocusable) firstFocusable.focus(); else { sidebar.setAttribute('tabindex', '-1'); sidebar.focus(); }
            document.addEventListener('keydown', escHandler);
          } else {
            bd.classList.remove('show');
            toggleBtn.setAttribute('aria-expanded', 'false');
            if (lastFocus) lastFocus.focus();
            document.removeEventListener('keydown', escHandler);
          }
        }
        updateSidebarStateClass();
      });

      function escHandler(e) {
        if (e.key === 'Escape' || e.key === 'Esc') {
          const bd = document.querySelector('.ots-backdrop');
          if (sidebar.classList.contains('active')) {
            sidebar.classList.remove('active');
            if (bd) bd.classList.remove('show');
            toggleBtn.setAttribute('aria-expanded', 'false');
            if (lastFocus) lastFocus.focus();
            document.removeEventListener('keydown', escHandler);
          }
        }
      }
    }

    if (collapseBtn) {
      let isCollapsed = false;
      try { isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true'; } catch(e) {}
      if (isCollapsed) {
        sidebar.classList.add('collapsed');
        body.classList.add('ots-sidebar-collapsed');
        document.documentElement.classList.add('ots-sidebar-collapsed');
        updateSidebarStateClass();
        // updateSidebarGap() disabled - use CSS variables instead
      }

      collapseBtn.addEventListener('click', function(e) {
        e.preventDefault();
        const nowCollapsed = !sidebar.classList.contains('collapsed');
        sidebar.classList.toggle('collapsed');
        body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
        document.documentElement.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
        try { localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false'); } catch(e) {}
        syncSubmenuDisplayForState(nowCollapsed);
        updateSidebarNavOffset();
        updateSidebarStateClass();
        // Update measured width immediately and again after CSS transition
        updateSidebarWidth();
        setTimeout(updateSidebarWidth, 250);
      });
    }

    if (expandBtn) {
      expandBtn.addEventListener('click', function(e) {
        e.preventDefault();
        sidebar.classList.remove('collapsed');
        body.classList.remove('ots-sidebar-collapsed');
        document.documentElement.classList.remove('ots-sidebar-collapsed');
        try { localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false'); } catch(e) {}
        syncSubmenuDisplayForState(false);
        updateSidebarNavOffset();
        updateSidebarStateClass();
        // Update measured width immediately and again after CSS transition
        updateSidebarWidth();
        setTimeout(updateSidebarWidth, 250);
      });
    }

    // Close sidebar when clicking outside on mobile
    document.addEventListener('click', function(e) {
      if (window.innerWidth <= 768) {
        const isClickInsideSidebar = sidebar.contains(e.target);
        const isClickOnToggle = toggleBtn && toggleBtn.contains(e.target);
        
        if (!isClickInsideSidebar && !isClickOnToggle && sidebar.classList.contains('active')) {
          sidebar.classList.remove('active');
          updateSidebarStateClass();
        }
      }
    });

    // Ensure initial state class is set
    updateSidebarStateClass();
  }

  /**
   * Build flyout headers for each sidebar-submenu.
   * Pulls the icon class(es) and label from the parent group toggle
   * and injects a styled header <li> at the top of the submenu.
   * Idempotent — skips submenus that already have a header.
   */
  function buildFlyoutHeaders() {
    var groups = document.querySelectorAll('.sidebar-group');
    groups.forEach(function(group) {
      var submenu = group.querySelector('.sidebar-submenu');
      if (!submenu) return;
      if (submenu.querySelector('.flyout-header')) return;

      var toggle = group.querySelector('.sidebar-group-toggle');
      if (!toggle) return;

      var iconEl = toggle.querySelector('.ots-nav-icon');
      var textEl = toggle.querySelector('.ots-nav-text');
      if (!textEl) return;

      var label = textEl.textContent.trim();

      var header = document.createElement('li');
      header.className = 'flyout-header';
      header.setAttribute('aria-hidden', 'true');

      if (iconEl) {
        var icon = document.createElement('span');
        icon.className = iconEl.className;
        icon.classList.add('flyout-header-icon');
        icon.setAttribute('aria-hidden', 'true');
        header.appendChild(icon);
      }

      var text = document.createElement('span');
      text.className = 'flyout-header-text';
      text.textContent = label;
      header.appendChild(text);

      submenu.insertBefore(header, submenu.firstChild);
    });
  }

  /**
   * When toggling between collapsed/expanded, sync all submenu inline
   * display styles so that:
   *  - Collapsed: no inline display → CSS :hover handles flyouts
   *  - Expanded:  inline display block/none based on is-open state
   */
  function syncSubmenuDisplayForState(isCollapsed) {
    var groups = document.querySelectorAll('.sidebar-group');
    groups.forEach(function(group) {
      var submenu = group.querySelector('.sidebar-submenu');
      if (!submenu) return;
      if (isCollapsed) {
        submenu.style.removeProperty('display');
      } else {
        var isOpen = group.classList.contains('is-open');
        submenu.style.display = isOpen ? 'block' : 'none';
      }
    });
  }

  /**
   * Initialize sidebar section collapse/expand functionality
   */
  function initSidebarSectionToggles() {
    const groupToggles = document.querySelectorAll('.sidebar-group-toggle');

    groupToggles.forEach(toggle => {
      const group = toggle.closest('.sidebar-group');
      const submenu = group ? group.querySelector('.sidebar-submenu') : null;
      if (submenu) {
        const isOpen = group.classList.contains('is-open');
        const sidebarEl = document.querySelector('.ots-sidebar');
        const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
        if (!isCollapsed) {
          submenu.style.display = isOpen ? 'block' : 'none';
        } else {
          submenu.style.removeProperty('display');
        }
        toggle.setAttribute('aria-expanded', isOpen.toString());
      }

      toggle.addEventListener('click', function(e) {
        e.preventDefault();

        const group = toggle.closest('.sidebar-group');
        const submenu = group ? group.querySelector('.sidebar-submenu') : null;
        if (!submenu) return;

        const sidebarEl = document.querySelector('.ots-sidebar');
        const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
        if (isCollapsed) return;

        const isOpen = group.classList.contains('is-open');
        group.classList.toggle('is-open', !isOpen);
        toggle.setAttribute('aria-expanded', (!isOpen).toString());
        submenu.style.display = isOpen ? 'none' : 'block';
        requestAnimationFrame(updateSidebarWidth);
      });
    });
  }

  /**
   * Arrow key navigation within sidebar submenus.
   * Up/Down moves between links, Escape collapses the group.
   */
  function initSidebarKeyboardNav() {
    var nav = document.querySelector('.ots-sidebar nav, .ots-sidebar [role="navigation"]');
    if (!nav) return;

    nav.addEventListener('keydown', function(e) {
      if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Escape') return;

      var submenu = e.target.closest('.sidebar-submenu');
      if (!submenu) return;

      var links = submenu.querySelectorAll('a:not([disabled])');
      if (!links.length) return;

      if (e.key === 'Escape') {
        e.preventDefault();
        var group = submenu.closest('.sidebar-group');
        var toggle = group ? group.querySelector('.sidebar-group-toggle') : null;
        if (toggle) toggle.click();
        if (toggle) toggle.focus();
        return;
      }

      e.preventDefault();
      var idx = Array.prototype.indexOf.call(links, e.target);
      if (e.key === 'ArrowDown') {
        idx = idx < links.length - 1 ? idx + 1 : 0;
      } else {
        idx = idx > 0 ? idx - 1 : links.length - 1;
      }
      links[idx].focus();
    });
  }

  /**
   * Initialize dropdown menus
   */
  function initDropdowns() {
    // Only handle the user-menu dropdown.
    // Let Bootstrap handle topbar nav dropdowns (Schedule, Design, etc.) natively
    // so that links like Dayparting navigate normally.
    var userToggle = document.querySelector('#navbarUserMenu');
    if (!userToggle) return;

    var userDropdown = userToggle.closest('.dropdown');
    if (!userDropdown) return;

    var userMenu = userDropdown.querySelector('.dropdown-menu');
    if (!userMenu) return;

    // ── Neutralize Bootstrap ──────────────────────────────────────────
    // Remove data-toggle so Bootstrap's delegated handler never fires.
    userToggle.removeAttribute('data-toggle');
    try {
      var jq = window.jQuery || window.$;
      if (jq) {
        jq(userToggle).off('.bs.dropdown');
        jq(userDropdown).off('.bs.dropdown');
      }
    } catch (e) {}

    // ── Move menu to <body> ONCE and leave it there ───────────────────
    // This escapes any overflow:hidden ancestors permanently.
    // We toggle visibility via the .ots-user-menu-open class (no DOM moves).
    document.body.appendChild(userMenu);
    // Mark it so the MutationObserver in observeAndFloatMenus() skips it
    userMenu.setAttribute('data-ots-floating', 'permanent');
    userMenu.setAttribute('data-ots-floating-obs', '1');
    // Start hidden
    userMenu.classList.add('ots-user-menu-body');
    userMenu.classList.remove('show', 'ots-floating-menu');

    function positionMenu() {
      var rect = userToggle.getBoundingClientRect();
      var menuWidth = userMenu.offsetWidth || 220;
      var spaceRight = window.innerWidth - rect.right;

      // Vertically: below the avatar
      userMenu.style.top = Math.round(rect.bottom + 6) + 'px';

      // Horizontally: align right edge to avatar right edge,
      // but fall back to left-aligned if not enough space
      if (spaceRight >= menuWidth) {
        userMenu.style.left = Math.round(rect.left) + 'px';
        userMenu.style.right = 'auto';
      } else {
        userMenu.style.left = 'auto';
        userMenu.style.right = Math.round(window.innerWidth - rect.right) + 'px';
      }
    }

    function openUserMenu() {
      userDropdown.classList.remove('show');
      userMenu.classList.remove('show');
      positionMenu();
      userMenu.classList.add('ots-user-menu-open');
      userDropdown.classList.add('active');
    }

    function closeUserMenu() {
      userMenu.classList.remove('ots-user-menu-open');
      userDropdown.classList.remove('active', 'show');
      userMenu.classList.remove('show');
    }

    // ── Click handler on the toggle element itself ─────────────────────
    userToggle.addEventListener('click', function(e) {
      e.preventDefault();
      e.stopPropagation();

      if (userMenu.classList.contains('ots-user-menu-open')) {
        closeUserMenu();
      } else {
        openUserMenu();
      }
    });

    // ── Close when clicking outside ───────────────────────────────────
    document.addEventListener('click', function(e) {
      if (!userMenu.classList.contains('ots-user-menu-open')) return;
      if (userToggle.contains(e.target)) return;
      if (userMenu.contains(e.target)) return;
      closeUserMenu();
    });

    // ── Close when a modal opens ─────────────────────────────────────
    // Menu items like Preferences / Edit Profile trigger Bootstrap modals
    // via .XiboFormButton — close the dropdown as soon as any modal shows.
    document.addEventListener('show.bs.modal', function() { closeUserMenu(); }, true);
    try {
      var jq = window.jQuery || window.$;
      if (jq) {
        jq(document).on('show.bs.modal', function() { closeUserMenu(); });
      }
    } catch (e) {}

    // ── Reposition on scroll/resize ───────────────────────────────────
    window.addEventListener('scroll', function() {
      if (userMenu.classList.contains('ots-user-menu-open')) positionMenu();
    }, true);
    window.addEventListener('resize', function() {
      if (userMenu.classList.contains('ots-user-menu-open')) positionMenu();
    });
  }

  /**
   * Float a menu element into document.body so it can escape overflowed parents.
   * Adds `.ots-floating-menu` and positions absolutely based on the trigger rect.
   */
  function floatMenu(menuEl, triggerEl) {
    if (!menuEl || !triggerEl) return;
    // Skip if already floating or permanently managed by initDropdowns
    var floatAttr = menuEl.getAttribute('data-ots-floating');
    if (floatAttr === '1' || floatAttr === 'permanent') return;
    try {
      // Remember original parent and next sibling so we can restore later
      menuEl._otsOriginalParent = menuEl.parentNode || null;
      menuEl._otsOriginalNext = menuEl.nextSibling || null;
      menuEl.setAttribute('data-ots-floating', '1');
      menuEl.classList.add('ots-floating-menu');
      // Append to body
      document.body.appendChild(menuEl);

      const rect = triggerEl.getBoundingClientRect();
      // Default placement below trigger, align to left edge
      const top = Math.max(8, Math.round(rect.bottom + window.scrollY + 6));
      const left = Math.max(8, Math.round(rect.left + window.scrollX));

      // Use fixed positioning so the menu floats above all stacking contexts
      // Use fixed positioning so the menu floats above all stacking contexts
      try {
        menuEl.style.setProperty('position', 'fixed', 'important');
        menuEl.style.setProperty('top', Math.max(6, Math.round(rect.bottom + 6)) + 'px', 'important');
        menuEl.style.setProperty('left', left + 'px', 'important');
        // Use the maximum reasonable z-index to ensure it appears on top
        menuEl.style.setProperty('z-index', '2147483647', 'important');
        // Ensure transforms won't clip rendering
        menuEl.style.setProperty('transform', 'none', 'important');
        menuEl.style.setProperty('min-width', (rect.width) + 'px', 'important');
        menuEl.style.setProperty('pointer-events', 'auto', 'important');
      } catch (err) {
        // fallback to non-important inline style
        menuEl.style.position = 'fixed';
        menuEl.style.top = Math.max(6, Math.round(rect.bottom + 6)) + 'px';
        menuEl.style.left = left + 'px';
        menuEl.style.zIndex = '2147483647';
        menuEl.style.transform = 'none';
        menuEl.style.minWidth = (rect.width) + 'px';
        menuEl.style.pointerEvents = 'auto';
      }

      // Reposition on scroll/resize while open
      const reposition = function() {
        if (menuEl.getAttribute('data-ots-floating') !== '1') return;
        const r = triggerEl.getBoundingClientRect();
        // For fixed positioning we only need viewport coords
        menuEl.style.top = Math.max(6, Math.round(r.bottom + 6)) + 'px';
        menuEl.style.left = Math.max(6, Math.round(r.left)) + 'px';
      };
      menuEl._otsReposition = reposition;
      window.addEventListener('scroll', reposition, true);
      window.addEventListener('resize', reposition);

      // Guard: some libraries move/drop menus. Keep a short-lived guard that
      // re-attaches the menu to body and re-applies important styles while open.
      let guardCount = 0;
      const guard = setInterval(function() {
        try {
          if (menuEl.getAttribute('data-ots-floating') !== '1') {
            clearInterval(guard);
            return;
          }
          // If parent moved, re-append to body
          if (menuEl.parentNode !== document.body) document.body.appendChild(menuEl);
          // Re-ensure important styles
          menuEl.style.setProperty('z-index', '2147483647', 'important');
          menuEl.style.setProperty('position', 'fixed', 'important');
        } catch (err) {}
        guardCount += 1;
        if (guardCount > 120) {
          clearInterval(guard);
        }
      }, 100);
      menuEl._otsGuard = guard;
    } catch (err) {
      console.warn('[OTS] floatMenu failed', err);
    }
  }

  function unfloatMenu(menuEl) {
    if (!menuEl) return;
    var floatAttr = menuEl.getAttribute('data-ots-floating');
    // Skip permanently managed menus and menus that aren't floating
    if (floatAttr === 'permanent' || floatAttr !== '1') return;
    try {
      menuEl.removeAttribute('data-ots-floating');
      menuEl.classList.remove('ots-floating-menu');
      // Clear ALL inline styles that floatMenu() may have set (including
      // transform which was previously missed, causing stale styles).
      menuEl.style.position = '';
      menuEl.style.top = '';
      menuEl.style.left = '';
      menuEl.style.zIndex = '';
      menuEl.style.minWidth = '';
      menuEl.style.pointerEvents = '';
      menuEl.style.transform = '';
      menuEl.style.visibility = '';
      menuEl.style.display = '';
      menuEl.style.opacity = '';
      // Remove reposition listeners
      if (menuEl._otsReposition) {
        window.removeEventListener('scroll', menuEl._otsReposition, true);
        window.removeEventListener('resize', menuEl._otsReposition);
        delete menuEl._otsReposition;
      }
      // Attempt to restore the original parent and insertion point
      try {
        if (menuEl._otsOriginalParent) {
          if (menuEl._otsOriginalNext && menuEl._otsOriginalNext.parentNode === menuEl._otsOriginalParent) {
            menuEl._otsOriginalParent.insertBefore(menuEl, menuEl._otsOriginalNext);
          } else {
            menuEl._otsOriginalParent.appendChild(menuEl);
          }
          delete menuEl._otsOriginalParent;
          delete menuEl._otsOriginalNext;
        } else {
          // fallback: append to body (leave it there)
          document.body.appendChild(menuEl);
        }
      } catch (err) {
        document.body.appendChild(menuEl);
      }
    } catch (err) {
      console.warn('[OTS] unfloatMenu failed', err);
    }
  }

  /**
   * Observe document for dynamically added dropdown menus and float them when necessary.
   */
  function observeAndFloatMenus() {
    try {
      const mo = new MutationObserver(function(muts) {
        muts.forEach(function(m) {
          (m.addedNodes || []).forEach(function(node) {
            try {
              if (!node || node.nodeType !== 1) return;
              // If the node itself is a dropdown menu
              if (node.classList && node.classList.contains('dropdown-menu')) {
                attachIfNeeded(node);
              }
              // Or contains dropdown menus
              const menus = node.querySelectorAll && node.querySelectorAll('.dropdown-menu');
              if (menus && menus.length) {
                menus.forEach(attachIfNeeded);
              }
            } catch (err) {}
          });
        });
      });
      mo.observe(document.body, { childList: true, subtree: true });
      // keep alive for the lifetime of the page
    } catch (err) {
      // ignore
    }

    function attachIfNeeded(menu) {
      try {
        if (!menu || menu.getAttribute('data-ots-floating-obs') === '1') return;
        menu.setAttribute('data-ots-floating-obs', '1');
        // find a reasonable trigger element: aria-labelledby or previous element
        let trigger = null;
        const labelled = menu.getAttribute('aria-labelledby');
        if (labelled) trigger = document.getElementById(labelled);
        if (!trigger) trigger = menu._otsOriginalParent ? menu._otsOriginalParent.querySelector('[data-toggle="dropdown"]') : null;
        if (!trigger) trigger = menu.previousElementSibling || null;
        // If the menu is visible and inside an overflowed ancestor, float it
        const rect = menu.getBoundingClientRect();
        if (rect.width === 0 && rect.height === 0) return; // not rendered yet
        if (isClippedByOverflow(menu) && trigger) {
          floatMenu(menu, trigger);
        }
        // Also watch for when dropdown gets toggled active via class
        const obs = new MutationObserver(function(ms) {
          ms.forEach(function(mm) {
            if (mm.type === 'attributes' && mm.attributeName === 'class') {
              const isActive = menu.classList.contains('show') || menu.parentNode && menu.parentNode.classList.contains('active');
              if (isActive && trigger) floatMenu(menu, trigger);
              if (!isActive) unfloatMenu(menu);
            }
          });
        });
        obs.observe(menu, { attributes: true, attributeFilter: ['class'] });
      } catch (err) {}
    }

    function isClippedByOverflow(el) {
      let p = el.parentElement;
      while (p && p !== document.body) {
        const s = window.getComputedStyle(p);
        if (/(hidden|auto|scroll)/.test(s.overflow + s.overflowY + s.overflowX)) {
          const r = el.getBoundingClientRect();
          const pr = p.getBoundingClientRect();
          // if element overflows parent's rect then it's clipped
          if (r.bottom > pr.bottom || r.top < pr.top || r.left < pr.left || r.right > pr.right) return true;
        }
        p = p.parentElement;
      }
      return false;
    }
  }

  /**
   * Force common menu classes to the top by moving them to body and keeping them there.
   * This is the most aggressive approach to ensure menus are never clipped.
   */
  function forceTopMenus() {
    const selectors = ['.dropdown-menu', '.ots-notif-menu', '.ots-user-menu', '.context-menu', '.row-menu', '.rowMenu', '.menu-popover'];

    function moveToBody(el) {
      try {
        if (!el || el.getAttribute('data-ots-moved-to-body') === '1') return;
        
        // Store original parent info
        el._otsOriginalParent = el.parentElement;
        el._otsOriginalNextSibling = el.nextElementSibling;
        
        el.setAttribute('data-ots-moved-to-body', '1');
        
        // Force fixed positioning with maximum z-index
        el.style.setProperty('position', 'fixed', 'important');
        el.style.setProperty('z-index', '2147483647', 'important');
        el.style.setProperty('transform', 'none', 'important');
        el.style.setProperty('pointer-events', 'auto', 'important');
        el.style.setProperty('visibility', 'visible', 'important');
        el.style.setProperty('display', 'block', 'important');
        el.style.setProperty('opacity', '1', 'important');
        el.style.setProperty('clip-path', 'none', 'important');
        
        // Move to body if not already there
        if (el.parentElement !== document.body) {
          document.body.appendChild(el);
        }
      } catch (err) {
        console.warn('[OTS] moveToBody failed', err);
      }
    }

    function applyMenuStyles(el) {
      try {
        if (!el) return;
        el.style.setProperty('position', 'fixed', 'important');
        el.style.setProperty('z-index', '2147483647', 'important');
        el.style.setProperty('transform', 'none', 'important');
        el.style.setProperty('pointer-events', 'auto', 'important');
      } catch (err) {}
    }

    // Apply to existing menus immediately
    selectors.forEach(sel => {
      document.querySelectorAll(sel).forEach(el => {
        moveToBody(el);
        applyMenuStyles(el);
      });
    });

    // Use a MutationObserver instead of polling to detect new menus
    try {
      var forceMenuObs = new MutationObserver(function(muts) {
        muts.forEach(function(m) {
          (m.addedNodes || []).forEach(function(node) {
            try {
              if (!node || node.nodeType !== 1) return;
              selectors.forEach(function(sel) {
                if (node.matches && node.matches(sel)) {
                  moveToBody(node);
                  applyMenuStyles(node);
                }
                var found = node.querySelectorAll && node.querySelectorAll(sel);
                if (found) found.forEach(function(el) {
                  moveToBody(el);
                  applyMenuStyles(el);
                });
              });
            } catch (err) {}
          });
        });
      });
      forceMenuObs.observe(document.body, { childList: true, subtree: true });
      window._otsForceMenuObs = forceMenuObs;
    } catch (err) {}
  }

  /**
   * Initialize search functionality
   */
  function initSearch() {
    const searchForm = document.querySelector('.topbar-search');
    if (!searchForm) return;

    const input = searchForm.querySelector('.search-input');
    if (input) {
      input.addEventListener('focus', function() {
        searchForm.style.borderColor = 'var(--color-primary)';
      });
      input.addEventListener('blur', function() {
        searchForm.style.borderColor = 'var(--color-border)';
      });
    }
  }

  /**
   * Initialize page specific interactions
   */
  function initPageInteractions() {
    // Displays page - folder selection
    const folderItems = document.querySelectorAll('.folder-item');
    folderItems.forEach(item => {
      item.addEventListener('click', function() {
        folderItems.forEach(f => f.classList.remove('active'));
        this.classList.add('active');
      });
    });

    // Filter collapse toggle
    const filterCollapseBtn = document.querySelector('#ots-filter-collapse-btn');
    const filterContent = document.querySelector('#ots-filter-content');
    
    if (filterCollapseBtn && filterContent) {
      const storageKey = `ots-filter-collapsed:${window.location.pathname}`;
      let isCollapsed = filterContent.classList.contains('collapsed');
      
      filterCollapseBtn.addEventListener('click', function() {
        isCollapsed = !isCollapsed;
        filterContent.classList.toggle('collapsed', isCollapsed);
        
        // Rotate icon
        const icon = filterCollapseBtn.querySelector('i');
        icon.classList.toggle('fa-chevron-up', !isCollapsed);
        icon.classList.toggle('fa-chevron-down', isCollapsed);
        
        // Save preference to localStorage
        try { localStorage.setItem(storageKey, isCollapsed); } catch(e) {}
      });
      
      // Restore saved preference (overrides HTML default)
      let savedState = null;
      try { savedState = localStorage.getItem(storageKey); } catch(e) {}
      if (savedState === 'true') {
        isCollapsed = true;
        filterContent.classList.add('collapsed');
        const icon = filterCollapseBtn.querySelector('i');
        icon.classList.remove('fa-chevron-up');
        icon.classList.add('fa-chevron-down');
      } else if (savedState === 'false') {
        isCollapsed = false;
        filterContent.classList.remove('collapsed');
        const icon = filterCollapseBtn.querySelector('i');
        icon.classList.remove('fa-chevron-down');
        icon.classList.add('fa-chevron-up');
      }
    }

    // Displays page - folder tree toggle layout
    const folderToggleBtn = document.querySelector('#folder-tree-select-folder-button');
    const folderContainer = document.querySelector('.grid-with-folders-container');
    const folderTree = document.querySelector('#grid-folder-filter');

    if (folderToggleBtn && folderContainer && folderTree) {
      let debounceTimeout;
      
      const syncFolderLayout = () => {
        requestAnimationFrame(() => {
          // Batch all reads first
          const computedStyle = window.getComputedStyle(folderTree);
          const display = computedStyle.display;
          const visibility = computedStyle.visibility;
          const height = folderTree.offsetHeight;
          const isHidden = display === 'none' || visibility === 'hidden' || height === 0;

          // Then write
          folderContainer.classList.toggle('ots-folder-collapsed', !!isHidden);
        });
      };
      
      const debouncedSync = () => {
        clearTimeout(debounceTimeout);
        debounceTimeout = setTimeout(syncFolderLayout, 50);
      };

      // Watch for style/class changes on folderTree (let Xibo's code run first)
      const treeObserver = new MutationObserver(() => {
        debouncedSync();
      });
      treeObserver.observe(folderTree, { 
        attributes: true, 
        attributeFilter: ['style', 'class']
      });
      
      // Initial sync
      syncFolderLayout();
      
      // Monitor the folder tree's parent for display changes
      const parentObserver = new MutationObserver(debouncedSync);
      const treeParent = folderTree.parentElement;
      if (treeParent) {
        parentObserver.observe(treeParent, { 
          childList: false,
          attributes: true,
          subtree: false
        });
      }
    }

    // Media page - item selection
    const mediaItems = document.querySelectorAll('.media-item');
    mediaItems.forEach(item => {
      item.addEventListener('click', function() {
        this.style.opacity = '0.7';
        setTimeout(() => this.style.opacity = '1', 200);
      });
    });
  }

  /**
   * Make sidebar responsive
   */
  function makeResponsive() {
    const sidebar = document.querySelector('.ots-sidebar');
    const main = document.querySelector('.ots-main');
    
    if (!sidebar) return;
    
    // Add toggle button for mobile
    if (window.innerWidth <= 768) {
      sidebar.classList.add('mobile');
    }
    
    window.addEventListener('resize', function() {
      if (window.innerWidth > 768) {
        sidebar.classList.remove('mobile', 'active');
      } else {
        sidebar.classList.add('mobile');
      }
      // updateSidebarGap() disabled - use CSS variables instead
    });
  }

  /**
   * Prevent Chart.js errors when chart elements are missing
   */
  function initChartSafeguard() {
    if (!window.Chart) return;

    if (typeof window.Chart.acquireContext === 'function') {
      window.Chart.acquireContext = function(item) {
        if (!item) return null;

        const candidate = item.length ? item[0] : item;
        if (candidate && typeof candidate.getContext === 'function') {
          return candidate.getContext('2d');
        }

        return null;
      };
      return;
    }

    if (window.Chart.prototype && typeof window.Chart.prototype.acquireContext === 'function') {
      window.Chart.prototype.acquireContext = function(item) {
        if (!item) return null;

        const candidate = item.length ? item[0] : item;
        if (candidate && typeof candidate.getContext === 'function') {
          return candidate.getContext('2d');
        }

        return null;
      };
    }
  }

  /**
   * Enhance tables: wrap in card, add per-table search box, client-side filtering
   * Non-destructive: skips tables already enhanced
   */
  function enhanceTables() {
    const selector = '.ots-content table, .content table, .container table, .card table, table';
    const tables = Array.from(document.querySelectorAll(selector));
    let counter = 0;

    tables.forEach(table => {
      // only enhance tables that have a thead and tbody
      if (!table || table.classList.contains('modern-table')) return;
      if (!table.querySelector('thead') || !table.querySelector('tbody')) return;

      counter += 1;
      table.classList.add('modern-table');

      // Build wrapper structure
      const wrapper = document.createElement('div');
      wrapper.className = 'modern-table-card';

      const controls = document.createElement('div');
      controls.className = 'table-controls';

      const input = document.createElement('input');
      input.type = 'search';
      input.placeholder = 'Search…';
      input.className = 'table-search-input';
      input.setAttribute('aria-label', 'Table search');

      controls.appendChild(input);

      const tableWrapper = document.createElement('div');
      tableWrapper.className = 'table-wrapper';
      tableWrapper.style.overflow = 'auto';

      // Insert wrapper into DOM in place of the table
      const parent = table.parentNode;
      parent.replaceChild(wrapper, table);
      wrapper.appendChild(controls);
      wrapper.appendChild(tableWrapper);
      tableWrapper.appendChild(table);

      // Simple, light-weight search filtering for this table only
      input.addEventListener('input', function (e) {
        const term = (e.target.value || '').toLowerCase();
        table.querySelectorAll('tbody tr').forEach(tr => {
          const text = tr.textContent.toLowerCase();
          tr.style.display = term === '' || text.includes(term) ? '' : 'none';
        });
      });
    });
  }

  /**
   * Initialize DataTables for enhanced behavior when available.
   * Falls back gracefully if DataTables or jQuery are not present.
   */
  function initDataTables() {
    if (!window.jQuery) return;
    const $ = window.jQuery;
    if (!$.fn || !$.fn.dataTable) return;

    // ── Override global DataTables template for modern layout ──
    // Only if Xibo hasn't already set a custom template
    if (typeof window.dataTablesTemplate === 'string') {
      // Keep Xibo's template but ensure it includes our improvements
    } else if (typeof window.dataTablesTemplate === 'undefined') {
      window.dataTablesTemplate = '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>rt<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>';
    }

    // ── Enhance Xibo DataTables on draw ──
    $(document).on('draw.dt', function(e, settings) {
      try {
        var api = new $.fn.dataTable.Api(settings);
        var wrapper = $(api.table().container());
        var tbody = $(api.table().body());

        // Inject custom empty state when table has no visible rows
        if (api.rows({ search: 'applied' }).count() === 0) {
          var existing = wrapper.find('.ots-table-empty-state');
          if (!existing.length) {
            var emptyHtml = '<div class="ots-table-empty-state">' +
              '<div class="ots-empty-icon"><i class="fa fa-inbox"></i></div>' +
              '<div class="ots-empty-text">No results found</div>' +
              '<div class="ots-empty-hint">Try adjusting your filters or search terms</div>' +
              '</div>';
            // Insert after the table, before pagination
            var tableEl = $(api.table().node());
            tableEl.after(emptyHtml);
          }
        } else {
          wrapper.find('.ots-table-empty-state').remove();
        }

        // Apply status badge styling to known status cells
        tbody.find('td .label, td .badge').each(function() {
          var el = $(this);
          if (el.hasClass('ots-badge')) return;
          var text = (el.text() || '').toLowerCase().trim();
          var cls = 'ots-badge ots-badge--neutral';
          if (/online|active|enabled|yes|licensed|authorised/.test(text)) cls = 'ots-badge ots-badge--success';
          else if (/offline|inactive|disabled|no|expired|revoked/.test(text)) cls = 'ots-badge ots-badge--danger';
          else if (/pending|waiting|unknown|checking/.test(text)) cls = 'ots-badge ots-badge--warning';
          else if (/edit|draft|building/.test(text)) cls = 'ots-badge ots-badge--info';
          el.addClass(cls);
        });
      } catch (err) {
        // DataTable enhancement failure is non-critical
      }
    });

    // Skip Xibo-managed grids to avoid double initialization
    if (document.querySelector('.XiboGrid')) return;

    $('.modern-table, table').each(function () {
      try {
        if (this.closest('.XiboGrid')) return;
        if (!$.fn.dataTable.isDataTable(this)) {
          $(this).DataTable({
            responsive: true,
            lengthChange: false,
            pageLength: 10,
            autoWidth: false,
            dom: '<"table-controls"f>rt<"table-meta"ip>',
            language: { search: '' }
          });
        }
      } catch (err) {
        // If initialization fails, ignore and allow fallback enhancer
        console.warn('DataTables init failed for table', this, err);
      }
    });
  }

  /**
   * Initialize light/dark mode toggle
   */
  function initThemeToggle() {
    const themeToggle = document.getElementById('ots-theme-toggle');
    if (!themeToggle) return;

    const storedTheme = localStorage.getItem('ots-theme-mode');
    const prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
    const effectiveTheme = storedTheme || (prefersLight ? 'light' : 'dark');
    const body = document.body;
    const root = document.documentElement;

    // Apply stored theme on page load (apply to both <html> and <body>)
    if (effectiveTheme === 'light') {
      body.classList.add('ots-light-mode');
      root.classList.add('ots-light-mode');
      updateThemeLabel();
    }

    // Toggle on click (keep <html> in sync so :root variables reflect mode)
    themeToggle.addEventListener('click', function(e) {
      e.preventDefault();
      const isLight = body.classList.toggle('ots-light-mode');
      root.classList.toggle('ots-light-mode', isLight);
      try { localStorage.setItem('ots-theme-mode', isLight ? 'light' : 'dark'); } catch(e) {}
      updateThemeLabel();
    });

    function updateThemeLabel() {
      const icon = document.getElementById('ots-theme-icon');
      const label = document.getElementById('ots-theme-label');
      const isLight = body.classList.contains('ots-light-mode');
      if (icon) {
        icon.className = isLight ? 'fa fa-sun-o' : 'fa fa-moon-o';
      }
      if (label) {
        label.textContent = isLight ? 'Light Mode' : 'Dark Mode';
      }
    }
  }

  /**
   * DataTable row action dropdowns — fully managed by OTS theme.
   *
   * Bootstrap 4 + Popper.js positions menus with transform: translate3d(),
   * but the theme CSS sets transform: none !important which breaks that.
   * Detaching the menu to <body> also triggers Bootstrap's hide event.
   *
   * Solution: intercept the click on the toggle button in the capture phase
   * (before Bootstrap sees it), prevent Bootstrap from handling it, and
   * manage show/hide/position entirely ourselves.
   */
  // Module-level reference to row dropdown's closeMenu, set by initRowDropdowns().
  var _closeRowDropdown = null;

  function initRowDropdowns() {
    var TOGGLE_SEL = '.dropdown-menu-container [data-toggle="dropdown"], .btn-group [data-toggle="dropdown"]';
    var activeMenu = null;   // currently open menu element (in <body>)
    var activeParent = null; // original parent (.btn-group / .dropdown-menu-container)
    var activeTrigger = null;

    function openMenu(trigger) {
      closeMenu(); // close any previously open menu first

      var $trigger = $(trigger);
      var $parent = $trigger.closest('.dropdown-menu-container, .btn-group');
      var $menu = $parent.find('.dropdown-menu').first();
      if (!$menu.length) return;

      activeTrigger = trigger;
      activeParent = $parent[0];

      // Snapshot button position while menu is still in the normal DOM
      var btnRect = trigger.getBoundingClientRect();

      // Detach menu and append to body so it escapes overflow:hidden
      $menu.detach().appendTo('body');
      activeMenu = $menu[0];

      // Make visible-but-hidden so we can measure it
      activeMenu.style.cssText = 'display:block !important; visibility:hidden !important; position:fixed !important; transform:none !important;';
      var menuW = $menu.outerWidth() || 180;
      var menuH = $menu.outerHeight() || 200;

      // Compute position: below button, right-aligned to button's right edge
      var top = btnRect.bottom + 2;
      var left = btnRect.right - menuW;

      // Viewport bounds check
      if (left < 8) left = 8;
      if (left + menuW > window.innerWidth - 8) left = window.innerWidth - menuW - 8;
      if (top + menuH > window.innerHeight - 8) {
        top = btnRect.top - menuH - 2; // flip above the button
      }
      if (top < 8) top = 8;

      // Apply final position — every property with !important
      activeMenu.style.cssText = [
        'position:fixed !important',
        'top:' + top + 'px !important',
        'left:' + left + 'px !important',
        'right:auto !important',
        'bottom:auto !important',
        'display:block !important',
        'visibility:visible !important',
        'transform:none !important',
        'will-change:auto !important',
        'margin:0 !important',
        'z-index:2147483647 !important'
      ].join(';') + ';';

      $menu.addClass('ots-row-dropdown show');
      $parent.addClass('show');
    }

    function closeMenu() {
      if (!activeMenu) return;
      var $menu = $(activeMenu);
      var $parent = $(activeParent);

      // Clear all inline styles
      activeMenu.style.cssText = '';
      $menu.removeClass('ots-row-dropdown show');

      // Move menu back to its original parent
      $menu.detach().appendTo($parent);
      $parent.removeClass('show open');

      activeMenu = null;
      activeParent = null;
      activeTrigger = null;
    }

    // Expose closeMenu so closeAllDropdowns() can reach it
    _closeRowDropdown = closeMenu;

    // Intercept clicks in CAPTURE phase — runs BEFORE Bootstrap's handler.
    document.addEventListener('click', function(e) {
      var toggle = e.target.closest(TOGGLE_SEL);
      if (!toggle) {
        // Click was not on a toggle — close any open menu
        // (unless click is inside the open menu itself)
        if (activeMenu && !e.target.closest('.ots-row-dropdown')) {
          closeMenu();
        }
        return;
      }

      // Only handle toggles inside DataTable areas
      var inTable = toggle.closest('.XiboData') || toggle.closest('.XiboGrid')
        || toggle.closest('#datatable-container') || toggle.closest('.dataTables_wrapper');
      if (!inTable) return; // not a row dropdown — let Bootstrap handle it

      // Prevent Bootstrap from handling this dropdown
      e.preventDefault();
      e.stopImmediatePropagation();

      // Toggle behaviour: if same trigger, close; otherwise open
      if (activeTrigger === toggle && activeMenu) {
        closeMenu();
      } else {
        openMenu(toggle);
      }
    }, true); // ← true = capture phase

    // Close on Escape key + arrow navigation within open menus
    document.addEventListener('keydown', function(e) {
      if (e.key === 'Escape' && activeMenu) {
        closeMenu();
        return;
      }

      if (!activeMenu) return;

      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault();
        var items = activeMenu.querySelectorAll('.dropdown-item:not(.disabled):not([disabled])');
        if (!items.length) return;
        var idx = Array.prototype.indexOf.call(items, document.activeElement);
        if (e.key === 'ArrowDown') {
          idx = idx < items.length - 1 ? idx + 1 : 0;
        } else {
          idx = idx > 0 ? idx - 1 : items.length - 1;
        }
        items[idx].focus();
      }
    });

    // Close on any scroll (window or scrollable ancestor)
    window.addEventListener('scroll', function() {
      if (activeMenu) closeMenu();
    }, true);

    // Block Bootstrap's show/hide events for DataTable row dropdowns
    // so it doesn't interfere with our manual management.
    $(document).on('show.bs.dropdown hide.bs.dropdown', function(e) {
      var toggle = e.relatedTarget;
      if (!toggle) return;
      var inTable = toggle.closest('.XiboData') || toggle.closest('.XiboGrid')
        || toggle.closest('#datatable-container') || toggle.closest('.dataTables_wrapper');
      if (inTable) {
        e.preventDefault();
      }
    });
  }

  /**
   * Close every open dropdown / popover on the page.
   * Covers: Bootstrap 4 native dropdowns, OTS custom dropdowns,
   * the user-menu, notification drawer, and DataTable row menus.
   */
  function closeAllDropdowns() {
    try {
      // 0. Close row dropdown managed by initRowDropdowns()
      if (typeof _closeRowDropdown === 'function') _closeRowDropdown();

      // Also force-remove any stray ots-row-dropdown elements left on <body>
      document.querySelectorAll('.ots-row-dropdown').forEach(function(m) {
        m.classList.remove('show', 'ots-row-dropdown');
        m.style.cssText = '';
      });

      // 1. Bootstrap 4 native dropdowns (.show on the wrapper or the menu)
      document.querySelectorAll('.dropdown.show, .btn-group.show').forEach(function(el) {
        el.classList.remove('show');
        var m = el.querySelector('.dropdown-menu.show');
        if (m) m.classList.remove('show');
      });
      document.querySelectorAll('.dropdown-menu.show').forEach(function(m) {
        m.classList.remove('show');
      });

      // 2. OTS custom dropdowns that use .active
      document.querySelectorAll('.dropdown.active, .ots-topbar-action .dropdown.active, .ots-page-actions .dropdown.active').forEach(function(el) {
        el.classList.remove('active');
      });

      // 3. User menu (body-level floating menu)
      var userMenu = document.querySelector('.ots-user-menu-body.ots-user-menu-open');
      if (userMenu) {
        userMenu.classList.remove('ots-user-menu-open');
        var userDropdown = document.querySelector('#navbarUserMenu');
        if (userDropdown) {
          var dd = userDropdown.closest('.dropdown');
          if (dd) dd.classList.remove('active', 'show');
        }
      }

      // 4. DataTable button collections
      document.querySelectorAll('.dt-buttons.active').forEach(function(w) {
        w.classList.remove('active');
      });
      document.querySelectorAll('.dt-button-collection.show').forEach(function(c) {
        c.classList.remove('show');
        c.style.display = 'none';
      });

      // 5. jQuery-level Bootstrap cleanup (if available)
      var jq = window.jQuery || window.$;
      if (jq) {
        jq('.dropdown-toggle[aria-expanded="true"]').attr('aria-expanded', 'false');
        jq('.dropdown-menu.show').removeClass('show');
        jq('.dropdown.show, .btn-group.show').removeClass('show');
      }
    } catch (err) {
      // never let this break the page
    }
  }

  /**
   * Wire up global listeners that trigger closeAllDropdowns().
   * Called once from init().
   */
  function initGlobalDropdownDismiss() {
    // ── Close when a Bootstrap modal / dialog opens ─────────────────
    document.addEventListener('show.bs.modal', closeAllDropdowns, true);
    try {
      var jq = window.jQuery || window.$;
      if (jq) {
        jq(document).on('show.bs.modal', closeAllDropdowns);
        // Xibo opens modals when .XiboFormButton / .XiboAjaxSubmit are clicked
        jq(document).on('click', '.XiboFormButton, .XiboAjaxSubmit, .XiboFormRender', function() {
          closeAllDropdowns();
        });
      }
    } catch (e) {}

    // ── Close when any <a> inside a dropdown is clicked (page nav) ──
    document.addEventListener('click', function(e) {
      var link = e.target.closest('.dropdown-menu a, .ots-topbar a.nav-link, .ots-topbar a.dropdown-item, .XiboFormButton');
      if (link && !e.defaultPrevented) {
        closeAllDropdowns();
      }
    });

    // ── Close on History navigation (Xibo uses pushState for AJAX pages) ──
    window.addEventListener('popstate', closeAllDropdowns);
    // Intercept pushState / replaceState so we catch Xibo's AJAX navigation
    try {
      var origPush = history.pushState;
      var origReplace = history.replaceState;
      history.pushState = function() {
        origPush.apply(this, arguments);
        closeAllDropdowns();
      };
      history.replaceState = function() {
        origReplace.apply(this, arguments);
        closeAllDropdowns();
      };
    } catch (e) {}

    // ── Close when main content area changes (AJAX page swap) ───────
    try {
      var content = document.getElementById('content') || document.querySelector('.ots-content');
      if (content) {
        var contentObs = new MutationObserver(debounce(closeAllDropdowns, 80));
        contentObs.observe(content, { childList: true });
      }
    } catch (e) {}
  }

  /**
   * Initialize all features when DOM is ready
   */
  function init() {
    initSidebarToggle();
    initSidebarSectionToggles();
    initSidebarKeyboardNav();
    buildFlyoutHeaders();
    initThemeToggle();
    initDropdowns();
    initRowDropdowns();
    initGlobalDropdownDismiss();
    initSearch();
    initPageInteractions();
    initDataTables();
    enhanceTables();
    makeResponsive();
    initChartSafeguard();
    updateSidebarWidth();
    updateSidebarNavOffset();
    updatePlaylistEditorBackground();
    // updateSidebarGap() disabled - use CSS variables instead
    initUserProfileQrFix();
    var debouncedUpdate = debounce(function() {
      updateSidebarNavOffset();
      updateSidebarWidth();
      updatePlaylistEditorBackground();
      // updateSidebarGap() disabled - use CSS variables instead
    }, 120);
    window.addEventListener('resize', debouncedUpdate);
  }

  // Wait for DOM to be ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

/**
 * OTS: Enhance all Xibo form modals to match the upload modal design.
 * Runs on every shown.bs.modal event and also exposed as window.otsEnhanceModal()
 * for direct invocation from form callbacks like mediaEditFormOpen.
 */
(function() {
  'use strict';

  var OTS_CLOSE_SVG = '<button type="button" class="ots-upload-close" data-dismiss="modal" aria-label="Close">' +
    '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>' +
    '</button>';

  function enhanceModal(modal) {
    var $m = window.jQuery ? window.jQuery(modal) : null;
    if (!$m || !$m.length) return;

    // Don't re-enhance
    if ($m.data('ots-enhanced')) return;
    $m.data('ots-enhanced', true);

    // Skip the custom upload modal (it has its own styling)
    if ($m.hasClass('ots-upload-modal') || $m.attr('id') === 'ots-upload-modal') return;

    // Add the OTS edit modal class
    $m.addClass('ots-edit-media-modal');

    // Replace the default close button with SVG version
    var $closeBtn = $m.find('.modal-header .close, .modal-header button[data-dismiss="modal"]:not(.ots-upload-close)');
    if ($closeBtn.length) {
      $closeBtn.first().replaceWith(OTS_CLOSE_SVG);
    }
  }

  // Expose globally so page callbacks can invoke it directly
  window.otsEnhanceModal = enhanceModal;

  // Hook into every modal show event
  if (window.jQuery) {
    window.jQuery(document).on('shown.bs.modal', '.modal', function() {
      enhanceModal(this);
    });
  } else {
    document.addEventListener('shown.bs.modal', function(e) {
      var modal = e.target;
      if (modal && modal.classList && modal.classList.contains('modal')) {
        enhanceModal(modal);
      }
    }, true);
  }
})();

// Replace broken QR images in user profile modals with a friendly placeholder
function initUserProfileQrFix() {
  function replaceIfEmptyDataUri(el) {
    try {
      if (!el || el.tagName !== 'IMG') return false;
      if (!el.closest || !el.closest('.modal, .modal-dialog')) return false;
      var src = el.getAttribute('src') || '';
      // matches empty/invalid data uri like "data:image/png;base64," or very short payloads
      if (/^data:image\/[a-zA-Z0-9.+-]+;base64,\s*$/.test(src) || (src.indexOf('data:image') === 0 && src.split(',')[1] && src.split(',')[1].length < 10)) {
        console.warn('[OTS] Replacing empty data URI for QR image inside modal:', src);
        var svg = 'data:image/svg+xml;utf8,' + encodeURIComponent(
          '<svg xmlns="http://www.w3.org/2000/svg" width="240" height="160">'
          + '<rect width="100%" height="100%" fill="#213041"/>'
          + '<text x="50%" y="50%" fill="#9fb1c8" font-family="Arial,Helvetica,sans-serif" font-size="14" text-anchor="middle" dy=".3em">QR unavailable</text>'
          + '</svg>'
        );
        if (el.getAttribute('data-ots-replaced') === '1') return true;
        el.setAttribute('data-ots-replaced', '1');
        el.src = svg;
        el.alt = 'QR code unavailable';
        var parent = el.parentNode;
        if (parent && !parent.querySelector('.ots-qr-note')) {
          var p = document.createElement('p');
          p.className = 'ots-qr-note text-muted';
          p.style.marginTop = '6px';
          p.textContent = 'QR failed to load. Close and re-open the Edit Profile dialog to retry.';
          parent.appendChild(p);
        }
        return true;
      }
    } catch (err) {
      console.error('[OTS] replaceIfEmptyDataUri error', err);
    }
    return false;
  }

  // Initial quick scan for any modal images already present
  try {
    var imgs = document.querySelectorAll('.modal img, .modal-dialog img');
    imgs.forEach(function(i) { replaceIfEmptyDataUri(i); });
  } catch (e) {}

  // Observe DOM for modals being added (some UIs load modal content via AJAX)
  try {
    var mo = new MutationObserver(function(muts) {
      muts.forEach(function(m) {
        m.addedNodes && m.addedNodes.forEach(function(node) {
          try {
            if (!node) return;
            if (node.nodeType === 1) {
              if (node.matches && node.matches('.modal, .modal-dialog')) {
                var imgs = node.querySelectorAll('img');
                imgs.forEach(function(i) { replaceIfEmptyDataUri(i); });
              } else {
                var imgs = node.querySelectorAll && node.querySelectorAll('img');
                imgs && imgs.forEach(function(i) { replaceIfEmptyDataUri(i); });
              }
            }
          } catch (err) {}
        });
      });
    });
    mo.observe(document.body, { childList: true, subtree: true });
    // stop observing after 20s to avoid long-lived observers in older pages
    setTimeout(function() { try { mo.disconnect(); } catch (e) {} }, 20000);
  } catch (err) {}

  // Also a short polling fallback for dynamic UIs for the first 6s
  var checks = 0;
  var interval = setInterval(function() {
    try {
      var imgs = document.querySelectorAll('.modal img, .modal-dialog img');
      imgs.forEach(function(i) { replaceIfEmptyDataUri(i); });
    } catch (e) {}
    checks += 1;
    if (checks > 12) clearInterval(interval);
  }, 500);
}

/**
 * OTS: Help pane fallback click handler.
 * The core help-pane.js may fail to render templates in white-label themes
 * (isXiboThemed = false). This handler catches the click and opens the
 * help landing page directly if the core handler didn't show the container.
 */
(function() {
  'use strict';
  var btn = document.querySelector('#help-pane .help-pane-btn');
  if (!btn) return;

  btn.addEventListener('click', function() {
    // Give the core handler 150ms to show the container
    setTimeout(function() {
      var container = document.querySelector('#help-pane .help-pane-container');
      if (container && container.offsetHeight > 0 && container.innerHTML.trim() !== '') {
        return; // Core handler worked — do nothing
      }
      // Fallback: open the help landing page directly
      var pane = document.getElementById('help-pane');
      var url = pane && pane.getAttribute('data-url-help-landing-page');
      if (url) {
        window.open(url, '_blank', 'noopener');
      }
    }, 150);
  });
})();
