// ============================================
// Sidebar Layout — icon rail, hamburger cycling, mobile backdrop & swipe
// ============================================

let _syncRailSideFn = null;

/**
 * Get the current syncRailSide function reference.
 * Needed because it gets patched after initial setup.
 */
export function syncRailSide() {
  if (_syncRailSideFn) _syncRailSideFn();
}

/**
 * Initialize sidebar layout: icon rail, hamburger cycling, mobile backdrop, swipe gestures.
 * @param {Object} Storage - Storage module
 * @param {Object} opts
 * @param {Object} opts.documentModule - Document module (for swapSide)
 * @param {Function} opts._closeCompareIfActive
 * @param {Function} opts._deactivateIncognito
 * @param {Object} opts.presetsModule
 * @param {Object} opts.sessionModule
 * @param {Function} opts.el - Element lookup helper
 * @param {*} opts._defaultChat - Default chat config
 * @param {Function} opts._syncResearchIndicator
 */
export function initSidebarLayout(Storage, opts) {
  const {
    documentModule, _closeCompareIfActive, _deactivateIncognito,
    presetsModule, sessionModule, el, _defaultChat, _syncResearchIndicator
  } = opts;

  // ── Icon rail + sidebar toggle ──
  const iconRail = document.getElementById('icon-rail');
  const hamburgerBtn = document.getElementById('hamburger-btn');

  function _syncRailSideCore() {
    const sidebar = document.getElementById('sidebar');
    if (!iconRail) return;
    const isRight = sidebar.classList.contains('right-side');
    const sidebarHidden = sidebar.classList.contains('hidden');
    const railHidden = iconRail.classList.contains('rail-hidden');
    const isMobileMini = iconRail.classList.contains('mobile-mini');
    iconRail.classList.toggle('right-side', isRight);
    // On mobile mini mode, JS already set inline styles — don't touch
    if (isMobileMini) {
      // Just update side positioning
      if (isRight) {
        iconRail.style.left = 'auto';
        iconRail.style.right = '0';
      } else {
        iconRail.style.left = '0';
        iconRail.style.right = 'auto';
      }
    } else {
      iconRail.style.display = (sidebarHidden && !railHidden) ? '' : 'none';
    }
    // Hamburger is always visible — just update body classes for CSS layout adjustments
    if (hamburgerBtn) {
      document.body.classList.toggle('hamburger-right', isRight);
      document.body.classList.toggle('hamburger-left', !isRight);
      document.body.classList.toggle('hamburger-only', sidebarHidden && railHidden);
      document.body.classList.toggle('sidebar-collapsed', sidebarHidden);
    }
    // Keep incognito button clear of hamburger
    const incogBtn = document.getElementById('incognito-btn');
    if (incogBtn) {
      if (isRight && sidebarHidden) {
        incogBtn.style.right = '48px';
      } else {
        incogBtn.style.right = '';
      }
    }
  }

  // Set initial reference and expose globally
  _syncRailSideFn = _syncRailSideCore;
  window.syncRailSide = syncRailSide;

  // Restore sidebar side preference
  if (Storage.get(Storage.KEYS.SIDEBAR_SIDE) === 'right') {
    document.getElementById('sidebar').classList.add('right-side');
  }
  syncRailSide();

  // In-sidebar toggle button — same behavior as hamburger
  const sidebarToggleBtn = document.getElementById('sidebar-toggle-btn');
  if (sidebarToggleBtn) {
    sidebarToggleBtn.addEventListener('click', (e) => {
      if (hamburgerBtn) hamburgerBtn.click();
    });
  }

  // New chat buttons — same as clicking brand
  const chatNewBtn = document.getElementById('chat-new-btn');
  const sidebarNewChat = document.getElementById('sidebar-new-chat-btn');
  [chatNewBtn, sidebarNewChat].forEach(btn => {
    if (btn) btn.addEventListener('click', () => {
      const brandBtn = document.getElementById('sidebar-brand-btn');
      if (brandBtn) brandBtn.click();
    });
  });

  // Hamburger cycles: full sidebar → mini → off → full
  // Shift-click swaps sidebar side
  let _userToggledSidebar = false;
  let _wasAutoCollapsed = false;

  // Deliberate "open the sidebar" used by the mobile swipe gesture (wired at
  // module scope). It MUST set _userToggledSidebar so the auto-collapse
  // MutationObserver doesn't immediately re-hide it (the swipe was opening it,
  // then checkSidebarAutoCollapse re-added .hidden because this flag was unset
  // — looked like nothing happened). Mirrors the hamburger's mobile-open path.
  window._odyOpenSidebar = function(side) {
    const sidebar = document.getElementById('sidebar');
    if (!sidebar) return;
    // On mobile, never open the sidebar while Compare is running — the panes
    // own the screen and stray gestures (swipe, dragging a dock chip to the X)
    // were popping it open. Blocking the open helper covers every path.
    const cc = document.getElementById('chat-container');
    if (window.innerWidth < 768 && cc && cc.classList.contains('compare-active')) return;
    _userToggledSidebar = true;
    // Optionally place the sidebar on a specific edge (the swipe gesture passes
    // the direction). Persist it + re-anchor the doc panel, same as a
    // shift-click on the hamburger.
    if (side === 'left' || side === 'right') {
      const wantRight = side === 'right';
      if (sidebar.classList.contains('right-side') !== wantRight) {
        sidebar.classList.toggle('right-side', wantRight);
        try { Storage.set(Storage.KEYS.SIDEBAR_SIDE, side); } catch (_) {}
        if (documentModule && documentModule.swapSide) { try { documentModule.swapSide(); } catch (_) {} }
      }
    }
    const backdrop = document.getElementById('sidebar-backdrop');
    if (window.innerWidth < 768 && iconRail) { iconRail.classList.remove('mobile-mini'); iconRail.style.cssText = ''; }
    sidebar.classList.remove('hidden');
    if (backdrop && window.innerWidth < 768) backdrop.classList.add('visible');
    syncRailSide();
  };

  if (hamburgerBtn) {
    hamburgerBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      const sidebar = document.getElementById('sidebar');
      if (e.shiftKey) {
        sidebar.classList.toggle('right-side');
        Storage.set(Storage.KEYS.SIDEBAR_SIDE, sidebar.classList.contains('right-side') ? 'right' : 'left');
        syncRailSide();
        if (documentModule && documentModule.swapSide) documentModule.swapSide();
        return;
      }

      _userToggledSidebar = true;
      const isSidebarVisible = !sidebar.classList.contains('hidden');

      if (window.innerWidth < 768) {
        // Mobile: full sidebar ↔ hidden — simple toggle, no mini rail
        const backdrop = document.getElementById('sidebar-backdrop');
        if (iconRail) { iconRail.classList.remove('mobile-mini'); iconRail.style.cssText = ''; }

        if (isSidebarVisible) {
          // Closing sidebar
          sidebar.classList.add('hidden');
          if (backdrop) backdrop.classList.remove('visible');
        } else {
          // Mobile: the hamburger always opens the sidebar from the RIGHT.
          // (Not persisted — keeps the desktop side preference untouched.)
          if (!sidebar.classList.contains('right-side')) {
            sidebar.classList.add('right-side');
            if (documentModule && documentModule.swapSide) { try { documentModule.swapSide(); } catch (_) {} }
          }
          // Opening sidebar — blur keyboard first, then open after layout settles
          if (document.activeElement && document.activeElement !== document.body
              && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) {
            document.activeElement.blur();
            // Wait for keyboard dismiss to settle, then open
            setTimeout(() => {
              sidebar.classList.remove('hidden');
              if (backdrop) backdrop.classList.add('visible');
              syncRailSide();
            }, 250);
          } else {
            sidebar.classList.remove('hidden');
            if (backdrop) backdrop.classList.add('visible');
          }
        }
        syncRailSide();
        return;
      }

      // Desktop: full sidebar ↔ mini (icon rail) — simple toggle
      if (isSidebarVisible) {
        sidebar.classList.add('hidden');
      } else {
        _wasAutoCollapsed = false;
        iconRail.classList.remove('rail-hidden');
        sidebar.classList.remove('hidden');
      }
      syncRailSide();
    });
  }

  // Icon rail section clicks — open sidebar and scroll to section
  if (iconRail) {
    iconRail.addEventListener('click', (e) => {
      const btn = e.target.closest('.icon-rail-btn');
      if (!btn || btn.id === 'rail-new-session' || btn.id === 'rail-delete-session' || btn.id === 'rail-search-btn' || btn.id === 'rail-settings' || btn.id === 'rail-admin') return;
      const sectionId = btn.dataset.section;
      if (!sectionId) return;
      const sidebar = document.getElementById('sidebar');
      sidebar.classList.remove('hidden');
      syncRailSide();
      const section = document.getElementById(sectionId);
      if (section) {
        section.scrollIntoView({ behavior: 'smooth', block: 'start' });
        section.classList.remove('collapsed');
      }
    });
  }

  // Auto-collapse sidebar when window gets small or chat area is squeezed
  const AUTO_COLLAPSE_WIDTH = 700;
  const MIN_CHAT_WIDTH = 380; // collapse sidebar if chat gets narrower than this

  function checkSidebarAutoCollapse() {
    if (_userToggledSidebar) return;
    const sidebar = document.getElementById('sidebar');
    if (!sidebar) return;
    const isHidden = sidebar.classList.contains('hidden');

    // Check if chat area is too narrow (e.g. sidebar + doc panel both open).
    // BUT — if a tile-snapped modal exists, IT is what's making chat narrow,
    // and that's the user's explicit choice. Don't auto-collapse the sidebar
    // in response, or we get a reactive loop: snap → narrow chat → hide
    // sidebar → safe-rect changes → reclamp modal → new chat width → ...
    const chatContainer = document.querySelector('.chat-container');
    const hasTileSnapped = document.querySelector('.modal-content[data-_tile-zone], .research-pane[data-_tile-zone]');
    const chatTooNarrow = chatContainer && chatContainer.offsetWidth < MIN_CHAT_WIDTH && !isHidden && !hasTileSnapped;

    if ((window.innerWidth < AUTO_COLLAPSE_WIDTH || chatTooNarrow) && !isHidden) {
      sidebar.classList.add('hidden');
      _wasAutoCollapsed = true;
      syncRailSide();
    } else if (window.innerWidth >= AUTO_COLLAPSE_WIDTH && isHidden && _wasAutoCollapsed) {
      // Only restore if chat won't be too narrow
      sidebar.classList.remove('hidden');
      void document.body.offsetWidth; // reflow
      if (chatContainer && chatContainer.offsetWidth < MIN_CHAT_WIDTH) {
        sidebar.classList.add('hidden');
      } else {
        _wasAutoCollapsed = false;
      }
      syncRailSide();
    }
  }

  window.addEventListener('resize', () => {
    _userToggledSidebar = false; // allow auto-collapse on actual resize
    requestAnimationFrame(checkSidebarAutoCollapse);
  });
  // Also re-check when doc panel toggles
  new MutationObserver(() => requestAnimationFrame(checkSidebarAutoCollapse))
    .observe(document.body, { attributes: true, attributeFilter: ['class'] });

  // Auto-collapse on initial load if window is small
  if (window.innerWidth < AUTO_COLLAPSE_WIDTH) {
    const sidebar = document.getElementById('sidebar');
    if (sidebar && !sidebar.classList.contains('hidden')) {
      sidebar.classList.add('hidden');
      _wasAutoCollapsed = true;
      syncRailSide();
    }
  }

  // ── Mobile sidebar backdrop + swipe-to-close ──
  // Backdrop overlay: tapping it closes the sidebar
  const mobileBackdrop = document.createElement('div');
  mobileBackdrop.id = 'sidebar-backdrop';
  document.body.appendChild(mobileBackdrop);

  function updateMobileBackdrop() {
    if (window.innerWidth >= 768) { mobileBackdrop.classList.remove('visible'); return; }
    const sb = document.getElementById('sidebar');
    const rail = document.getElementById('icon-rail');
    const sidebarOpen = sb && !sb.classList.contains('hidden');
    const miniOpen = rail && rail.classList.contains('mobile-mini');
    mobileBackdrop.classList.toggle('visible', sidebarOpen || miniOpen);
  }

  // Suppress sidebar close briefly after dropdown actions
  window._suppressSidebarClose = false;
  mobileBackdrop.addEventListener('click', (e) => {
    if (window._suppressSidebarClose) return;
    // Don't close while a session is being renamed inline — the rename input
    // lives inside the sidebar, and a backdrop tap (e.g. to dismiss the
    // keyboard) would otherwise kick the user out mid-rename.
    if (document.querySelector('.session-rename-input')) return;
    // Don't close if a dropdown or submenu is visible
    const openDD = document.querySelector('.session-dropdown-menu[style*="display: block"], .session-dropdown-menu[style*="display:block"]');
    const openSub = document.querySelector('.session-folder-submenu[style*="display: block"], .session-folder-submenu[style*="display:block"]');
    if (openDD || openSub) {
      if (openSub) openSub.style.display = 'none';
      if (openDD) openDD.style.display = 'none';
      return;
    }
    const sb = document.getElementById('sidebar');
    if (sb && !sb.classList.contains('hidden')) {
      sb.classList.add('hidden');
    }
    mobileBackdrop.classList.remove('visible');
    syncRailSide();
  });

  // Patch syncRailSide to also update backdrop
  const _origSyncRailSideCore = _syncRailSideCore;
  _syncRailSideFn = function() { _origSyncRailSideCore(); updateMobileBackdrop(); };
  window.syncRailSide = syncRailSide;

  // Swipe sidebar toward edge to close
  const sidebar = document.getElementById('sidebar');
  if (sidebar && 'ontouchstart' in window) {
    let _swStartX = 0, _swStartY = 0, _swSwiping = false;
    sidebar.addEventListener('touchstart', (e) => {
      if (e.target.closest('.list-item')) { _swSwiping = false; return; }
      _swStartX = e.touches[0].clientX;
      _swStartY = e.touches[0].clientY;
      _swSwiping = true;
    }, { passive: true });
    sidebar.addEventListener('touchmove', (e) => {
      if (!_swSwiping) return;
      const dx = e.touches[0].clientX - _swStartX;
      const dy = Math.abs(e.touches[0].clientY - _swStartY);
      if (dy > 40) { _swSwiping = false; return; }
      const isRight = sidebar.classList.contains('right-side');
      if ((!isRight && dx < -60) || (isRight && dx > 60)) {
        _swSwiping = false;
        const _backdrop = document.getElementById('sidebar-backdrop');
        if (_backdrop) _backdrop.classList.remove('visible');
        sidebar.classList.add('hidden');
        syncRailSide();
      }
    }, { passive: true });
    sidebar.addEventListener('touchend', () => { _swSwiping = false; }, { passive: true });
  }

  // ── Click outside sidebar / icon rail to close (mobile only) ──
  document.addEventListener('click', (e) => {
    if (window.innerWidth >= 700) return; // desktop keeps sidebar open
    const sb = document.getElementById('sidebar');
    const rail = document.getElementById('icon-rail');
    // Ignore clicks on elements removed from DOM (e.g. session list re-render during folder toggle)
    if (!e.target.isConnected) return;
    // Ignore clicks on the sidebar, icon rail, or hamburger button itself
    if (e.target.closest('#sidebar') || e.target.closest('#icon-rail') || e.target.closest('#hamburger-btn')) return;
    // Ignore clicks inside modals or the chat input area
    if (e.target.closest('.modal') || e.target.closest('.input-bar') || e.target.closest('#message')) return;
    // Ignore clicks on session/folder dropdowns and the styled prompt
    // overlay — they're body-level elements logically tied to a sidebar
    // action (e.g. "Move to folder → New Folder…"), so closing the
    // sidebar when the user clicks one yanks the action mid-flight.
    if (e.target.closest('.session-dropdown, .folder-submenu, #styled-prompt-overlay, #styled-confirm-overlay')) return;
    // Close full sidebar if open (with animation)
    if (sb && !sb.classList.contains('hidden')) {
      const backdrop = document.getElementById('sidebar-backdrop');
      if (backdrop) backdrop.classList.remove('visible');
      sb.classList.add('hidden');
      syncRailSide();
      return;
    }
    // Close mobile-mini icon rail overlay if open
    if (rail && rail.classList.contains('mobile-mini')) {
      rail.classList.remove('mobile-mini');
      rail.style.cssText = '';
      const backdrop = document.getElementById('sidebar-backdrop');
      if (backdrop) backdrop.classList.remove('visible');
      syncRailSide();
    }
  });

  // ── Mobile: close sidebar/rail when a tool button is tapped ──
  // The user expects the sidebar to get out of the way the moment a tool
  // window opens — otherwise the modal lands behind the sidebar on phones.
  // We remember whether the sidebar was open at the moment the tool was
  // tapped so we can re-open it when the tool's modal is dismissed; that
  // way clicking around the app doesn't leave the sidebar permanently
  // shut.
  let _sidebarWasOpenBeforeTool = false;
  let _railWasOpenBeforeTool = false;
  document.addEventListener('click', (e) => {
    if (window.innerWidth >= 700) return;
    const btn = e.target.closest('[id^="tool-"], [id^="rail-"]');
    if (!btn) return;
    setTimeout(() => {
      const sb = document.getElementById('sidebar');
      const rail = document.getElementById('icon-rail');
      const backdrop = document.getElementById('sidebar-backdrop');
      let changed = false;
      if (sb && !sb.classList.contains('hidden')) {
        _sidebarWasOpenBeforeTool = true;
        sb.classList.add('hidden');
        changed = true;
      }
      if (rail && rail.classList.contains('mobile-mini')) {
        _railWasOpenBeforeTool = true;
        rail.classList.remove('mobile-mini');
        rail.style.cssText = '';
        changed = true;
      }
      if (changed) {
        if (backdrop) backdrop.classList.remove('visible');
        syncRailSide();
      }
    }, 0);
  });

  // When a tool is dismissed by swiping it down (ui.js fires `modal-dismissed`),
  // don't bounce the sidebar back open — the swipe should just dismiss the tool.
  // Button-close still restores the prior sidebar state (no event fired there).
  window.addEventListener('modal-dismissed', () => {
    _sidebarWasOpenBeforeTool = false;
    _railWasOpenBeforeTool = false;
  });

  // ── Mobile: when a tool modal closes, restore the sidebar/rail to
  // whatever state it was in before the tool was opened. ──
  // We watch every .modal for the .hidden class going on, and if our
  // remembered "sidebar-was-open" flag is set, undo the auto-close.
  if (window.innerWidth < 700) {
    const _restoreSidebar = () => {
      const sb = document.getElementById('sidebar');
      const rail = document.getElementById('icon-rail');
      const backdrop = document.getElementById('sidebar-backdrop');
      // Skip if any modal is still visible (.modal without .hidden) — we only
      // restore once the user is back to bare chat. A tool swiped DOWN to a
      // dock chip is minimized (display:none via .modal-minimized), not closed
      // — it's still "around", so don't bounce the sidebar open behind it. Only
      // a full close (no minimized modal, no dock chips) should restore.
      const anyOpen = [...document.querySelectorAll('.modal')]
        .some(m => (!m.classList.contains('hidden') && getComputedStyle(m).display !== 'none')
                   || m.classList.contains('modal-minimized'));
      const anyDocked = document.querySelectorAll('.minimized-dock-chip').length > 0;
      if (anyOpen || anyDocked) {
        // A tool is still minimized/docked. The user has left the "launched
        // from the sidebar" context — drop the restore intent so that later
        // FULLY closing the tool (e.g. dragging its chip to the trash) doesn't
        // bounce the sidebar open. (The modal-dismissed listener that normally
        // clears these gets blocked by modalManager's stopImmediatePropagation.)
        _sidebarWasOpenBeforeTool = false;
        _railWasOpenBeforeTool = false;
        return;
      }
      if (_sidebarWasOpenBeforeTool && sb && sb.classList.contains('hidden')) {
        sb.classList.remove('hidden');
        if (backdrop) backdrop.classList.add('visible');
      }
      if (_railWasOpenBeforeTool && rail && !rail.classList.contains('mobile-mini')) {
        rail.classList.add('mobile-mini');
      }
      _sidebarWasOpenBeforeTool = false;
      _railWasOpenBeforeTool = false;
      if (_sidebarWasOpenBeforeTool || _railWasOpenBeforeTool) syncRailSide();
    };
    const _modalObs = new MutationObserver((muts) => {
      let triggered = false;
      for (const m of muts) {
        if (m.type !== 'attributes' || m.attributeName !== 'class') continue;
        const t = m.target;
        if (!(t instanceof HTMLElement) || !t.classList) continue;
        if (t.classList.contains('modal')) { triggered = true; break; }
      }
      if (triggered) setTimeout(_restoreSidebar, 50);
    });
    _modalObs.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['class'] });
  }

  // (Mobile swipe-to-open-sidebar is wired at MODULE scope — see
  // _initChatSwipeToOpenSidebar() at the bottom of this file — so it attaches
  // independently of this init function completing.)
}

// ── Mobile: swipe horizontally on the splash/chat to open the sidebar ──
// Wired at MODULE scope (not inside initSidebarLayout) so a throw anywhere in
// that init can't drop this listener. Bound on `document` so it catches the
// touch regardless of which child element is under the finger. touchmove is
// NON-passive and calls preventDefault() once the gesture is locked
// horizontal — without that, Firefox (and others) treat the horizontal swipe
// as their own scroll/navigation gesture and our handler never gets to act.
function _initChatSwipeToOpenSidebar() {
  if (window.__odySwipeWired) return;
  window.__odySwipeWired = true;

  // Areas where a horizontal drag means something else (their own scroll/drag).
  const EXCLUDE = [
    '#sidebar', '#icon-rail', '.modal', '.input-bar', '#message',
    '#minimized-dock', '.minimized-dock-chip', '#dock-trash-zone',
    'pre', 'table', '.agent-tool-output', '.agent-thread-cmd',
    'input', 'textarea', 'select',
  ].join(', ');

  let sx = 0, sy = 0, track = false, decided = false;

  const reset = () => { track = false; decided = false; };

  document.addEventListener('touchstart', (e) => {
    reset();
    if (window.innerWidth >= 768) return;
    if (!e.touches || e.touches.length !== 1) return;
    if (window._chipDragging) return;
    const sb = document.getElementById('sidebar');
    if (sb && !sb.classList.contains('hidden')) return; // already open
    // Only in the chat / empty-chat view. Not when a document or PDF is open
    // (body.doc-view), notes is open (body.notes-view), or a tool modal is up.
    if (document.body.classList.contains('doc-view') ||
        document.body.classList.contains('notes-view')) return;
    // Not while Compare is running — it takes over #chat-container with its own
    // panes/scroll, and the swipe-to-open-sidebar gesture gets in the way there.
    const cc = document.getElementById('chat-container');
    if (cc && cc.classList.contains('compare-active')) return;
    const anyModalOpen = [...document.querySelectorAll('.modal')].some(
      m => !m.classList.contains('hidden') && getComputedStyle(m).display !== 'none');
    if (anyModalOpen) return;
    const t = e.target;
    if (t && t.closest && t.closest(EXCLUDE)) return;
    // The gesture must start within the chat area itself.
    if (!(t && t.closest && t.closest('#chat-container'))) return;
    sx = e.touches[0].clientX;
    sy = e.touches[0].clientY;
    track = true;
  }, { passive: true, capture: true });

  document.addEventListener('touchmove', (e) => {
    if (!track) return;
    if (window._chipDragging) { track = false; return; }
    if (!e.touches || !e.touches.length) return;
    const dx = e.touches[0].clientX - sx;
    const dy = e.touches[0].clientY - sy;
    const adx = Math.abs(dx), ady = Math.abs(dy);
    if (!decided) {
      if (adx < 10 && ady < 10) return;          // not enough travel to judge
      if (ady > adx) { track = false; return; }   // vertical-dominant → let it scroll
      decided = true;                             // locked into a horizontal swipe
    }
    // Claim the gesture from the browser so it doesn't scroll/navigate instead.
    if (e.cancelable) e.preventDefault();
    if (adx >= 40) {
      track = false;
      // Direction picks the side (per user preference): swipe LEFT → sidebar
      // on the left, swipe RIGHT → sidebar on the right. dx<0 is a leftward
      // finger motion; mapping it to 'right' (and dx>0 to 'left') is what makes
      // it feel correct in practice.
      const side = dx < 0 ? 'right' : 'left';
      // Use the deliberate-open helper (sets _userToggledSidebar so the
      // auto-collapse observer doesn't instantly re-hide it). Fall back to a
      // plain unhide if the helper isn't wired yet.
      if (typeof window._odyOpenSidebar === 'function') {
        window._odyOpenSidebar(side);
      } else {
        const sb = document.getElementById('sidebar');
        if (sb) { sb.classList.remove('hidden'); try { syncRailSide(); } catch (_) {} }
      }
    }
  }, { passive: false, capture: true });

  document.addEventListener('touchend', reset, { passive: true, capture: true });
  document.addEventListener('touchcancel', reset, { passive: true, capture: true });
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', _initChatSwipeToOpenSidebar);
} else {
  _initChatSwipeToOpenSidebar();
}
