(() => {
  const { useState, useEffect, useMemo, useRef } = React;
  const HiraApi = window.HiraApi;
  const HiraStorage = window.HiraStorage;
  const HiraFilters = window.HiraFilters;
  const HiraFormat = window.HiraFormat;
  const HiraValidation = window.HiraValidation;
  const HiraRuntime = window.HiraRuntime;
  const { Icon } = window.HiraIcons;
  const { useTheme, Toast, ErrorBoundary, SkeletonGrid } = window.HiraUI;
  const { Masonry, ShareSheet, LibraryScrollStrip } = window.HiraGallery;
  const { UploadModal, DevAuthModal, EditModal, BatchEditModal, ImportModal, ImportFullBackupModal, DeleteConfirmModal, ExpiryPickerModal } = window.HiraModals;
  const { SettingsDrawer, AdminDashboard, ShareLinksPanel, TagManagerPanel } = window.HiraAdmin;
  const { NAV, HOME_FILTERS, CONTENT_FILTERS, SMART_ALBUMS, REQUIRED_GLOBALS } = window.HiraConstants;
  const getImageContent = HiraFilters.getImageContent;
  const API = HiraApi.API;
  const handleAuthFail = (message) => HiraRuntime.handleAuthFail(message);

// ── App ───────────────────────────────────────────────────────────────────────
function App() {
  const [dark, setDarkState] = useState(() => {
    try {
      const saved = HiraStorage.localGet("hira_dark", null);
      if (saved === "0") return false;
      if (saved === "1") return true;
    } catch (_) {}
    return true;
  });
  const setDark = (updater) => {
    setDarkState((prev) => {
      const next = typeof updater === "function" ? updater(prev) : updater;
      try { HiraStorage.localSet("hira_dark", next ? "1" : "0"); } catch (_) {}
      return next;
    });
  };
  const T = useTheme(dark);

  const [images, setImages] = useState([]);
  const [libraries, setLibraries] = useState([]);
  const [accounts, setAccounts] = useState([]);
  const [allTags, setAllTags] = useState([]);
  const [loading, setLoading] = useState(true);
  const [errorBanner, setErrorBanner] = useState("");

  const [page, setPage] = useState("home");
  const [activeLibId, setActiveLibId] = useState(null);
  const [activeSmartId, setActiveSmartId] = useState(null);
  const [activePinId, setActivePinId] = useState(null);
  const [query, setQuery] = useState("");
  const [searchSort, setSearchSort] = useState("newest");
  const [serverSearchImages, setServerSearchImages] = useState([]);
  const [searchLoading, setSearchLoading] = useState(false);
  const [searchHasMore, setSearchHasMore] = useState(false);
  const [searchLoadingMore, setSearchLoadingMore] = useState(false);
  const searchSentinelRef = useRef(null);
  const [trashImages, setTrashImages] = useState([]);
  const [onlyFav, setOnlyFav] = useState(false);
  const [activeContentFilter, setActiveContentFilter] = useState("all");
  const [favorites, setFavorites] = useState(new Set());
  const [showUpload, setShowUpload] = useState(false);
  const [showDrawer, setShowDrawer] = useState(false);
  const [showAdvanced, setShowAdvanced] = useState(false);
  const [activeTagFilter, setActiveTagFilter] = useState("");
  const [activeLibFilter, setActiveLibFilter] = useState("");
  const [homeFilter, setHomeFilterState] = useState(() => {
    try {
      const saved = HiraStorage.localGet("hira_home_filter", null);
      if (saved && HOME_FILTERS.some((f) => f.id === saved)) return saved;
    } catch (_) {}
    return "all";
  });
  const setHomeFilter = (id) => {
    setHomeFilterState(id);
    try { HiraStorage.localSet("hira_home_filter", id); } catch (_) {}
  };
  const [savePickerFor, setSavePickerFor] = useState(null);
  const [lightbox, setLightbox] = useState(false);
  const [sheetItem, setSheetItem] = useState(null);
  const [editItem, setEditItem] = useState(null);
  const [devUnlocked, setDevUnlocked] = useState(false);
  const [deleteEnabled, setDeleteEnabled] = useState(false);
  const [batchMode, setBatchMode] = useState(false);
  const [batchSelected, setBatchSelected] = useState(new Set());
  const [showBatchEdit, setShowBatchEdit] = useState(false);
  const [showImport, setShowImport] = useState(false);
  const [showImportFull, setShowImportFull] = useState(false);
  const [deleteConfirm, setDeleteConfirm] = useState({ visible: false, item: null });
  const [importResult, setImportResult] = useState(null);
  const [toast, setToast] = useState({ visible: false, message: "", type: "info" });
  const [showDevAuth, setShowDevAuth] = useState(false);
  const [pendingAction, setPendingAction] = useState(null);
  const [hasMoreImages, setHasMoreImages] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [openMode, setOpenMode] = useState(false);
  const [turnstileSiteKey, setTurnstileSiteKey] = useState(null);
  const [offline, setOffline] = useState(() => typeof navigator !== "undefined" ? !navigator.onLine : false);
  const [shareExpiryRequest, setShareExpiryRequest] = useState(null);
  const sentinelRef = useRef(null);
  const imagesRef = useRef(images);
  useEffect(() => { imagesRef.current = images; }, [images]);

  useEffect(() => {
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("sw.js").catch(() => {});
    }
    const syncOnline = () => setOffline(!navigator.onLine);
    window.addEventListener("online", syncOnline);
    window.addEventListener("offline", syncOnline);
    return () => {
      window.removeEventListener("online", syncOnline);
      window.removeEventListener("offline", syncOnline);
    };
  }, []);

  const showToast = (message, type = "success") => {
    setToast({ visible: true, message, type });
  };

  // Session-based auth: password stored in sessionStorage only
  useEffect(() => {
    const pwd = HiraStorage.sessionGet("hira_access_pwd", "");
    setDevUnlocked(!!pwd);
    setDeleteEnabled(HiraStorage.localGet("hira_delete_enabled", "0") === "1");

    HiraApi.checkAuth(pwd || undefined)
      .then((data) => {
        if (data.openMode) {
          setOpenMode(true);
          setDevUnlocked(true);
        } else if (pwd) {
          setDevUnlocked(true);
        }
      })
      .catch(() => {});

    HiraApi.getAuthConfig()
      .then((data) => { if (data.turnstileSiteKey) setTurnstileSiteKey(data.turnstileSiteKey); })
      .catch(() => {});
  }, []);

  const unlockDev = async (pwd, turnstileToken) => {
    try {
      const data = await HiraApi.checkAuth(pwd, turnstileToken);
      if (data.openMode) {
        setOpenMode(true);
        setDevUnlocked(true);
      } else {
        HiraStorage.sessionSet("hira_access_pwd", pwd);
        setDevUnlocked(true);
      }
      setShowDevAuth(false);
      if (pendingAction) {
        const action = pendingAction;
        setPendingAction(null);
        action();
      }
      return true;
    } catch (_) {
      return false;
    }
  };

  const requireAuth = (action) => {
    const pwd = HiraStorage.sessionGet("hira_access_pwd", "");
    // openMode = ADMIN_TOKEN kosong di server → edit bebas tanpa password
    if (pwd || openMode || devUnlocked) {
      action();
    } else {
      setPendingAction(() => action);
      setShowDevAuth(true);
    }
  };

  useEffect(() => {
    const onAuthFail = (event) => {
      setDevUnlocked(false);
      setShowDevAuth(true);
      if (event && event.detail && event.detail.message) showToast(event.detail.message, "error");
    };
    const onGlobalError = (event) => {
      if (event && event.detail && event.detail.message) showToast(event.detail.message, "error");
    };
    window.addEventListener("hira:authfail", onAuthFail);
    window.addEventListener("hira:global-error", onGlobalError);
    return () => {
      window.removeEventListener("hira:authfail", onAuthFail);
      window.removeEventListener("hira:global-error", onGlobalError);
    };
  }, []);

  const toggleBatchMode = () => {
    setBatchMode((v) => {
      if (v) setBatchSelected(new Set());
      return !v;
    });
  };
  const toggleBatchItem = (id) => {
    setBatchSelected((prev) => {
      const n = new Set(prev);
      n.has(id) ? n.delete(id) : n.add(id);
      return n;
    });
  };
  const batchSelectAll = () => { setBatchSelected(new Set(images.map((i) => i.id))); };
  const batchClear = () => { setBatchSelected(new Set()); setBatchMode(false); };
  const batchFavorite = async () => {
    const selected = [...batchSelected]
      .map((id) => images.find((i) => i.id === id))
      .filter(Boolean);
    for (const img of selected) await toggleFav(img);
    setBatchSelected(new Set());
    setBatchMode(false);
    showToast(`${selected.length} gambar diperbarui favoritnya`, "success");
  };
  const batchDelete = async () => {
    if (!batchSelected.size) return;
    if (!window.confirm(`Hapus ${batchSelected.size} gambar?`)) return;
    requireAuth(async () => {
      const ids = [...batchSelected];
      let ok = 0;
      for (const id of ids) {
        try {
          await HiraApi.deleteImage(id);
          ok += 1;
        } catch (error) {
          if (error.status === 401) { handleAuthFail(); return; }
        }
      }
      setImages((prev) => prev.filter((i) => !batchSelected.has(i.id)));
      setBatchSelected(new Set());
      setBatchMode(false);
      showToast(`${ok} gambar dihapus`, ok ? "success" : "error");
      load();
    });
  };

  const toggleDeleteEnabled = () => {
    setDeleteEnabled((v) => {
      HiraStorage.localSet("hira_delete_enabled", !v ? "1" : "0");
      return !v;
    });
  };

  const renameLibrary = async (id, newName) => {
    try {
      const validation = HiraValidation.validateLibraryName(newName);
      if (!validation.valid) throw new Error(validation.message);
      await HiraApi.updateLibrary(id, { name: validation.value });
      setLibraries((prev) => prev.map((library) => (library.id === id ? { ...library, name: validation.value } : library)));
      showToast("Nama koleksi diperbarui", "success");
    } catch (error) {
      if (error.status === 401) handleAuthFail();
      else {
        console.error("Rename library:", error);
        showToast(error.message || "Gagal mengubah nama koleksi", "error");
      }
    }
  };

  const deleteLibrary = async (id) => {
    try {
      await HiraApi.deleteLibrary(id);
      setLibraries((prev) => prev.filter((library) => library.id !== id));
      setImages((prev) => prev.map((img) => {
        const next = { ...img };
        if (next.libraryId === id) next.libraryId = null;
        next.savedTo = (next.savedTo || []).filter((libraryId) => libraryId !== id);
        return next;
      }));
      showToast("Koleksi dihapus", "success");
    } catch (error) {
      if (error.status === 401) handleAuthFail();
      else {
        console.error("Delete library:", error);
        showToast(error.message || "Gagal menghapus koleksi", "error");
      }
    }
  };

  const createLibraryFromSettings = async () => {
    const name = prompt("Nama koleksi baru:");
    if (!name?.trim()) return;
    try {
      const validation = HiraValidation.validateLibraryName(name);
      if (!validation.valid) throw new Error(validation.message);
      const data = await HiraApi.createLibrary(validation.value);
      if (data.library) {
        setLibraries((prev) => [...prev, data.library]);
        showToast("Koleksi dibuat", "success");
      } else {
        showToast(data.error || "Gagal membuat koleksi", "error");
      }
    } catch (error) {
      if (error.status === 401) handleAuthFail();
      else {
        console.error("Create library:", error);
        showToast(error.message || "Gagal membuat koleksi", "error");
      }
    }
  };

  const PAGE_SIZE = 30;

  const load = async (append = false) => {
    try {
      const offset = append ? imagesRef.current.length : 0;
      const fetches = [
        HiraApi.getImages({ limit: PAGE_SIZE, offset }),
      ];
      if (!append) {
        fetches.push(
          HiraApi.getLibraries(),
          HiraApi.getDriveAccounts().catch((error) => {
            if (error.status === 401) return { accounts: [] };
            throw error;
          }),
          HiraApi.getTags(),
        );
      }
      const results = await Promise.all(fetches);
      const imgRes = results[0];
      const imgs = imgRes.images || [];
      if (append) {
        setImages((prev) => {
          const seen = new Set(prev.map((i) => i.id));
          return [...prev, ...imgs.filter((i) => !seen.has(i.id))];
        });
        setFavorites((prev) => {
          const n = new Set(prev);
          imgs.filter((i) => i.favorite).forEach((i) => n.add(i.id));
          return n;
        });
      } else {
        setImages(imgs);
        setFavorites(new Set(imgs.filter((i) => i.favorite).map((i) => i.id)));
        if (results[1]) setLibraries(results[1].libraries || []);
        if (results[2]) setAccounts(results[2].accounts || []);
        if (results[3]) setAllTags(results[3].tags || []);
        setErrorBanner("");
      }
      const total = typeof imgRes.total === "number" ? imgRes.total : null;
      if (total !== null) {
        const loaded = (append ? imagesRef.current.length : 0) + imgs.length;
        setHasMoreImages(loaded < total && imgs.length > 0);
      } else {
        setHasMoreImages(imgs.length === PAGE_SIZE);
      }
    } catch (e) {
      if (!append) setErrorBanner("Gagal terhubung ke server. Cek koneksi atau URL API.");
    } finally {
      if (!append) setLoading(false);
    }
  };

  useEffect(() => { load(); }, []);

  // Infinite scroll: load more when sentinel is visible
  useEffect(() => {
    const el = sentinelRef.current;
    if (!el) return;
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasMoreImages && !loadingMore && !loading) {
          setLoadingMore(true);
          load(true).finally(() => setLoadingMore(false));
        }
      },
      { rootMargin: "600px" }
    );
    observer.observe(el);
    return () => observer.disconnect();
  }, [hasMoreImages, loadingMore, loading, page]);

  const toggleFav = async (img) => {
    if (!img?.id) return;
    const willFav = !favorites.has(img.id);
    // Optimistic update
    setFavorites((prev) => {
      const n = new Set(prev);
      willFav ? n.add(img.id) : n.delete(img.id);
      return n;
    });
    setImages((prev) => prev.map((i) => (i.id === img.id ? { ...i, favorite: willFav } : i)));
    try {
      await HiraApi.updateImage(img.id, { favorite: willFav });
      showToast(
        willFav ? `"${img.title}" ditambahkan ke favorit` : `"${img.title}" dihapus dari favorit`,
        "success"
      );
    } catch (_) {
      // rollback
      setFavorites((prev) => {
        const n = new Set(prev);
        willFav ? n.delete(img.id) : n.add(img.id);
        return n;
      });
      setImages((prev) => prev.map((i) => (i.id === img.id ? { ...i, favorite: !willFav } : i)));
      showToast("Gagal memperbarui favorit", "error");
    }
  };

  const toggleSave = async (imgId, libId) => {
    const prevImages = images;
    setImages((prev) => prev.map((i) => {
      if (i.id !== imgId) return i;
      const savedTo = i.savedTo || [];
      const has = savedTo.includes(libId);
      return { ...i, savedTo: has ? savedTo.filter((x) => x !== libId) : [...savedTo, libId] };
    }));
    try {
      await HiraApi.updateImage(imgId, { saveToLibraryId: libId });
      showToast("Koleksi diperbarui", "success");
    } catch (error) {
      setImages(prevImages);
      if (error.status === 401) handleAuthFail();
      else showToast("Gagal menyimpan ke koleksi", "error");
    }
  };

  const deleteImage = async (img) => {
    requireAuth(() => {
      setDeleteConfirm({ visible: true, item: img });
    });
  };

  const confirmDelete = async () => {
    const img = deleteConfirm.item;
    if (!img) return;
    setDeleteConfirm({ visible: false, item: null });
    const snapshot = images;
    setImages((prev) => prev.filter((i) => i.id !== img.id));
    if (activePinId === img.id) goto("home");
    try {
      await HiraApi.deleteImage(img.id);
      showToast(`"${img.title}" dihapus`, "success");
      load();
    } catch (error) {
      setImages(snapshot);
      if (error.status === 401) handleAuthFail();
      else showToast("Gagal menghapus gambar", "error");
    }
  };

  const goto = (id) => { setPage(id); setActiveLibId(null); setActiveSmartId(null); };
  const openSmart = (id) => { setActiveSmartId(id); setActiveLibId(null); setPage("smart"); };
  const openPin = (item) => { setActivePinId(item.id); setPage("pin"); };

  const activePin = images.find((i) => i.id === activePinId);
  const activeLib = libraries.find((l) => l.id === activeLibId);
  const activeSmart = SMART_ALBUMS.find((album) => album.id === activeSmartId);

  // Performance: memoize grouping per collection
  // Search is deliberately server-side; debounce keeps typing inexpensive.
  // Builds search params; helper reused by initial fetch and load-more.
  function buildSearchParams(offset) {
    const p = new URLSearchParams({ limit: "30", offset: String(offset || 0), q: query.replace(/^#/, ""), sort: searchSort });
    if (query.trim().startsWith("#")) p.set("tag", query.trim().slice(1));
    if (activeContentFilter !== "all") { const contentMap={sfw_anime:"sfw-anime",anime_ecchi:"anime",ecchi_nudity:"ecchi",hentai:"hentai",restricted:"restricted",unrestricted:"unrestricted"}; p.set("content",contentMap[activeContentFilter]); }
    if (activeTagFilter) p.set("tag", activeTagFilter); if (activeLibFilter) p.set("libraryId", activeLibFilter); if (onlyFav) p.set("favorite","true");
    return p;
  }
  useEffect(() => {
    if (page !== "search") return;
    const timer = setTimeout(async () => { setSearchLoading(true); setSearchHasMore(false); try {
      const d = await HiraApi.getImages(Object.fromEntries(buildSearchParams(0).entries()));
      const imgs=d.images||[]; setServerSearchImages(imgs); setSearchHasMore(!!d.hasMore);
    } catch(e) { showToast(e.message||"Gagal mencari", "error"); } finally { setSearchLoading(false); } }, 300);
    return () => clearTimeout(timer);
  }, [page, query, searchSort, activeContentFilter, activeTagFilter, activeLibFilter, onlyFav]);

  // Infinite scroll for search page
  const loadMoreSearch = async () => {
    if (searchLoadingMore || !searchHasMore) return;
    setSearchLoadingMore(true);
    try {
      const d = await HiraApi.getImages(Object.fromEntries(buildSearchParams(serverSearchImages.length).entries()));
      const imgs=d.images||[];
      setServerSearchImages(prev => { const seen = new Set(prev.map(i=>i.id)); return [...prev, ...imgs.filter(i=>!seen.has(i.id))]; });
      setSearchHasMore(!!d.hasMore);
    } catch(e) { /* silent */ } finally { setSearchLoadingMore(false); }
  };
  useEffect(() => {
    const el = searchSentinelRef.current;
    if (!el || page !== "search") return;
    const observer = new IntersectionObserver(
      (entries) => { if (entries[0].isIntersecting && searchHasMore && !searchLoadingMore && !searchLoading) loadMoreSearch(); },
      { rootMargin: "600px" }
    );
    observer.observe(el);
    return () => observer.disconnect();
  }, [searchHasMore, searchLoadingMore, searchLoading, page, serverSearchImages.length]);

  const loadTrash = async () => {
    try {
      const data = await HiraApi.getTrashImages({ limit: 200 });
      setTrashImages(data.images || []);
    } catch (error) {
      if (error.status === 401) handleAuthFail();
      else showToast(error.message || "Gagal memuat sampah", "error");
    }
  };

  const exportJson = async () => {
    try {
      const data = await HiraApi.exportImages();
      const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `hira-export-${new Date().toISOString().slice(0,10)}.json`;
      a.click();
      URL.revokeObjectURL(url);
      showToast(`Export ${data.count || 0} gambar`, "success");
    } catch (error) {
      if (error.status === 401) handleAuthFail();
      else showToast("Export gagal: " + error.message, "error");
    }
  };

  const exportFullJson = async (includeShareTokens) => {
    try {
      const data = await HiraApi.exportFullBackup(includeShareTokens);
      const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `hira-full-backup-${new Date().toISOString().slice(0,10)}.json`;
      a.click();
      URL.revokeObjectURL(url);
      showToast("Full backup v2 berhasil diunduh", "success");
    } catch (error) {
      if (error.status === 401) handleAuthFail();
      else showToast("Export full backup gagal: " + error.message, "error");
    }
  };

  const copyPublicShare = (type, targetId) => {
    requireAuth(() => {
      setShareExpiryRequest({ type, targetId });
    });
  };

  const batchMetadata = () => { if (!batchSelected.size) return; requireAuth(() => setShowBatchEdit(true)); };

  const itemsByLibrary = useMemo(() => {
    const map = {};
    images.forEach((it) => {
      const ids = [it.libraryId, ...(it.savedTo || [])].filter(Boolean);
      ids.forEach((id) => {
        if (!map[id]) map[id] = [];
        map[id].push(it);
      });
    });
    return map;
  }, [images, libraries]);

  const getLibraryCoverId = (lib) => {
    if (!lib) return null;
    return lib.coverImageId || lib.cover_image_id || null;
  };
  const getLibraryCoverImage = (lib) => {
    const cid = getLibraryCoverId(lib);
    if (!cid) return null;
    return images.find(i=>i.id===cid) || null;
  };
  const libraryItems = (libId) => {
    const base = itemsByLibrary[libId] || [];
    const lib = libraries.find(l=>l.id===libId);
    const coverId = lib ? getLibraryCoverId(lib) : null;
    if (!coverId) return base;
    const coverImg = base.find(i=>i.id===coverId) || images.find(i=>i.id===coverId);
    if (!coverImg) return base;
    return [coverImg, ...base.filter(i=>i.id!==coverId)];
  };
  const setLibraryCover = async (libraryId, imageId) => {
    requireAuth(async () => {
      try {
        await HiraApi.updateLibrary(libraryId, { coverImageId: imageId });
        setLibraries(prev=>prev.map(l=>l.id===libraryId ? { ...l, coverImageId: imageId, cover_image_id: imageId } : l));
        showToast(`Cover koleksi "${libraries.find(l=>l.id===libraryId)?.name||libraryId}" diperbarui`, "success");
      } catch (error) {
        if (error.status === 401) handleAuthFail();
        else showToast(error.message||"Gagal set cover","error");
      }
    });
  };
  const clearLibraryCover = async (libraryId) => {
    requireAuth(async () => {
      try {
        await HiraApi.updateLibrary(libraryId, { coverImageId: null });
        setLibraries(prev=>prev.map(l=>l.id===libraryId ? { ...l, coverImageId: null, cover_image_id: null } : l));
        showToast("Cover manual dihapus, fallback ke terbaru","success");
      } catch (error) {
        if (error.status === 401) handleAuthFail();
        else showToast(error.message||"Gagal hapus cover","error");
      }
    });
  };

  const searchResults = serverSearchImages;

  // Auto-load trash when entering admin page
  useEffect(() => { if (page === "admin") loadTrash(); }, [page]);

  const relatedForPin = useMemo(() => {
    if (!activePin) return [];
    return images.filter((it) => it.id !== activePin.id && (
      it.libraryId === activePin.libraryId ||
      (it.tags || []).some((t) => (activePin.tags || []).includes(t))
    )).slice(0, 12);
  }, [activePin, images]);

  const filteredHomeImages = useMemo(() => {
    const contentFilter = HOME_FILTERS.find((filter) => filter.id === homeFilter) || HOME_FILTERS[0];
    return images.filter((image) => contentFilter.match(getImageContent(image)));
  }, [images, homeFilter]);

  const smartItems = useMemo(() => HiraFilters.filterSmartAlbum(activeSmartId, images), [activeSmartId, images]);
  const smartCounts = useMemo(() => Object.fromEntries(SMART_ALBUMS.map((album) => [album.id, HiraFilters.filterSmartAlbum(album.id, images).length])), [images]);

  return (
    <div className={`min-h-screen ${T.bg} ${T.text} flex`} style={{ fontFamily: "'Zen Kaku Gothic New', sans-serif" }}>
      

      {/* Sidebar desktop */}
      <nav className={`hidden sm:flex w-20 shrink-0 border-r ${T.railBg} flex-col items-center py-5 gap-2 sticky top-0 h-screen`}>
        <button onClick={() => goto("home")} className="mb-4 text-amber-400 text-xl" aria-label="Beranda">灯</button>
        {NAV.map((n) => {
          const Ic = Icon[n.icon];
          const isActive = n.id === "menu" ? showDrawer : page === n.id;
          return (
            <button key={n.id} title={n.label} aria-label={n.label}
              onClick={() => n.id === "create" ? requireAuth(() => setShowUpload(true)) : n.id === "menu" ? setShowDrawer(true) : goto(n.id)}
              className={`w-11 h-11 rounded-xl flex items-center justify-center smooth-nav ${isActive ? "bg-amber-400 text-[#0a0c16]" : `${T.sub} ${T.hoverSoft}`}`}>
              <Ic />
            </button>
          );
        })}
      </nav>

      {/* Nav mobile */}
      <nav className={`sm:hidden fixed bottom-0 inset-x-0 z-40 border-t ${T.railBg} ${T.bg} flex items-center justify-around py-2`}>
        {NAV.map((n) => {
          const Ic = Icon[n.icon];
          const isActive = n.id === "menu" ? showDrawer : page === n.id;
          return (
            <button key={n.id} aria-label={n.label}
              onClick={() => n.id === "create" ? requireAuth(() => setShowUpload(true)) : n.id === "menu" ? setShowDrawer(true) : goto(n.id)}
              className={`p-2.5 rounded-full smooth-nav ${isActive ? "bg-amber-400 text-[#0a0c16]" : T.sub}`}>
              <Ic />
            </button>
          );
        })}
      </nav>

      <div className="flex-1 min-w-0 pb-20 sm:pb-0">
        {offline && <div className="bg-amber-400/15 text-amber-300 text-xs px-5 py-2">Mode offline — UI tetap terbuka, data terbaru akan dimuat saat koneksi kembali.</div>}
        {errorBanner && <div className="bg-rose-500/10 text-rose-300 text-xs px-5 py-2">{errorBanner}</div>}

        {/* ── Home ── */}
        {page === "home" && (
          <div className="page-enter">
            <div className="flex items-center justify-between px-5 sm:px-8 pt-8 pb-2">
              <div>
                <p className="text-xs tracking-[0.3em] text-amber-400/70 uppercase mb-2">Selamat datang kembali</p>
                <h1 className="font-display text-3xl sm:text-4xl">Koleksimu</h1>
              </div>
              <div className="flex items-center gap-1">
                {devUnlocked && (
                  <button
                    type="button"
                    onClick={toggleBatchMode}
                    className={`hidden sm:inline-flex text-xs px-3 py-1.5 rounded-full border ${batchMode ? "bg-amber-400 text-[#0a0c16] border-amber-400" : T.chipInactive}`}
                    title="Mode pilih banyak"
                  >
                    {batchMode ? "Selesai pilih" : "Pilih"}
                  </button>
                )}
                <button onClick={() => setShowDrawer(true)} className={`sm:hidden p-2.5 rounded-xl ${T.hoverSoft}`}><Icon.Menu /></button>
              </div>
            </div>

            {/* Chip filter konten — discoverable di beranda */}
            <div className="px-5 sm:px-8 pb-3 flex gap-2 overflow-x-auto no-scrollbar">
              {HOME_FILTERS.map((f) => (
                <button
                  key={f.id}
                  type="button"
                  onClick={() => setHomeFilter(f.id)}
                  className={`shrink-0 text-xs px-3 py-1.5 rounded-full border transition-colors ${
                    homeFilter === f.id
                      ? "bg-amber-400 text-[#0a0c16] border-amber-400 font-medium"
                      : T.chipInactive
                  }`}
                >
                  {f.label}
                </button>
              ))}
            </div>

            <div className="px-5 sm:px-8 pb-6 grid grid-cols-2 lg:grid-cols-4 gap-3">
              {SMART_ALBUMS.map((album) => (
                <button key={album.id} onClick={() => openSmart(album.id)} className={`text-left rounded-2xl border p-4 ${T.panel} hover:border-amber-400/40 transition-colors`}>
                  <div className="flex items-center justify-between mb-3">
                    <span className="text-xl text-amber-400">{album.icon}</span>
                    <span className={`text-xs ${T.sub}`}>{smartCounts[album.id] || 0}</span>
                  </div>
                  <p className="font-display text-base">{album.label}</p>
                  <p className={`text-[11px] ${T.sub}`}>{album.description}</p>
                </button>
              ))}
            </div>

            <div>
              {libraries.map((lib) => {
                const libImgs = libraryItems(lib.id);
                if (libImgs.length === 0) return null;
                return (
                  <div key={lib.id} className="mb-8">
                    <button onClick={() => { setActiveLibId(lib.id); setPage("library"); }} className="w-full flex items-center justify-between px-5 sm:px-8 mb-3">
                      <div className="text-left">
                        <p className={`text-xs ${T.sub}`}>Ide-ide buat Anda · {libImgs.length} gambar</p>
                        <h2 className="font-display text-xl">{lib.name}</h2>
                      </div>
                      <span className={T.sub}><Icon.ChevronRight /></span>
                    </button>
                    <LibraryScrollStrip items={libImgs} onOpen={openPin} />
                  </div>
                );
              })}
            </div>
            <div className="px-5 sm:px-8 mt-2 mb-4 flex items-center justify-between gap-3">
              <p className={`text-xs uppercase tracking-widest ${T.sub}`}>Semua gambar</p>
              {devUnlocked && (
                <button
                  type="button"
                  onClick={toggleBatchMode}
                  className={`sm:hidden text-xs px-3 py-1 rounded-full border ${batchMode ? "bg-amber-400 text-[#0a0c16] border-amber-400" : T.chipInactive}`}
                >
                  {batchMode ? "Selesai" : "Pilih"}
                </button>
              )}
            </div>
            <main className="px-4 sm:px-8 pb-20">
              {loading ? <SkeletonGrid T={T} /> :
                !filteredHomeImages.length ? (
                  <div className="flex flex-col items-center justify-center py-20 text-center">
                    <Icon.Image className={`${T.faint} text-4xl mb-4`} />
                    <p className={`text-sm ${T.sub} mb-6 max-w-xs`}>Belum ada gambar. Sambungkan Google Drive dan unggah gambar pertama kamu.</p>
                    <div className="flex flex-col sm:flex-row gap-3">
                      <a href={`${API}/api/auth/login`} className="w-full sm:w-auto bg-amber-400 text-[#0a0c16] font-medium py-2.5 px-4 rounded-full text-center">Sambungkan Drive</a>
                      <button onClick={() => requireAuth(() => setShowUpload(true))} className="w-full sm:w-auto bg-amber-400/20 text-amber-400 border border-amber-400 font-medium py-2.5 px-4 rounded-full text-center">Unggah gambar</button>
                    </div>
                  </div>
                ) : (
                  <>
                    <Masonry items={filteredHomeImages} T={T} favorites={favorites} onToggleFav={toggleFav} onOpen={openPin} onMenu={setSheetItem} batchMode={batchMode} batchSelected={batchSelected} onBatchToggle={toggleBatchItem} />
                    <div ref={sentinelRef} className="h-4" />
                    {loadingMore && (
                      <div className="flex justify-center py-6">
                        <div className="flex gap-1.5">
                          <div className="w-2 h-2 rounded-full bg-amber-400 animate-bounce" style={{animationDelay:'0ms'}} />
                          <div className="w-2 h-2 rounded-full bg-amber-400 animate-bounce" style={{animationDelay:'150ms'}} />
                          <div className="w-2 h-2 rounded-full bg-amber-400 animate-bounce" style={{animationDelay:'300ms'}} />
                        </div>
                      </div>
                    )}
                    {!hasMoreImages && filteredHomeImages.length > 0 && (
                      <p className={`text-center text-xs ${T.sub} py-6`}>Semua gambar sudah dimuat</p>
                    )}
                  </>
                )}
            </main>
          </div>
        )}

        {/* ── Search ── */}
        {page === "search" && (
          <div className="pb-20 page-enter">
            {/* Sticky header */}
            <div className={`sticky top-0 z-10 px-5 sm:px-8 pt-6 pb-3 ${T.bg}`}>
              {/* Search bar */}
              <div className="flex gap-2 mb-3">
                <div className={`flex-1 flex items-center gap-2 border rounded-xl px-4 py-2.5 ${T.inputBg}`}>
                  <Icon.Search className={T.sub} />
                  <input autoFocus value={query} onChange={(e) => setQuery(e.target.value)}
                    placeholder="Cari judul, deskripsi, atau #tag..."
                    className="flex-1 bg-transparent outline-none text-sm" />
                  {query && <button onClick={() => setQuery("")} className={T.sub}><Icon.X /></button>}
                </div>
                <select value={searchSort} onChange={(e)=>setSearchSort(e.target.value)} className={`text-xs border rounded-xl px-2 ${T.inputBg}`}><option value="newest">Terbaru</option><option value="oldest">Terlama</option><option value="title">Judul</option><option value="favorite">Favorit</option></select>
                <button onClick={() => setOnlyFav((v) => !v)}
                  className={`px-3 rounded-xl border flex items-center gap-1 ${onlyFav ? "bg-amber-400 text-[#0a0c16] border-amber-400" : T.chipInactive}`}>
                  <Icon.Heart filled={onlyFav} />
                </button>
              </div>

              {/* Advanced toggle */}
              <button onClick={() => setShowAdvanced(v => !v)}
                className={`w-full flex items-center justify-between px-4 py-2.5 rounded-xl border transition-colors ${showAdvanced ? "border-amber-400/40 bg-amber-400/5" : T.chipInactive}`}>
                <span className={`flex items-center gap-2 text-sm ${showAdvanced ? "text-amber-400" : T.sub}`}>
                  <Icon.Filter />
                  Lanjutan
                  {(activeContentFilter !== "all" || activeTagFilter || activeLibFilter) && (
                    <span className="w-2 h-2 rounded-full bg-amber-400 inline-block" />
                  )}
                </span>
                <Icon.ChevronDown className={`transition-transform duration-200 ${showAdvanced ? "rotate-180 text-amber-400" : T.sub}`} />
              </button>
            </div>

            {/* Advanced filter panel */}
            {showAdvanced && (
              <div className={`mx-5 sm:mx-8 mt-1 mb-3 rounded-2xl border overflow-hidden panel-enter ${T.panel}`}>
                <div className={`px-4 py-3 border-b ${T.railBg}`}>
                  <p className="font-medium text-sm">Filter</p>
                </div>

                {/* Content */}
                <div className={`flex items-center gap-4 px-4 py-3 border-b ${T.railBg}`}>
                  <span className={`text-sm w-20 shrink-0 ${T.sub}`}>Content</span>
                  <select value={activeContentFilter} onChange={(e) => setActiveContentFilter(e.target.value)}
                    className={`flex-1 border rounded-lg px-3 py-2 text-sm ${T.inputBg}`}>
                    {CONTENT_FILTERS.map((filter) => <option key={filter.id} value={filter.id}>{filter.label}</option>)}
                  </select>
                </div>

                {/* Tag */}
                <div className={`flex items-center gap-4 px-4 py-3 border-b ${T.railBg}`}>
                  <span className={`text-sm w-20 shrink-0 ${T.sub}`}>Tag</span>
                  <select value={activeTagFilter} onChange={(e) => setActiveTagFilter(e.target.value)}
                    className={`flex-1 border rounded-lg px-3 py-2 text-sm ${T.inputBg}`}>
                    <option value="">Semua</option>
                    {allTags.map(({ tag, count }) => <option key={tag} value={tag}>#{tag} ({count})</option>)}
                  </select>
                </div>

                {/* Koleksi */}
                <div className={`flex items-center gap-4 px-4 py-3 ${(activeContentFilter !== "all" || activeTagFilter || activeLibFilter) ? `border-b ${T.railBg}` : ""}`}>
                  <span className={`text-sm w-20 shrink-0 ${T.sub}`}>Koleksi</span>
                  <select value={activeLibFilter} onChange={(e) => setActiveLibFilter(e.target.value)}
                    className={`flex-1 border rounded-lg px-3 py-2 text-sm ${T.inputBg}`}>
                    <option value="">Semua</option>
                    {libraries.map(l => <option key={l.id} value={l.id}>{l.name}</option>)}
                  </select>
                </div>

                {/* Reset */}
                {(activeContentFilter !== "all" || activeTagFilter || activeLibFilter) && (
                  <div className="px-4 py-2.5">
                    <button onClick={() => { setActiveContentFilter("all"); setActiveTagFilter(""); setActiveLibFilter(""); }}
                      className="text-xs text-amber-400 hover:text-amber-300">
                      Reset semua filter
                    </button>
                  </div>
                )}
              </div>
            )}

            {/* Results */}
            <div className="px-5 sm:px-8 pb-2">
              <p className={`text-xs ${T.sub} mb-4`}>{searchLoading ? "Mencari…" : `${searchResults.length} hasil`}{query && <> untuk "<span className="text-amber-400">{query}</span>"</>}</p>
            </div>
            <div className="px-4 sm:px-8">
              {searchLoading ? <SkeletonGrid T={T} /> : (
                <>
                  <Masonry items={searchResults} T={T} favorites={favorites} onToggleFav={toggleFav} onOpen={openPin} onMenu={setSheetItem} batchMode={batchMode} batchSelected={batchSelected} onBatchToggle={toggleBatchItem} />
                  <div ref={searchSentinelRef} className="h-4" />
                  {searchLoadingMore && (
                    <div className="flex justify-center py-6">
                      <div className="flex gap-1.5">
                        <div className="w-2 h-2 rounded-full bg-amber-400 animate-bounce" style={{animationDelay:'0ms'}} />
                        <div className="w-2 h-2 rounded-full bg-amber-400 animate-bounce" style={{animationDelay:'150ms'}} />
                        <div className="w-2 h-2 rounded-full bg-amber-400 animate-bounce" style={{animationDelay:'300ms'}} />
                      </div>
                    </div>
                  )}
                  {!searchHasMore && searchResults.length > 0 && (
                    <p className={`text-center text-xs ${T.sub} py-6`}>Semua hasil sudah dimuat</p>
                  )}
                </>
              )}
            </div>
          </div>
        )}

        {/* ── Library ── */}
        {page === "library" && activeLib && (
          <div className="pb-20 page-enter">
            <div className="px-5 sm:px-8 pt-6">
              <button onClick={() => goto("home")} className={`text-xs ${T.sub} mb-4 flex items-center gap-1`}><Icon.ArrowLeft />Kembali</button>
              <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-6">
                <div>
                  <h1 className="font-display text-3xl mb-1">{activeLib.name}</h1>
                  <p className={`text-xs ${T.sub}`}>{libraryItems(activeLib.id).length} gambar · disimpan di Google Drive</p>
                </div>
                <div className="flex gap-2">
                  <button onClick={() => copyPublicShare("library", activeLib.id)} className={`text-xs px-4 py-2 rounded-full border ${T.chipInactive}`}>Salin link publik</button>
                  {getLibraryCoverId(activeLib) && (
                    <button onClick={() => clearLibraryCover(activeLib.id)} className={`text-xs px-4 py-2 rounded-full border border-white/10 ${T.sub}`}>Hapus cover manual</button>
                  )}
                </div>
              </div>
            </div>
            <div className="px-5 sm:px-8 mb-2">
              {getLibraryCoverId(activeLib) && getLibraryCoverImage(activeLib) && (
                <p className={`text-xs ${T.sub}`}>Cover manual: {getLibraryCoverImage(activeLib)?.title || getLibraryCoverId(activeLib)} · fallback otomatis jika cover masuk Trash</p>
              )}
            </div>
            <main className="px-4 sm:px-8">
              <Masonry items={libraryItems(activeLib.id)} T={T} favorites={favorites} onToggleFav={toggleFav} onOpen={openPin} onMenu={setSheetItem} batchMode={batchMode} batchSelected={batchSelected} onBatchToggle={toggleBatchItem} />
            </main>
          </div>
        )}

        {/* ── Smart Album ── */}
        {page === "smart" && activeSmart && (
          <div className="pb-20 page-enter">
            <div className="px-5 sm:px-8 pt-6">
              <button onClick={() => goto("home")} className={`text-xs ${T.sub} mb-4 flex items-center gap-1`}><Icon.ArrowLeft />Kembali</button>
              <div className="flex items-center gap-3 mb-1">
                <span className="text-2xl text-amber-400">{activeSmart.icon}</span>
                <h1 className="font-display text-3xl">{activeSmart.label}</h1>
              </div>
              <p className={`text-xs ${T.sub} mb-6`}>{smartItems.length} gambar · {activeSmart.description}</p>
            </div>
            <main className="px-4 sm:px-8">
              <Masonry items={smartItems} T={T} favorites={favorites} onToggleFav={toggleFav} onOpen={openPin} onMenu={setSheetItem} batchMode={batchMode} batchSelected={batchSelected} onBatchToggle={toggleBatchItem} />
            </main>
          </div>
        )}

        {/* ── Pin ── */}
        {page === "pin" && activePin && (
          <div className="pb-20 page-enter">
            <div className="px-5 sm:px-8 pt-6">
              <button onClick={() => activeLibId ? setPage("library") : activeSmartId ? setPage("smart") : goto("home")} className={`text-xs ${T.sub} mb-4 flex items-center gap-1`}><Icon.ArrowLeft />Kembali</button>
            </div>
            <div className="px-5 sm:px-8 grid sm:grid-cols-2 gap-6 mb-8">
              <img
                src={HiraFormat.imageFull(activePin)}
                onClick={() => setLightbox(true)}
                className={`w-full rounded-2xl object-cover cursor-zoom-in ring-1 ${T.ring}`}
                alt={activePin.title}
                onError={(e) => {
                  e.target.onerror = null;
                  e.target.style.display = "none";
                  e.target.alt = "Gagal memuat";
                }}
              />
              <div>
                <h1 className="font-display text-2xl sm:text-3xl mb-2">{activePin.title}</h1>
                <p className={`text-sm ${T.sub} mb-5 leading-relaxed`}>{activePin.description}</p>
                <div className="flex flex-wrap gap-2 mb-6">
                  {(activePin.tags || []).map((t) => (
                    <button key={t} onClick={() => { setQuery(`#${t}`); goto("search"); }}
                      className={`text-xs px-3 py-1 rounded-full border ${T.chipInactive}`}>#{t}</button>
                  ))}
                </div>
                <div className="flex flex-wrap gap-2">
                  <button onClick={() => setSheetItem(activePin)} className={`px-4 py-2 rounded-full text-sm border flex items-center gap-1.5 ${T.chipInactive}`}>Bagikan</button>
                  <button onClick={() => copyPublicShare("image", activePin.id)} className={`px-4 py-2 rounded-full text-sm border flex items-center gap-1.5 ${T.chipInactive}`}>Salin link publik</button>
                  {activeLib && (
                    <button onClick={() => setLibraryCover(activeLib.id, activePin.id)} className={`px-4 py-2 rounded-full text-sm border border-amber-400/30 text-amber-400 flex items-center gap-1.5`}>Jadikan cover {activeLib.name}</button>
                  )}
                  {(activePin.libraryId || (activePin.savedTo||[]).length>0) && libraries.filter(l=> (activePin.libraryId===l.id || (activePin.savedTo||[]).includes(l.id)) && l.id!==activeLib?.id).slice(0,2).map(lib=>(
                    <button key={lib.id} onClick={() => setLibraryCover(lib.id, activePin.id)} className={`px-4 py-2 rounded-full text-sm border border-amber-400/20 text-amber-400/80`}>Cover {lib.name}</button>
                  ))}
                  <button onClick={() => setEditItem(activePin)} className={`px-4 py-2 rounded-full text-sm border flex items-center gap-1.5 ${T.chipInactive}`}><Icon.Edit />Edit</button>
                  <button onClick={() => toggleFav(activePin)} className={`px-4 py-2 rounded-full text-sm border flex items-center gap-1.5 ${favorites.has(activePin.id) ? "bg-rose-500/15 border-rose-400/40 text-rose-400" : T.chipInactive}`}><Icon.Heart filled={favorites.has(activePin.id)} />Favorit</button>

                  <div className="relative">
                    <button onClick={() => setSavePickerFor(savePickerFor === activePin.id ? null : activePin.id)} className={`px-4 py-2 rounded-full text-sm border ${T.chipInactive}`}>Simpan</button>
                    {savePickerFor === activePin.id && (
                      <div className={`absolute left-0 mt-2 w-52 rounded-xl border shadow-xl p-2 z-30 drop-enter ${T.modalBg}`}>
                        <p className={`text-xs ${T.sub} px-2 pb-1`}>Simpan ke koleksi</p>
                        {libraries.map((l) => {
                          const checked = (activePin.savedTo || []).includes(l.id);
                          return (
                            <button key={l.id} onClick={() => toggleSave(activePin.id, l.id)} className="w-full flex items-center justify-between px-2 py-1.5 rounded-lg text-sm hover:bg-amber-400/10">
                              {l.name}{checked && <span className="text-amber-400"><Icon.Check /></span>}
                            </button>
                          );
                        })}
                      </div>
                    )}
                  </div>
                  <a href={HiraFormat.imageFull(activePin)} target="_blank" rel="noreferrer" className={`px-4 py-2 rounded-full text-sm border ${T.chipInactive}`}>Unduh</a>
                  {deleteEnabled && (
                    <button
                      onClick={() => {
                        setDeleteConfirm({ visible: true, item: activePin });
                      }}
                      className="px-4 py-2 rounded-full text-sm border border-rose-400/30 text-rose-400"
                    >
                      Hapus
                    </button>
                  )}
                </div>
              </div>
            </div>
            <div className="px-5 sm:px-8">
              <p className={`text-xs uppercase tracking-widest ${T.sub} mb-3`}>Lainnya untuk dijelajahi</p>
              <Masonry items={relatedForPin} T={T} favorites={favorites} onToggleFav={toggleFav} onOpen={openPin} onMenu={setSheetItem} batchMode={batchMode} batchSelected={batchSelected} onBatchToggle={toggleBatchItem} />
            </div>
          </div>
        )}

        {/* ── Admin Dashboard ── */}
        {page === "admin" && (
          <div className="pb-20 page-enter px-5 sm:px-8 pt-8">
            <button onClick={() => goto("home")} className={`text-xs ${T.sub} mb-4 flex items-center gap-1`}><Icon.ArrowLeft />Kembali</button>
            <h1 className="font-display text-3xl mb-6">Dashboard Admin</h1>

            {/* ── Trash Section ── */}
            <div className={`rounded-2xl border p-5 mb-6 ${T.panel}`}>
              <div className="flex items-center justify-between mb-4">
                <h2 className="font-display text-lg flex items-center gap-2"><Icon.Trash />Sampah</h2>
                <div className="flex items-center gap-2">
                  <button onClick={loadTrash} className={`text-xs px-3 py-1.5 rounded-full border ${T.chipInactive}`}>Muat ulang</button>
                  {trashImages.length > 0 && (
                    <button onClick={async () => {
                      if (!window.confirm(`Hapus permanen ${trashImages.length} item? File di Drive akan hilang selamanya.`)) return;
                      let ok = 0;
                      for (const img of trashImages) {
                        try {
                          await HiraApi.permanentDeleteImage(img.id);
                          ok += 1;
                        } catch (error) {
                          if (error.status === 401) { handleAuthFail(); return; }
                        }
                      }
                      showToast(`${ok} item dihapus permanen`, ok ? "success" : "error");
                      loadTrash(); load();
                    }} className="text-xs px-3 py-1.5 rounded-full bg-rose-500/20 text-rose-400 border border-rose-400/30">Kosongkan Sampah</button>
                  )}
                </div>
              </div>
              <p className={`text-xs ${T.sub} mb-3`}>{trashImages.length} item di sampah</p>
              {trashImages.length === 0 ? (
                <p className={`text-xs ${T.sub} py-6 text-center`}>Sampah kosong</p>
              ) : (
                <div className="space-y-2">
                  {trashImages.map(img => {
                    const age = img.deletedAt ? Math.floor((Date.now() - new Date(img.deletedAt).getTime()) / 86400000) : null;
                    return (
                      <div key={img.id} className={`flex items-center gap-3 px-3 py-2.5 rounded-xl border ${dark ? "border-white/10 bg-white/[0.03]" : "border-stone-200 bg-stone-50"}`}>
                        <img src={HiraFormat.imageThumb(img)} className="w-12 h-12 rounded-lg object-cover bg-white/5 shrink-0" onError={e => { e.target.style.display = "none"; }} />
                        <div className="flex-1 min-w-0">
                          <p className="text-sm truncate">{img.title}</p>
                          <p className={`text-[10px] ${T.sub}`}>
                            {age !== null ? (age === 0 ? "Hari ini" : age === 1 ? "Kemarin" : `${age} hari lalu`) : ""}
                            {img.deletedAt ? ` · ${new Date(img.deletedAt).toLocaleDateString("id-ID")}` : ""}
                          </p>
                        </div>
                        <div className="flex items-center gap-1.5 shrink-0">
                          <button onClick={async () => {
                            try {
                              await HiraApi.restoreImage(img.id);
                              showToast(`"${img.title}" dipulihkan`, "success");
                              loadTrash(); load();
                            } catch (error) {
                              if (error.status === 401) handleAuthFail();
                              else showToast(error.message || "Gagal memulihkan item", "error");
                            }
                          }} className="text-xs text-amber-400 px-2.5 py-1 rounded-full border border-amber-400/30 hover:bg-amber-400/10">Pulihkan</button>
                          <button onClick={async () => {
                            if (!window.confirm(`Hapus permanen "${img.title}"?`)) return;
                            try {
                              await HiraApi.permanentDeleteImage(img.id);
                              showToast(`"${img.title}" dihapus permanen`, "success");
                              loadTrash(); load();
                            } catch (error) {
                              if (error.status === 401) handleAuthFail();
                              else showToast(error.message || "Gagal menghapus permanen", "error");
                            }
                          }} className="text-xs text-rose-400 px-2.5 py-1 rounded-full border border-rose-400/30 hover:bg-rose-400/10">Permanen</button>
                        </div>
                      </div>
                    );
                  })}
                </div>
              )}
            </div>

            {/* ── Statistics ── */}
            <AdminDashboard T={T} onExport={exportJson} onImport={() => setShowImport(true)} onExportFull={exportFullJson} onImportFull={() => setShowImportFull(true)} onToast={showToast} onChanged={load} />
            <ShareLinksPanel T={T} onToast={showToast} />
            <TagManagerPanel T={T} onToast={showToast} onChanged={load} />

            {/* ── Export / Import ── */}
            <div className={`rounded-2xl border p-5 mt-6 ${T.panel}`}>
              <h2 className="font-display text-lg mb-3">Export & Import</h2>
              <div className="flex flex-wrap gap-2 mb-4">
                <button onClick={exportJson} className="text-xs px-4 py-2 rounded-full bg-amber-400 text-[#0a0c16] font-medium">Export JSON</button>
                <button onClick={() => setShowImport(true)}
                  className={`text-xs px-4 py-2 rounded-full border ${T.chipInactive}`}>Import JSON</button>
              </div>
              {importResult && (
                <div className={`text-xs rounded-xl border p-3 ${T.panel}`}>
                  <p className="font-medium mb-1">Hasil Import:</p>
                  <p>Baru: <span className="text-emerald-400">{importResult.imported || 0}</span> · Diperbarui: <span className="text-amber-400">{importResult.updated || 0}</span> · Dilewati: <span className={T.sub}>{importResult.skipped || 0}</span></p>
                  {importResult.errors?.length > 0 && (
                    <details className="mt-2">
                      <summary className="text-rose-400 cursor-pointer">{importResult.errors.length} error</summary>
                      <ul className="mt-1 space-y-0.5 max-h-32 overflow-y-auto">
                        {importResult.errors.slice(0, 20).map((e, i) => <li key={i} className={T.sub}>{e.id}: {e.error}</li>)}
                      </ul>
                    </details>
                  )}
                </div>
              )}
            </div>
          </div>
        )}

        {/* ── Profile ── */}
        {page === "profile" && (
          <div className="px-5 sm:px-8 pt-8 pb-20 page-enter">
            <div className="flex flex-col items-center text-center mb-8">
              <div className="w-16 h-16 rounded-full bg-gradient-to-br from-amber-300 to-orange-500 flex items-center justify-center text-[#0a0c16] text-xl font-bold mb-3">H</div>
              <h1 className="font-display text-2xl">Hira Studio</h1>
              <p className={`text-xs ${T.sub} mt-1`}>{images.length} gambar · {libraries.length} koleksi · {accounts.length} akun Drive tersambung</p>
              <a href={`${API}/api/auth/login`} className="text-xs text-amber-400 mt-2">+ Sambungkan akun Drive lain</a>
            </div>
            <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
              {libraries.map((l) => {
                const cover = libraryItems(l.id)[0];
                return (
                  <button key={l.id} onClick={() => { setActiveLibId(l.id); setPage("library"); }} className="text-left">
                    <div className={`rounded-xl overflow-hidden relative h-40 ring-1 ${T.ring}`}>
                      {cover ? <img src={HiraFormat.imageThumb(cover)} className="w-full h-full object-cover" /> : <div className={`w-full h-full ${T.panel}`} />}
                    </div>
                    <p className="text-sm mt-2">{l.name}</p>
                    <p className={`text-xs ${T.sub}`}>{libraryItems(l.id).length} gambar</p>
                  </button>
                );
              })}
            </div>
          </div>
        )}
      </div>

      {/* Batch Floating Actions */}
      {batchMode && (
        <div className={`fixed bottom-20 sm:bottom-8 left-1/2 -translate-x-1/2 z-40 ${dark ? "bg-[#12141f]" : "bg-white"} border ${dark ? "border-white/10" : "border-stone-200"} rounded-2xl px-4 py-3 shadow-2xl drop-enter max-w-[95vw]`}>
          <p className={`text-[10px] ${T.sub} mb-2 text-center`}>{batchSelected.size} dipilih</p>
          <div className="flex items-center gap-2 flex-wrap justify-center">
            <button onClick={batchSelectAll} className={`text-xs px-3 py-1.5 rounded-full border ${T.chipInactive}`}>Semua</button>
            <button onClick={batchFavorite} className="text-xs px-3 py-1.5 rounded-full bg-amber-400 text-[#0a0c16] font-medium">♡ Favorit</button>
            <button onClick={batchMetadata} className="text-xs px-3 py-1.5 rounded-full bg-amber-400 text-[#0a0c16] font-medium"><span className="flex items-center gap-1"><Icon.Edit />Edit</span></button>
            <button onClick={batchDelete} className="text-xs px-3 py-1.5 rounded-full bg-rose-500/20 text-rose-400 border border-rose-400/30"><span className="flex items-center gap-1"><Icon.Trash />Hapus</span></button>
            <button onClick={batchClear} className={`text-xs px-3 py-1.5 rounded-full border ${T.chipInactive}`}>Batal</button>
          </div>
        </div>
      )}

      {/* Lightbox */}
      {lightbox && activePin && (
        <div className={`fixed inset-0 ${T.overlay} z-50 flex items-center justify-center p-4 overlay-enter`} onClick={() => setLightbox(false)}>
          <button className="absolute top-5 right-5 text-stone-200"><Icon.X width="24" height="24" /></button>
          <img src={HiraFormat.imageFull(activePin)} className="max-h-full max-w-full rounded-lg object-contain" alt={activePin.title} />
        </div>
      )}

      {sheetItem && (
        <ShareSheet item={sheetItem} T={T} fav={favorites.has(sheetItem.id)} onToggleFav={toggleFav}
          onSave={(it) => { setSheetItem(null); openPin(it); setTimeout(() => setSavePickerFor(it.id), 50); }}
          onOpenPreview={openPin} onEdit={setEditItem} onDelete={deleteImage} onCopyPublic={copyPublicShare} canDelete={deleteEnabled} onClose={() => setSheetItem(null)} />
      )}

      {editItem && (
        <EditModal T={T} item={editItem} allTags={allTags} libraries={libraries}
          onClose={() => setEditItem(null)} onToggleSave={toggleSave} onToast={setToast}
          onSaved={(updated) => { setImages((prev) => prev.map((i) => (i.id === updated.id ? updated : i))); load(); }} />
      )}

      {showBatchEdit && (
        <BatchEditModal T={T} libraries={libraries} allTags={allTags} selectedCount={batchSelected.size}
          onClose={() => setShowBatchEdit(false)}
          onSubmit={async (patch) => {
            const ids = [...batchSelected];
            // Optimistic update
            const before = images;
            setImages(prev => prev.map(i => {
              if (!batchSelected.has(i.id)) return i;
              const next = { ...i };
              if (patch.addTags) next.tags = [...new Set([...(next.tags || []), ...patch.addTags])];
              if (patch.removeTags) { const rm = new Set(patch.removeTags); next.tags = (next.tags || []).filter(t => !rm.has(t)); }
              if (patch.tags) next.tags = patch.tags;
              if (patch.setContent) next.content = patch.setContent;
              if (patch.libraryId) next.libraryId = patch.libraryId;
              if (patch.addToLibraryId) next.savedTo = [...new Set([...(next.savedTo || []), patch.addToLibraryId])];
              if (patch.removeFromLibraryId) next.savedTo = (next.savedTo || []).filter(x => x !== patch.removeFromLibraryId);
              return next;
            }));
            try {
              const d = await HiraApi.batchImages(ids, patch);
              if (d.errors?.length) showToast(`${d.updated.length} berhasil, ${d.errors.length} gagal`, "error");
              else showToast(`${d.updated.length} gambar diperbarui`, "success");
            } catch (error) {
              setImages(before);
              if (error.status === 401) handleAuthFail();
              else showToast(error.message || "Batch gagal", "error");
            }
            setShowBatchEdit(false);
            setBatchSelected(new Set());
            setBatchMode(false);
            load();
          }}
        />
      )}

      {showImport && (
        <ImportModal T={T} onClose={() => setShowImport(false)} onResult={(result) => { setImportResult(result); setShowImport(false); load(); }} />
      )}

      {showImportFull && (
        <ImportFullBackupModal T={T} onClose={() => setShowImportFull(false)} onResult={(result) => { setShowImportFull(false); showToast("Import backup v2 berhasil!", "success"); load(); }} />
      )}

      {showDevAuth && <DevAuthModal T={T} onSubmit={unlockDev} onClose={() => setShowDevAuth(false)} turnstileSiteKey={turnstileSiteKey} />}

      {showUpload && (
        <UploadModal T={T} libraries={libraries} allTags={allTags}
          onClose={() => setShowUpload(false)} onUploaded={load} />
      )}

      {shareExpiryRequest && (
        <ExpiryPickerModal T={T} request={shareExpiryRequest} onClose={()=>setShareExpiryRequest(null)} onToast={showToast} onCreated={()=>{ /* optional reload share links if admin page */ }} />
      )}
      <SettingsDrawer
        open={showDrawer}
        onClose={() => setShowDrawer(false)}
        T={T}
        dark={dark}
        setDark={setDark}
        accounts={accounts}
        devUnlocked={devUnlocked}
        deleteEnabled={deleteEnabled}
        toggleDeleteEnabled={toggleDeleteEnabled}
        onDevAuth={() => setShowDevAuth(true)}
        images={images}
        libraries={libraries}
        libraryItems={libraryItems}
        onNavToLib={(libId) => { setActiveLibId(libId); setPage("library"); }}
        homeFilter={homeFilter}
        setHomeFilter={setHomeFilter}
        onRenameLibrary={renameLibrary}
        onDeleteLibrary={deleteLibrary}
        onCreateLibrary={createLibraryFromSettings}
        batchMode={batchMode}
        onToggleBatchMode={toggleBatchMode}
        onOpenAdmin={() => setPage("admin")}
      />
      <Toast message={toast.message} type={toast.type} visible={toast.visible} onHide={() => setToast((prev) => ({ ...prev, visible: false }))} />
      <DeleteConfirmModal T={T} item={deleteConfirm.item} visible={deleteConfirm.visible} onConfirm={confirmDelete} onCancel={() => setDeleteConfirm({ visible: false, item: null })} />
    </div>
  );
}


  function bootstrap() {
    const root = document.getElementById("root");
    if (!root) return;

    const missingGlobals = HiraRuntime.checkRequiredGlobals(REQUIRED_GLOBALS);
    if (missingGlobals.length) {
      const friendly = missingGlobals[0] === "HiraApi"
        ? "HiraApi gagal dimuat"
        : "Helper frontend belum ter-load";
      HiraRuntime.renderFatalFallback({
        title: friendly,
        body: `Aplikasi tidak bisa dibuka karena helper berikut belum siap: ${missingGlobals.join(", ")}.`,
        details: { message: `Missing globals: ${missingGlobals.join(", ")}` },
      });
      return;
    }

    window.__HIRA_BOOTSTRAPPED__ = true;
    ReactDOM.createRoot(root).render(<ErrorBoundary><App /></ErrorBoundary>);
  }

  window.HiraApp = { App, bootstrap };
})();
