// /js/main.js — light bootstrap

// ---- Toast bridge (global, early, race-proof) ----
(function installToastBridge() {
  if (window.__FR_TOAST_BRIDGE__) return;
  window.__FR_TOAST_BRIDGE__ = true;

  window.__FR_TOAST_Q = window.__FR_TOAST_Q || [];     // queued toasts until real toast is ready
  window.__REAL_TOAST__ = window.__REAL_TOAST__ || null; // set later once domUtils is loaded
  window.__FR_TOAST_FILTER__ = window.__FR_TOAST_FILTER__ || null; // filter hook (auth.js)

  window.showToast = function (msgOrKey, duration) {
    // Let auth.js (or anyone) rewrite/suppress messages centrally.
    try {
      if (typeof window.__FR_TOAST_FILTER__ === 'function') {
        const out = window.__FR_TOAST_FILTER__(msgOrKey);
        if (out === null) return;           // suppressed
        msgOrKey = out;                     // rewritten/translated
      }
    } catch { }

    if (typeof window.__REAL_TOAST__ === 'function') {
      return window.__REAL_TOAST__(msgOrKey, duration);
    }
    window.__FR_TOAST_Q.push([msgOrKey, duration]);
  };

  // Optional: generic event bridge
  window.addEventListener('filerise:toast', (e) => {
    const { message, duration } = (e && e.detail) || {};
    if (message) window.showToast(message, duration);
  });
})();

async function ensureToastReady() {
  if (window.__REAL_TOAST__) return;
  try {
    const dom = await import('/js/domUtils.js?v={{APP_QVER}}');  // real toast
    const real = dom.showToast || ((m, d) => console.log('TOAST:', m, d));

    // (Optional) “false-negative to success” normalizer
    const normalized = function (msg, dur) {
      try {
        const m = (msg || '').toString().toLowerCase();
        if (/does not exist|already exist|not found/.test(m) && window.__FR_LAST_OK) {
          window.__FR_LAST_OK = false;
          return real('Done.', dur);
        }
      } catch { }
      return real(msg, dur);
    };

    window.__REAL_TOAST__ = normalized;

    // Flush anything that queued before domUtils was ready
    const q = window.__FR_TOAST_Q || [];
    window.__FR_TOAST_Q = [];
    q.forEach(([m, d]) => window.__REAL_TOAST__(m, d));
  } catch {
    window.__REAL_TOAST__ = (m, d) => console.log('TOAST:', m, d);
  }
}

function isDemoHost() {
  // Handles optional "www." just in case
  return location.hostname.replace(/^www\./, '') === 'demo.filerise.net';
}

function showLoginTip(message) {
  const tip = document.getElementById('fr-login-tip');
  if (!tip) return;
  tip.innerHTML = '';                           // clear
  if (message) tip.append(document.createTextNode(message));
  if (location.hostname.replace(/^www\./, '') === 'demo.filerise.net') {
    const line = document.createElement('div'); line.style.marginTop = '6px';
    const mk = t => { const k = document.createElement('code'); k.textContent = t; return k; };
    line.append(document.createTextNode('Demo login — user: '), mk('demo'),
      document.createTextNode(' · pass: '), mk('demo'));
    tip.append(line);
  }
  tip.style.display = 'block';                  // reveal without shifting layout
}

async function hideOverlaySmoothly(overlay) {
  if (!overlay) return;
  try { await document.fonts?.ready; } catch { }
  await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
  overlay.style.display = 'none';
}

function wireModalEnterDefault() {
  if (window.__FR_FLAGS.wired.enterDefault) return;
  window.__FR_FLAGS.wired.enterDefault = true;

  document.addEventListener('keydown', (e) => {
    if (e.key !== 'Enter' || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;

    // don’t hijack multiline inputs or anything explicitly opted-out
    const tgt = e.target;
    if (tgt && (tgt.tagName === 'TEXTAREA' || tgt.isContentEditable || tgt.closest('[data-no-enter]'))) return;

    // pick the topmost visible modal
    const modal = Array.from(document.querySelectorAll('.modal')).reverse().find(m => {
      const s = getComputedStyle(m);
      return s.display !== 'none' && s.visibility !== 'hidden' && s.pointerEvents !== 'none';
    });
    if (!modal) return;

    const btn = modal.querySelector('[data-default]');
    if (!btn || btn.disabled) return;

    e.preventDefault();
    btn.click();
  }, true); // capture so we beat other handlers
}

// One-shot guards
window.__FR_FLAGS = window.__FR_FLAGS || {
  booted: false,
  initialized: false,
  domReadyFired: false,
  wired: Object.create(null)
};

window.__FR_FLAGS.bootPromise = window.__FR_FLAGS.bootPromise || null;
window.__FR_FLAGS.entryStarted = window.__FR_FLAGS.entryStarted || false;

// ---- Result guard + request coalescer (dedupe) ----
(function installResultGuardAndCoalescer() {
  if (window.__FR_FETCH_GUARD_INSTALLED) return;
  window.__FR_FETCH_GUARD_INSTALLED = true;

  const nativeFetch = window.fetch.bind(window);
  window.__FR_LAST_OK = false;

  const inFlight = new Map(); // key -> { ts, promise }

  function normalizeUrl(u) {
    try {
      const url = new URL(u, window.location.origin);
      // Keep path + stable query ordering
      const params = new URLSearchParams(url.search);
      const sorted = new URLSearchParams();
      [...params.keys()].sort().forEach(k => sorted.set(k, params.get(k)));
      return url.pathname + (sorted.toString() ? '?' + sorted.toString() : '');
    } catch { return String(u || ''); }
  }

  async function toStableBody(init) {
    const b = init && init.body;
    if (!b) return '';
    try {
      if (typeof b === 'string') {
        // try JSON
        try {
          const j = JSON.parse(b);
          // remove volatile fields like csrf
          delete j.csrf; delete j.csrf_token; delete j._;
          return 'JSON:' + JSON.stringify(j, Object.keys(j).sort());
        } catch {
          // maybe urlencoded
          const p = new URLSearchParams(b);
          ['csrf', 'csrf_token', '_'].forEach(k => p.delete(k));
          return 'FORM:' + [...p.entries()].map(([k, v]) => `${k}=${v}`).sort().join('&');
        }
      }
    } catch { }
    return 'B:' + String(b);
  }

  window.fetch = async (input, init = {}) => {
    const method = (init.method || 'GET').toUpperCase();
    const urlKey = normalizeUrl(typeof input === 'string' ? input : (input && input.url) || '');
    const bodyKey = await toStableBody(init);
    const key = method + ' ' + urlKey + ' ' + bodyKey;

    const now = Date.now();
    const existing = inFlight.get(key);
    if (existing && (now - existing.ts) < 800) {
      // coalesce: return the same promise
      return existing.promise.then(r => r.clone());
    }

    // 1) Only coalesce mutating methods
    if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
      return nativeFetch(input, init);
    }

    // 2) Only same-origin API calls
    const isSameOrigin = (typeof input === 'string')
      ? input.startsWith('/') || input.startsWith(location.origin)
      : new URL(input.url).origin === location.origin;
    const urlPath = (typeof input === 'string') ? input : new URL(input.url).pathname;
    if (!isSameOrigin || !urlPath.startsWith('/api/')) {
      return nativeFetch(input, init);
    }

    // 3) Never touch downloads/streams
    if (urlPath.includes('download') || urlPath.includes('zip')) {
      return nativeFetch(input, init);
    }

    // 4) Kill switch (handy for debugging)
    if (window.__FR_DISABLE_COALESCE) {
      return nativeFetch(input, init);
    }

    const p = nativeFetch(input, init).then(async (res) => {
      try {
        const clone = res.clone();
        let okJson = null;
        try { okJson = await clone.json(); } catch { }
        const okFlag = res.ok && okJson && (
          okJson.success === true || okJson.status === 'ok' || okJson.result === 'ok'
        );
        window.__FR_LAST_OK = !!okFlag;
      } catch { }
      return res;
    }).finally(() => {
      // let it linger briefly so very-rapid duplicates still coalesce
      setTimeout(() => inFlight.delete(key), 200);
    });

    inFlight.set(key, { ts: now, promise: p });
    return p.then(r => r.clone());
  };

  // Gentle toast normalizer (compatible with showToast(message, duration))
  const origToast = window.showToast;
  if (typeof origToast === 'function' && !origToast.__frWrapped) {
    const wrapped = function (msg, maybeDuration) {
      try {
        const m = (msg || '').toString().toLowerCase();
        const looksWrong =
          /does not exist|already exist|not found/.test(m) &&
          window.__FR_LAST_OK === true;
        if (looksWrong) {
          window.__FR_LAST_OK = false;
          // Keep default duration if not numeric
          const dur = (typeof maybeDuration === 'number') ? maybeDuration : undefined;
          return origToast('Done.', dur);
        }
      } catch { }
      const dur = (typeof maybeDuration === 'number') ? maybeDuration : undefined;
      return origToast(msg, dur);
    };
    wrapped.__frWrapped = true;
    window.showToast = wrapped;
  }
})();


function bindDragAutoScroll() {
  if (window.__FR_FLAGS.wired.dragScroll) return;
  window.__FR_FLAGS.wired.dragScroll = true;

  const THRESH = 50;
  const SPEED = 20;
  document.addEventListener('dragover', (e) => {
    const y = e && typeof e.clientY === 'number' ? e.clientY : null;
    if (y == null) return;
    if (y < THRESH) window.scrollBy(0, -SPEED);
    else if (y > (window.innerHeight - THRESH)) window.scrollBy(0, SPEED);
  }, { passive: true });
}

function bindClickIfMissing(id, fnName) {
  const el = document.getElementById(id);
  if (!el || el.__bound) return;
  el.__bound = true;

  el.addEventListener('click', async (e) => {
    e.preventDefault();
    if (el.dataset.busy === '1') return;
    el.dataset.busy = '1';

    try {
      const acts = await ensureFileActionsLoaded();
      if (acts && typeof acts[fnName] === 'function') {
        await acts[fnName]();
      }
      // if API said success but no positive toast happened, give a neutral one
      if (window.__FR_LAST_OK === true && typeof window.showToast === 'function') {
        window.showToast('Done.', 'success');
        window.__FR_LAST_OK = false;
      }
    } catch (err) {
      console.warn(`[wire] ${fnName} error`, err);
    } finally {
      const modal = el.closest('.modal'); if (modal) modal.style.display = 'none';
      setTimeout(() => { el.dataset.busy = '0'; }, 600); // short cooldown
    }
  }, true);
}

function dispatchLegacyReadyOnce() {
  if (window.__FR_FLAGS.domReadyFired) return;
  window.__FR_FLAGS.domReadyFired = true;
  try { document.dispatchEvent(new Event('DOMContentLoaded')); } catch { }
  try { window.dispatchEvent(new Event('load')); } catch { }
}

// -------- username label ( --------
function wireUserNameLabel(state) {
  const username = (state && state.username) || localStorage.getItem('username') || '';
  const btn = document.getElementById('userDropdownToggle') || document.getElementById('userMenuBtn');
  if (!btn) return;
  const label = btn.querySelector('.user-name-label');
  if (!label) return; // don’t inject new DOM
  label.textContent = username || '';
}

// -------- DARK MODE (persist + system fallback + a11y labels) --------
function applyDarkMode({ fromSystemChange = false } = {}) {
  let stored = null;
  try { stored = localStorage.getItem('darkMode'); } catch { }

  let isDark = (stored === null)
    ? (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
    : (stored === '1' || stored === 'true');

  const root = document.documentElement;
  const body = document.body;

  [root, body].forEach(el => {
    if (!el) return;
    el.classList.toggle('dark-mode', isDark);
    el.setAttribute('data-theme', isDark ? 'dark' : 'light');
  });

  // keep UA chrome & bg consistent post-toggle
  const bg = isDark ? '#121212' : '#ffffff';
  root.style.backgroundColor = bg;
  root.style.colorScheme = isDark ? 'dark' : 'light';
  if (body) {
    body.style.backgroundColor = bg;
    body.style.colorScheme = isDark ? 'dark' : 'light';
  }
  const mt = document.querySelector('meta[name="theme-color"]');
  if (mt) mt.content = bg;
  const mcs = document.querySelector('meta[name="color-scheme"]');
  if (mcs) mcs.content = isDark ? 'dark light' : 'light dark';

  const btn = document.getElementById('darkModeToggle');
  const icon = document.getElementById('darkModeIcon');
  if (icon) icon.textContent = isDark ? 'light_mode' : 'dark_mode';
  if (btn) {
    const ttOn = (typeof t === 'function' ? t('switch_to_dark_mode') : 'Switch to dark mode');
    const ttOff = (typeof t === 'function' ? t('switch_to_light_mode') : 'Switch to light mode');
    const aria = (typeof t === 'function' ? (isDark ? t('light_mode') : t('dark_mode')) : (isDark ? 'Light mode' : 'Dark mode'));
    btn.classList.toggle('active', isDark);
    btn.setAttribute('aria-label', aria);
    btn.setAttribute('title', isDark ? ttOff : ttOn);
  }
}

function bindDarkMode() {
  const btn = document.getElementById('darkModeToggle');
  if (btn && !btn.__bound) {
    btn.__bound = true;
    applyDarkMode(); // apply once on boot

    btn.addEventListener('click', () => {
      // Toggle relative to current DOM state
      const isDarkNext = !(document.documentElement.classList.contains('dark-mode') || document.body.classList.contains('dark-mode'));
      try { localStorage.setItem('darkMode', isDarkNext ? '1' : '0'); } catch { }
      applyDarkMode();
    });
  }

  // Listen to system changes only if user has NOT set a preference
  if (!window.__FR_FLAGS.wired.sysDarkMO) {
    window.__FR_FLAGS.wired.sysDarkMO = true;
    let stored = null; try { stored = localStorage.getItem('darkMode'); } catch { }
    if (stored === null && window.matchMedia) {
      const mq = window.matchMedia('(prefers-color-scheme: dark)');
      const handler = () => applyDarkMode({ fromSystemChange: true });
      try { mq.addEventListener('change', handler); } catch { mq.addListener(handler); }
    }
  }
}

(function () {
  // ---------- tiny utils ----------
  const $ = (s, root = document) => root.querySelector(s);
  const $$ = (s, root = document) => Array.from(root.querySelectorAll(s));
  // Safe show/hide that work with both CSS and [hidden]
  const unhide = (el) => { if (!el) return; el.removeAttribute('hidden'); el.style.display = ''; };
  const hideEl = (el) => { if (!el) return; el.setAttribute('hidden', ''); el.style.display = 'none'; };
  const show = (el) => {
    if (!el) return;
    el.hidden = false; el.classList?.remove('d-none', 'hidden');
    el.style.display = 'block'; el.style.visibility = 'visible'; el.style.opacity = '1';
  };
  const hide = (el) => { if (!el) return; el.style.display = 'none'; };
  const setMeta = (name, val) => {
    let m = document.querySelector(`meta[name="${name}"]`);
    if (!m) { m = document.createElement('meta'); m.name = name; document.head.appendChild(m); }
    m.content = val;
  };

  // ---------- site config / auth ----------
  function applySiteConfig(cfg, { phase = 'final' } = {}) {
    try {
      const title = (cfg && cfg.header_title) ? String(cfg.header_title) : 'FileRise';
  
      // Always keep <title> correct early (no visual flicker)
      document.title = title;
  
      // --- Login options (apply in BOTH phases so login page is correct) ---
      const lo = (cfg && cfg.loginOptions) ? cfg.loginOptions : {};
      const disableForm  = !!lo.disableFormLogin;
      const disableOIDC  = !!lo.disableOIDCLogin;
      const disableBasic = !!lo.disableBasicAuth;
  
      const row = $('#loginForm');
      if (row) {
        if (disableForm) {
          row.setAttribute('hidden', '');
          row.style.display = ''; // don't leave display:none lying around
        } else {
          row.removeAttribute('hidden');
          row.style.display = '';
        }
      }
      const oidc  = $('#oidcLoginBtn'); if (oidc)  oidc.style.display  = disableOIDC ? 'none' : '';
      const basic = document.querySelector('a[href="/api/auth/login_basic.php"]');
      if (basic) basic.style.display = disableBasic ? 'none' : '';
  
      // --- Header <h1> only in the FINAL phase (prevents visible flips) ---
      if (phase === 'final') {
        const h1 = document.querySelector('.header-title h1');
        if (h1) {
          // prevent i18n or legacy from overwriting it
          if (h1.hasAttribute('data-i18n-key')) h1.removeAttribute('data-i18n-key');
  
          if (h1.textContent !== title) h1.textContent = title;
  
          // lock it so late code can't stomp it
          if (!h1.__titleLock) {
            const mo = new MutationObserver(() => {
              if (h1.textContent !== title) h1.textContent = title;
            });
            mo.observe(h1, { childList: true, characterData: true, subtree: true });
            h1.__titleLock = mo;
          }
        }
      }
    } catch { }
  }

  async function readyToReveal() {
    // Wait for CSS + fonts so the first revealed frame is fully styled
    try { await (window.__CSS_PROMISE__ || Promise.resolve()); } catch { }
    try { await document.fonts?.ready; } catch { }
    // Give layout one paint to settle
    await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
  }

  async function revealAppAndHideOverlay() {
    const appRoot = document.getElementById('appRoot');
    const overlay = document.getElementById('loadingOverlay');
    await readyToReveal();
    if (appRoot) appRoot.style.visibility = 'visible';
    if (overlay) {
      overlay.style.transition = 'opacity .18s ease-out';
      overlay.style.opacity = '0';
      setTimeout(() => { overlay.style.display = 'none'; }, 220);
    }
  }

  async function loadSiteConfig() {
    try {
      const r = await fetch('/api/siteConfig.php', { credentials: 'include' });
      const j = await r.json().catch(() => ({}));
      window.__FR_SITE_CFG__ = j || {};
      // Early pass: title + login options (skip touching <h1> to avoid flicker)
      applySiteConfig(window.__FR_SITE_CFG__, { phase: 'early' });
      return window.__FR_SITE_CFG__;
    } catch {
      window.__FR_SITE_CFG__ = {};
      applySiteConfig({}, { phase: 'early' });
      return null;
    }
  }
  async function primeCsrf() {
    try {
      const tr = await fetch('/api/auth/token.php', { credentials: 'include' });
      const tj = await tr.json().catch(() => ({}));
      if (tj?.csrf_token) { setMeta('csrf-token', tj.csrf_token); window.csrfToken = tj.csrf_token; try { localStorage.setItem('csrf', tj.csrf_token); } catch { } }
    } catch { }
  }
  async function checkAuth() {
    try {
      const r = await fetch('/api/auth/checkAuth.php', { credentials: 'include' });
      const j = await r.json().catch(() => ({}));

      if (j?.csrf_token) {
        setMeta('csrf-token', j.csrf_token);
        window.csrfToken = j.csrf_token;
        try { localStorage.setItem('csrf', j.csrf_token); } catch { }
      }
      if (typeof j?.isAdmin !== 'undefined') {
        try { localStorage.setItem('isAdmin', j.isAdmin ? '1' : '0'); } catch { }
      }
      if (typeof j?.username !== 'undefined') {
        try { localStorage.setItem('username', j.username || ''); } catch { }
      }

      const setup = !!j?.setup || !!j?.setup_mode || j?.mode === 'setup' || j?.status === 'setup' || !!j?.requires_setup || !!j?.needs_setup;
      return { authed: !!j?.authenticated, setup, raw: j };
    } catch {
      return { authed: false, setup: false, raw: {} };
    }
  }

  // ---- Create dropdown + its two modals  ----
  function wireCreateDropdown() {
    const container = document.getElementById('createDropdown');
    const btn = document.getElementById('createBtn');
    const menu = document.getElementById('createMenu');
    const makeF = document.getElementById('createFileOption');
    const makeD = document.getElementById('createFolderOption');
    if (!container || !btn || !menu) return;

    // Ensure layout basics
    if (getComputedStyle(container).position === 'static') container.style.position = 'relative';
    btn.style.pointerEvents = 'auto';
    menu.style.pointerEvents = 'auto';
    menu.style.zIndex = '10010';

    // Show/hide button based on live auth flags
    const st = (window.__FR_AUTH_STATE || {});
    const readOnly = !!st.readOnly || (localStorage.getItem('readOnly') === 'true' || localStorage.getItem('readOnly') === '1');
    const disableUpload = !!st.disableUpload || (localStorage.getItem('disableUpload') === 'true' || localStorage.getItem('disableUpload') === '1');
    btn.style.display = (!readOnly && !disableUpload) ? 'inline-flex' : 'none';

    let justToggledAt = 0;

    function openMenu() {
      // Beat any CSS with !important
      menu.style.setProperty('display', 'block', 'important');
      btn.setAttribute('aria-expanded', 'true');
      justToggledAt = Date.now();
    }
    function closeMenu() {
      menu.style.setProperty('display', 'none', 'important');
      btn.setAttribute('aria-expanded', 'false');
    }
    function isOpen() {
      return getComputedStyle(menu).display !== 'none';
    }

    if (!btn.__bound) {
      btn.__bound = true;
      btn.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation(); // block bubble-phase global closers
        if (isOpen()) closeMenu(); else openMenu();
      }, true); // use capture so we run before bubble-phase closers
    }

    // Close only when clicking truly outside our container.
    // Use capture and ignore the click that immediately follows our open() call.
    if (!menu.__outside) {
      menu.__outside = true;
      document.addEventListener('click', (e) => {
        // ignore the same-tick/rapid click that opened the menu
        if (Date.now() - justToggledAt < 120) return;
        if (!container.contains(e.target)) closeMenu();
      }, true); // capture to pre-empt other handlers
    }

    if (!menu.__esc) {
      menu.__esc = true;
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && isOpen()) closeMenu();
      });
    }

    const openModal = (id) => {
      const m = document.getElementById(id);
      if (m) { m.style.display = 'block'; closeMenu(); }
    };
    if (makeF && !makeF.__bound) { makeF.__bound = true; makeF.addEventListener('click', (e) => { e.preventDefault(); openModal('createFileModal'); }); }
    if (makeD && !makeD.__bound) { makeD.__bound = true; makeD.addEventListener('click', (e) => { e.preventDefault(); openModal('createFolderModal'); }); }
  }

  // ---- Modal cancel safety ----
  function bindCancelSafeties() {
    const ids = [
      '#cancelDeleteFiles', '#cancelCopyFiles', '#cancelMoveFiles', '#cancelDownloadZip', '#cancelDownloadFile',
      '#cancelCreateFile', '#cancelMoveFolder', '#cancelRenameFolder', '#cancelDeleteFolder', '#closeRestoreModal'
    ];
    ids.forEach(id => {
      const el = document.querySelector(id);
      if (el && !el.__safe) {
        el.__safe = true;
        el.addEventListener('click', (e) => {
          e.preventDefault();
          const modal = el.closest('.modal');
          if (modal) modal.style.display = 'none';
        });
      }
    });
  }

  function keepCreateDropdownWired() {
    if (window.__FR_FLAGS.wired.keepCreateMO) return;
    window.__FR_FLAGS.wired.keepCreateMO = true;
    const mo = new MutationObserver(() => {
      const btn = document.getElementById('createBtn');
      const menu = document.getElementById('createMenu');
      if (btn && menu && !btn.__bound) wireCreateDropdown();
    });
    mo.observe(document.body, { childList: true, subtree: true });
  }

  // ---- Folder-level helpers: de-dupe selects only when a modal opens ----
  function dedupeSelect(el) {
    if (!el) return;
    const seen = new Set();
    const rm = [];
    Array.from(el.options).forEach(opt => {
      const key = (opt.value || opt.textContent || '').trim();
      if (!key) return;
      if (seen.has(key)) rm.push(opt); else seen.add(key);
    });
    rm.forEach(o => o.remove());
  }
  function dedupeByIdSoon(id) { setTimeout(() => { const el = document.getElementById(id); if (el) dedupeSelect(el); }, 60); }

  function wireFolderButtons() {
    const open = (id) => { const m = document.getElementById(id); if (m) m.style.display = 'block'; };

    const moveBtn = document.getElementById('moveFolderBtn');
    if (moveBtn && !moveBtn.__bound) {
      moveBtn.__bound = true;
      moveBtn.addEventListener('click', (e) => { e.preventDefault(); open('moveFolderModal'); dedupeByIdSoon('moveFolderTarget'); });
    }

    const shareBtn = document.getElementById('shareFolderBtn');
    if (shareBtn && !shareBtn.__bound) {
      shareBtn.__bound = true;
      shareBtn.addEventListener('click', (e) => {
        e.preventDefault();
        const shareModal = document.getElementById('shareFolderModal');
        if (shareModal) shareModal.style.display = 'block';
        else document.dispatchEvent(new CustomEvent('filerise:share-folder', { detail: { folder: window.currentFolder || 'root' } }));
      });
    }

    const moveFilesOpenBtn = document.getElementById('moveSelectedBtn');
    if (moveFilesOpenBtn && !moveFilesOpenBtn.__dedupeHook) {
      moveFilesOpenBtn.__dedupeHook = true;
      moveFilesOpenBtn.addEventListener('click', () => dedupeByIdSoon('moveTargetFolder'));
    }
  }

  // ---- Lift modals above cards, always clickable ----
  function liftModals() {
    document.querySelectorAll('.modal').forEach(m => { m.style.pointerEvents = 'auto'; m.style.zIndex = '10000'; });
    document.querySelectorAll('.modal .modal-content').forEach(c => { c.style.position = 'relative'; c.style.zIndex = '10001'; });
  }

  // ---- Title fix after first list ----
  function updateFileListTitle() {
    const el = document.getElementById('fileListTitle');
    if (!el) return;
    const folder = (window.currentFolder || localStorage.getItem('lastOpenedFolder') || 'root');
    const name = folder === 'root' ? '(Root)' : folder;
    el.textContent = `Files in ${name}`;
    el.setAttribute('data-i18n-key', 'file_list_title');
  }

  // ---------- SETUP MODE ----------
  async function createUserSetup(username, password, isAdmin) {
    const csrf = window.csrfToken || localStorage.getItem('csrf') || '';
    const headers = { 'Content-Type': 'application/json' };
    if (csrf) headers['X-CSRF-Token'] = csrf;

    const res = await fetch('/api/addUser.php?setup=1', {
      method: 'POST',
      credentials: 'include',
      headers,
      body: JSON.stringify({ username, password, isAdmin: true, admin: true, grant_admin: true })
    });

    let j = {};
    try { j = await res.json(); } catch { }
    if (!res.ok || j.error) throw new Error(j.error || `Add user failed (${res.status})`);
    return true;
  }
  function bindSetupAddUser() {
    const form = document.getElementById('addUserForm');
    if (!form || form.__bound) return;
    form.__bound = true;

    form.addEventListener('submit', async (ev) => {
      ev.preventDefault();

      const usernameEl = document.getElementById('newUsername');
      const passwordEl = document.getElementById('addUserPassword');
      const isAdminEl = document.getElementById('isAdmin');

      const username = (usernameEl?.value || '').trim();
      const password = (passwordEl?.value || '');
      const isAdmin = isAdminEl ? !!isAdminEl.checked : true;

      if (!username || !password) {
        alert('Enter a username and password.');
        (username ? passwordEl : usernameEl)?.focus();
        return;
      }

      try {
        await primeCsrf();
        await createUserSetup(username, password, isAdmin);
        const addModal = document.getElementById('addUserModal'); if (addModal) addModal.style.display = 'none';
        window.setupMode = false;
        window.location.reload();
      } catch (e) {
        alert(e.message || 'Failed to create user. Check server logs.');
        if (!username) usernameEl?.focus();
        else if (!password) passwordEl?.focus();
      }
    }, true);
  }

  // ---------- pre-auth login ----------
  function forceLoginVisible() {
    show($('#main'));
    show($('#loginForm'));
    const hb = $('.header-buttons'); if (hb) hb.style.visibility = 'hidden';
    const ov = $('#loadingOverlay'); if (ov) ov.style.display = 'none';
  }
  function looksLikeTOTP(res, body) {
    try {
      if (res && (res.headers.get('X-TOTP-Required') === '1' ||
        (res.redirected && /[?&]totp_required=1\b/.test(res.url)))) {
        return true;
      }
      if (body && (body.totp_required === true || body.error === 'TOTP_REQUIRED')) {
        return true;
      }
    } catch { }
    return false;
  }

  async function openTotpNow() {
    // refresh CSRF for the upcoming /totp_verify call
    try { await primeCsrf(); } catch { }
    window.pendingTOTP = true;
    // reuse the function you already export from auth.js
    try {
      const auth = await import('/js/auth.js?v={{APP_QVER}}');
      if (typeof auth.openTOTPLoginModal === 'function') auth.openTOTPLoginModal();
    } catch (e) {
      console.warn('Could not import auth.js to open TOTP modal:', e);

      const m = document.getElementById('totpLoginModal'); if (m) m.style.display = 'block';
    }
  }
  function bindLogin() {
    const oidcBtn = $('#oidcLoginBtn');
    if (oidcBtn && !oidcBtn.__bound) {
      oidcBtn.__bound = true;
      oidcBtn.addEventListener('click', () => { window.location.href = '/api/auth/auth.php?oidc=initiate'; });
    }

    const form = $('#authForm');
    if (!form || form.__bound) return;
    form.__bound = true;

    form.addEventListener('submit', async (ev) => {
      ev.preventDefault();
      const username = ($('#loginUsername') || {}).value || '';
      const password = ($('#loginPassword') || {}).value || '';
      const remember = !!(document.getElementById('rememberMeCheckbox') || {}).checked;

      await primeCsrf();
      const csrf = window.csrfToken || localStorage.getItem('csrf') || '';

      // After showing the login form in the not-authed branch
      (async () => {
        const qp = new URLSearchParams(location.search);
        if (qp.get('totp_required') === '1') {
          try { await primeCsrf(); } catch { }
          window.pendingTOTP = true;
          try {
            const auth = await import('/js/auth.js?v={{APP_QVER}}');
            if (typeof auth.openTOTPLoginModal === 'function') auth.openTOTPLoginModal();
          } catch (e) {
            console.warn('openTOTPLoginModal import failed', e);
          }
        }
      })();

      // JSON first
      try {
        const r = await fetch('/api/auth/auth.php', {
          method: 'POST', credentials: 'include',
          headers: Object.assign({ 'Content-Type': 'application/json', 'Accept': 'application/json' }, csrf ? { 'X-CSRF-Token': csrf } : {}),
          body: JSON.stringify({ username: String(username).trim(), password: String(password).trim(), remember_me: !!remember })
        });
        const j = await r.clone().json().catch(() => ({}));

        //  TOTP step-up?
        if (looksLikeTOTP(r, j)) { await openTotpNow(); return; }

        if (j && (j.authenticated || j.success || j.status === 'ok' || j.result === 'ok')) return afterLogin();
      } catch { }

      // fallback form
      try {
        const p = new URLSearchParams();
        p.set('username', username); p.set('password', password); p.set('remember_me', remember ? '1' : '0');
        const r2 = await fetch('/api/auth/auth.php', {
          method: 'POST', credentials: 'include',
          headers: Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, csrf ? { 'X-CSRF-Token': csrf } : {}),
          body: p.toString()
        });
        const j2 = await r2.clone().json().catch(() => ({}));

        //  TOTP step-up on fallback too
        if (looksLikeTOTP(r2, j2)) { await openTotpNow(); return; }

        if (j2 && (j2.authenticated || j2.success || j2.status === 'ok' || j2.result === 'ok')) return afterLogin();
      } catch { }
      alert('Login failed');
    });
  }
  function afterLogin() {
    const start = Date.now();
    (function poll() {
      checkAuth().then(({ authed }) => {
        if (authed) { window.location.reload(); return; }
        if (Date.now() - start < 5000) return setTimeout(poll, 200);
        alert('Login session not established');
      }).catch(() => setTimeout(poll, 250));
    })();
  }

  // ---------- SETUP MODE (no flicker) ----------
  async function bootSetupWizard() {
    const overlay = document.getElementById('loadingOverlay'); if (overlay) overlay.remove();
    const wrap = document.querySelector('.main-wrapper'); if (wrap) wrap.style.display = '';
    const login = document.getElementById('loginForm'); if (login) login.style.display = 'none';
    const hb = document.querySelector('.header-buttons'); if (hb) hb.style.visibility = 'hidden';
    (document.getElementById('mainOperations') || {}).style && (document.getElementById('mainOperations').style.display = 'none');
    (document.getElementById('uploadFileForm') || {}).style && (document.getElementById('uploadFileForm').style.display = 'none');
    (document.getElementById('fileListContainer') || {}).style && (document.getElementById('fileListContainer').style.display = 'none');

    window.setupMode = true;
    await primeCsrf();

    try { await import('/js/adminPanel.js?v={{APP_QVER}}'); } catch { }
    try { document.dispatchEvent(new Event('DOMContentLoaded')); } catch { }

    const addModal = document.getElementById('addUserModal'); if (addModal) addModal.style.display = 'block';

    const lu = document.getElementById('loginUsername'); if (lu) { lu.removeAttribute('autofocus'); lu.disabled = true; }
    const lp = document.getElementById('loginPassword'); if (lp) lp.disabled = true;

    document.querySelectorAll('[autofocus]').forEach(el => el.removeAttribute('autofocus'));
    bindSetupAddUser();
  }

  // ---------- HEAVY BOOT ----------
  async function bootHeavy() {
    if (window.__FR_FLAGS.bootPromise) return window.__FR_FLAGS.bootPromise;
    window.__FR_FLAGS.bootPromise = (async () => {
      if (window.__FR_FLAGS.booted) return; // no-op if somehow set
      window.__FR_FLAGS.booted = true;
      ensureToastReady();
      // show chrome

      const hb = document.querySelector('.header-buttons'); if (hb) hb.style.visibility = 'visible';
      const ov = document.getElementById('loadingOverlay'); if (ov) ov.style.display = 'flex';

      try {
        // 0) refresh auth snapshot (once)
        let state = {};
        try {
          const r = await fetch('/api/auth/checkAuth.php', { credentials: 'include' });
          state = await r.json();
          if (state && state.username) localStorage.setItem('username', state.username);
          if (typeof state.isAdmin !== 'undefined') localStorage.setItem('isAdmin', state.isAdmin ? '1' : '0');
          window.__FR_AUTH_STATE = state;
        } catch { }

        // authed → heavy boot path
        document.body.classList.add('authed');

        // 1) i18n (safe)
        // i18n: honor saved language first, then apply translations
        try {
          const i18n = await import('/js/i18n.js?v={{APP_QVER}}').catch(() => import('/js/i18n.js'));
          let saved = 'en';
          try { saved = localStorage.getItem('language') || 'en'; } catch { }
          if (typeof i18n.setLocale === 'function') { await i18n.setLocale(saved); }
          if (typeof i18n.applyTranslations === 'function') { i18n.applyTranslations(); }
          try { document.documentElement.setAttribute('lang', saved); } catch { }
        } catch { }
        // 2) core app — **initialize exactly once** (this calls initUpload/initFileActions/loadFolderTree/etc.)
        const app = await import('/js/appCore.js?v={{APP_QVER}}');
        if (!window.__FR_FLAGS.initialized) {
          if (typeof app.loadCsrfToken === 'function') await app.loadCsrfToken();
          if (typeof app.initializeApp === 'function') app.initializeApp();
          const darkBtn = document.getElementById('darkModeToggle');
          if (darkBtn) {
            darkBtn.removeAttribute('hidden');
            darkBtn.style.setProperty('display', 'inline-flex', 'important'); // beats any CSS
            darkBtn.style.visibility = ''; // just in case
          }
          const link = document.createElement('link');
          link.rel = 'stylesheet';
          link.href = '/css/vendor/material-icons.css?v={{APP_QVER}}';
          document.head.appendChild(link);


          window.__FR_FLAGS.initialized = true;

          try {
            if (!sessionStorage.getItem('__fr_welcomed')) {
              const name = (window.__FR_AUTH_STATE?.username) || localStorage.getItem('username') || '';
              const safe = String(name).replace(/[\r\n<>]/g, '').trim().slice(0, 60);

              window.showToast(safe ? `Welcome back, ${safe}!` : 'Welcome!', 3000);
              sessionStorage.setItem('__fr_welcomed', '1'); // prevent repeats on reload
            }
          } catch { }
        }


        // 3) auth/header bits — pass real state so “Admin Panel” shows up
        if (!window.__FR_FLAGS.wired.auth) {
          try {
            const auth = await import('/js/auth.js?v={{APP_QVER}}');
            auth.updateLoginOptionsUIFromStorage && auth.updateLoginOptionsUIFromStorage();
            auth.applyProxyBypassUI && auth.applyProxyBypassUI();
            auth.updateAuthenticatedUI && auth.updateAuthenticatedUI(state);

            //  bind ALL the admin / change-password buttons once
            if (!window.__FR_FLAGS.wired.authInit && typeof auth.initAuth === 'function') {
              try { auth.initAuth(); } catch (e) { console.warn('[auth] initAuth failed', e); }
              window.__FR_FLAGS.wired.authInit = true;
            }
          } catch (e) {
            console.warn('[auth] import failed', e);
          }
          wireUserNameLabel(state);
          window.__FR_FLAGS.wired.auth = true;
        }

        // 4) legacy ready **only once** (prevents loops)
        dispatchLegacyReadyOnce();

        // 5) first file list — once (initializeApp doesn’t fetch list)
        if (!window.__FR_FLAGS.wired.firstList) {
          try {
            const flv = await import('/js/fileListView.js?v={{APP_QVER}}');
            window.currentFolder ||= 'root';
            if (typeof flv.loadFileList === 'function') await flv.loadFileList(window.currentFolder);
            const list = document.getElementById('fileListContainer'); if (list) list.style.display = '';
            updateFileListTitle();
          } catch { }
          window.__FR_FLAGS.wired.firstList = true;
        }
        // 6) light UI wiring — once each (no confirm bindings here; your modules own them)
        if (!window.__FR_FLAGS.wired.dark) { bindDarkMode(); window.__FR_FLAGS.wired.dark = true; }
        if (!window.__FR_FLAGS.wired.create) { wireCreateDropdown(); window.__FR_FLAGS.wired.create = true; }
        if (!window.__FR_FLAGS.wired.folder) { wireFolderButtons(); window.__FR_FLAGS.wired.folder = true; }
        if (!window.__FR_FLAGS.wired.lift) { liftModals(); window.__FR_FLAGS.wired.lift = true; }
        if (!window.__FR_FLAGS.wired.cancel) { bindCancelSafeties(); window.__FR_FLAGS.wired.cancel = true; }
        if (!window.__FR_FLAGS.wired.dragScroll) { bindDragAutoScroll(); window.__FR_FLAGS.wired.dragScroll = true; }
        wireModalEnterDefault();


      } catch (e) {
        console.error('[main] heavy boot failed', e);
        alert('Failed to load app');
      } finally {
        if (ov) ov.style.display = 'none';
        window.setupMode = false;
      }
    })();
    return window.__FR_FLAGS.bootPromise;
  }

  // ---------- entry (no flicker: decide state BEFORE showing login) ----------
  document.addEventListener('DOMContentLoaded', async () => {
    if (window.__FR_FLAGS.entryStarted) return;
    window.__FR_FLAGS.entryStarted = true;

    // Always start clean
    document.body.classList.remove('authed');

    const overlay = document.getElementById('loadingOverlay');
    const wrap = document.querySelector('.main-wrapper');   // app shell
    const mainEl = document.getElementById('main');           // contains loginForm
    const login = document.getElementById('loginForm');

    bindDarkMode();
    await loadSiteConfig();

    const { authed, setup } = await checkAuth();

    if (setup) {
      // Setup wizard runs inside app shell
      unhide(wrap);
      hideEl(login);
      await bootSetupWizard();
      await revealAppAndHideOverlay();

      return;
    }

    if (authed) {
      // Authenticated path: show app, hide login
      document.body.classList.add('authed');
      unhide(wrap);            // works whether CSS or [hidden] was used
      hideEl(login);
      await bootHeavy();
      await revealAppAndHideOverlay();
      requestAnimationFrame(() => {
        const pre = document.getElementById('pretheme-css');
        if (pre) pre.remove();
      });
      return;
    }

    // ---- NOT AUTHED: show only the login view ----
    hideEl(wrap);              // ensure app shell stays hidden while logged out
    unhide(mainEl);
    unhide(login);
    if (login) login.style.display = '';
    // …wire stuff…
    applySiteConfig(window.__FR_SITE_CFG__ || {}, { phase: 'final' });
    await revealAppAndHideOverlay();
    const hb = document.querySelector('.header-buttons');
    if (hb) hb.style.visibility = 'hidden';

    // keep app cards inert while logged out (no layout poke)
    ['uploadCard', 'folderManagementCard'].forEach(id => {
      const el = document.getElementById(id);
      if (!el) return;
      el.setAttribute('aria-hidden', 'true');
      try { el.inert = true; } catch { }
    });

    bindLogin();
    wireCreateDropdown();
    keepCreateDropdownWired();
    wireModalEnterDefault();
    showLoginTip('Please log in to continue');

    if (overlay) overlay.style.display = 'none';
  }, { once: true });
})();


// --- Mobile switcher + PWA SW (mobile-only) ---
(() => {
  // keep it simple + robust
  const qs = new URLSearchParams(location.search);
  const hasFrAppHint = qs.get('frapp') === '1';

  const isStandalone =
    (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) ||
    (typeof navigator.standalone === 'boolean' && navigator.standalone);

  const isCapUA = /\bCapacitor\b/i.test(navigator.userAgent);
  const hasCapBridge = !!(window.Capacitor && window.Capacitor.Plugins);

  // “mobile-ish”: native mobile UAs OR touch + reasonably narrow viewport (covers iPad-on-Mac UA)
  const isMobileish =
    /Android|iPhone|iPad|iPod|Mobile|Silk|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
    (navigator.maxTouchPoints > 1 && Math.min(screen.width, screen.height) <= 900);

  // load the switcher only in the mobile app, or mobile standalone PWA, or when explicitly hinted
  const shouldLoadSwitcher =
    hasCapBridge || isCapUA || (isStandalone && isMobileish) || (hasFrAppHint && isMobileish);

  // expose a flag to inspect later
  window.FR_APP = !!(hasCapBridge || isCapUA || (isStandalone && isMobileish));

  const QVER = (window.APP_QVER && String(window.APP_QVER)) || '{{APP_QVER}}';

  if (shouldLoadSwitcher) {
    import(`/js/mobile/switcher.js?v=${encodeURIComponent(QVER)}`)
      .then(() => {
        if (hasFrAppHint && !sessionStorage.getItem('frx_opened_once')) {
          sessionStorage.setItem('frx_opened_once', '1');
          window.dispatchEvent(new CustomEvent('frx:openSwitcher'));
        }
      })
      .catch(err => console.info('[FileRise] switcher import failed:', err));
  }

  // SW only for web (https or localhost), never in Capacitor
  const onHttps = location.protocol === 'https:' || location.hostname === 'localhost';
  if ('serviceWorker' in navigator && onHttps && !hasCapBridge && !isCapUA) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register(`/js/pwa/sw.js?v=${encodeURIComponent(QVER)}`).catch(() => {});
    });
  }
})();