Danbooru Enchanced Gallery

Overhauls Danbooru's default gallery view with a sleek, responsive justified grid layout. Includes high-res thumbnails, hover previews, overlay actions, and more.

// ==UserScript==
// @name         Danbooru Enchanced Gallery
// @namespace    https://github.com/yourusername/danbooru-justified-gallery
// @version      1.53
// @description  Overhauls Danbooru's default gallery view with a sleek, responsive justified grid layout. Includes high-res thumbnails, hover previews, overlay actions, and more.
// @author       Claude Sonnet 4 (prompted by orx_ibx)
// @match        https://danbooru.donmai.us/
// @match        https://danbooru.donmai.us/posts*
// @match        https://danbooru.donmai.us/?tags=*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.8.1/js/jquery.justifiedGallery.min.js
// @resource     https://cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.8.1/css/justifiedGallery.min.css
// @license      MIT (https://opensource.org/licenses/MIT)
// ==/UserScript==

(function () {
    "use strict";

    // Config for added features
    const CONFIG = {
        // toggle features
        enableHoverPreview: true,
        enableHighRes: true,
        enableRelocateSearchBar: true,
        enableHiddenSidebar: true,

        // High rez thumbnail loader config
        viewportBuffer: 600, // pixels below viewport to start loading HR thumbs
        maxRetries: 0, // retry attempts for failed HR loads
        batchSize: 5, // num of HR thumbnails to load at once
        batchDelay: 0, // delay between each HR batch load
        enableScrollPause: true,
        scrollThrottleDelay: 0, // Add this line (ms to wait after scroll stops)

        // Hover preview config
        hoverDelay: 300, // ms delay before showing preview
    };

    // Config for Justified Gallery
    const JG_CONFIG = {
        rowHeight: 160,
        maxRowHeight: 720,
        lastRow: "nojustify",
        margins: 8,
        border: 0,
        captions: true,
        randomize: false,
        waitThumbnailsLoad: true,
        cssAnimation: false,
        imagesAnimationDuration: 300,
        captionSettings: {
            animationDuration: 50,
            visibleOpacity: 0.5,
            nonVisibleOpacity: 0.0,
        },
    };

    // Elements that trigger a Justified Gallery reset when clicked
    // Add CSS selectors for any buttons or toggles that should reset JG on interaction
    const JG_RESET_SELECTORS = ["#jg-reload-btn"];

    class ImageManager {
        constructor() {
            this.urlCache = new Map();
            this.loadingCache = new Set();
            this.abortController = null;
            this.batchQueue = [];
            this.processingBatch = false;
            this.currentPreview = null;
            this.hoverContainer = null;
            this.previewAbortController = null;
            this.isScrolling = false; // Add this line
            this.scrollTimeout = null; // Add this line
            this.activeLoaders = new Map(); // Add this line (track active image loaders)

            if (CONFIG.enableHoverPreview) this.createHoverContainer();

            if (CONFIG.enableScrollPause) this.setupScrollDetection();
        }

        // Get the current zoom level
        getZoomLevel() {
            return window.devicePixelRatio || 1;
        }

        // Get zoom-independent dimensions
        getZoomIndependentDimensions() {
            const zoom = this.getZoomLevel();
            return {
                windowWidth: window.innerWidth * zoom,
                windowHeight: window.innerHeight * zoom,
                maxWidth: window.innerWidth * 0.95 * zoom,
                maxHeight: window.innerHeight * 0.95 * zoom,
            };
        }

        createHoverContainer() {
            this.hoverContainer = document.createElement("div");
            this.hoverContainer.id = "danbooru-hover-preview";
            document.body.appendChild(this.hoverContainer);
        }

        showPreview(imageUrl, mouseEvent) {
            if (!imageUrl || !this.hoverContainer) return;

            const img = document.createElement("img");
            img.src = imageUrl;
            img.onerror = () => {
                this.hidePreview();
            };

            // Apply zoom compensation when image loads
            img.onload = () => {
                this.applyZoomCompensation(img);
                // Reposition after zoom compensation is applied
                this.positionPreview(mouseEvent);
            };

            this.hoverContainer.innerHTML = "";
            this.hoverContainer.appendChild(img);

            // Position the preview
            this.positionPreview(mouseEvent);

            // Show with fade-in effect
            this.hoverContainer.classList.add("visible");
            this.currentPreview = img;
        }

        applyZoomCompensation(img) {
            const zoom = this.getZoomLevel();
            const zoomIndependent = this.getZoomIndependentDimensions();

            // Calculate the scale factor to counteract zoom
            const scaleCompensation = 1 / zoom;

            // Get natural image dimensions
            const naturalWidth = img.naturalWidth;
            const naturalHeight = img.naturalHeight;

            // Calculate how much we need to scale down to fit in viewport
            const widthScale = zoomIndependent.maxWidth / naturalWidth;
            const heightScale = zoomIndependent.maxHeight / naturalHeight;
            const fitScale = Math.min(widthScale, heightScale, 1); // Don't scale up

            // Apply both zoom compensation and fit scaling
            const finalScale = scaleCompensation * fitScale;

            // Set the transform to counteract zoom and fit to screen
            img.style.transform = `scale(${finalScale})`;
            img.style.transformOrigin = "top left";

            // Set dimensions based on the scaled size
            img.style.width = `${naturalWidth}px`;
            img.style.height = `${naturalHeight}px`;

            // Update the container to match the effective size
            this.hoverContainer.style.width = `${naturalWidth * finalScale}px`;
            this.hoverContainer.style.height = `${
                naturalHeight * finalScale
            }px`;
        }

        positionPreview(mouseEvent) {
            if (!this.hoverContainer) return;

            const zoom = this.getZoomLevel();
            const { clientX: mouseX, clientY: mouseY } = mouseEvent;
            const zoomIndependent = this.getZoomIndependentDimensions();

            // Adjust mouse coordinates for zoom
            const adjustedMouseX = mouseX * zoom;
            const adjustedMouseY = mouseY * zoom;

            // Temporarily position at mouse location to get accurate dimensions
            this.hoverContainer.style.left = `${adjustedMouseX / zoom}px`;
            this.hoverContainer.style.top = `${adjustedMouseY / zoom}px`;
            this.hoverContainer.style.visibility = "hidden";
            this.hoverContainer.style.display = "block";

            // Force layout recalculation
            this.hoverContainer.offsetHeight;

            // Get the effective size of the container
            const containerWidth =
                parseFloat(this.hoverContainer.style.width) ||
                this.hoverContainer.offsetWidth;
            const containerHeight =
                parseFloat(this.hoverContainer.style.height) ||
                this.hoverContainer.offsetHeight;

            // Calculate position in zoom-adjusted coordinates
            let left = (adjustedMouseX + 15) / zoom;
            let top = (adjustedMouseY + 15) / zoom;

            // Check boundaries and adjust
            if (
                left * zoom + containerWidth * zoom >
                zoomIndependent.windowWidth
            ) {
                left = (adjustedMouseX - containerWidth * zoom - 15) / zoom;
            }

            if (
                top * zoom + containerHeight * zoom >
                zoomIndependent.windowHeight
            ) {
                top = (adjustedMouseY - containerHeight * zoom - 15) / zoom;
            }

            // Ensure preview stays within viewport
            left = Math.max(
                10 / zoom,
                Math.min(
                    left,
                    (zoomIndependent.windowWidth - containerWidth * zoom - 10) /
                        zoom
                )
            );
            top = Math.max(
                10 / zoom,
                Math.min(
                    top,
                    (zoomIndependent.windowHeight -
                        containerHeight * zoom -
                        10) /
                        zoom
                )
            );

            // Apply final position and make visible
            this.hoverContainer.style.left = `${left}px`;
            this.hoverContainer.style.top = `${top}px`;
            this.hoverContainer.style.visibility = "visible";
        }

        hidePreview() {
            if (!this.hoverContainer) return;

            this.hoverContainer.classList.remove("visible");
            this.currentPreview = null;

            // Cancel any pending preview request
            if (this.previewAbortController) {
                this.previewAbortController.abort();
                this.previewAbortController = null;
            }

            // Clear content after fade-out
            setTimeout(() => {
                this.hoverContainer.innerHTML = "";
                // Reset container dimensions
                this.hoverContainer.style.width = "";
                this.hoverContainer.style.height = "";
            }, 200);
        }

        async fetchImageUrlForPreview(postId) {
            if (this.urlCache.has(postId)) {
                return this.urlCache.get(postId);
            }

            try {
                // Cancel any previous preview request
                if (this.previewAbortController) {
                    this.previewAbortController.abort();
                }

                this.previewAbortController = new AbortController();

                const response = await fetch(
                    `https://danbooru.donmai.us/posts/${postId}.json`,
                    {
                        signal: this.previewAbortController.signal,
                    }
                );

                if (!response.ok) {
                    throw new Error(
                        `HTTP ${response.status}: ${response.statusText}`
                    );
                }

                const data = await response.json();
                const imageUrl = data.large_file_url || data.file_url;

                if (imageUrl) {
                    this.urlCache.set(postId, imageUrl);
                    return imageUrl;
                }

                throw new Error("No image URL found in response");
            } catch (error) {
                if (error.name === "AbortError") {
                    return null; // Request was cancelled
                }
                // console.warn(
                //     `Failed to fetch preview image for post ${postId}:`,
                //     error
                // );
                return null;
            }
        }

        // Add to ImageManager class
        setupScrollDetection() {
            const handleScroll = () => {
                this.isScrolling = true;

                // Clear existing timeout
                if (this.scrollTimeout) {
                    clearTimeout(this.scrollTimeout);
                }

                // Set new timeout
                this.scrollTimeout = setTimeout(() => {
                    this.isScrolling = false;
                    // Resume batch processing if queue has items
                    if (this.batchQueue.length > 0) {
                        this.processBatch();
                    }
                }, CONFIG.scrollThrottleDelay);
            };

            window.addEventListener("scroll", handleScroll, { passive: true });
            window.addEventListener("wheel", handleScroll, { passive: true });
            window.addEventListener("touchmove", handleScroll, {
                passive: true,
            });
        }

        // Add this method to the class
        async processBatch() {
            if (this.processingBatch || this.batchQueue.length === 0) return;

            // Skip processing if scrolling and scroll pause is enabled
            if (CONFIG.enableScrollPause && this.isScrolling) return;

            this.processingBatch = true;
            const batch = this.batchQueue.splice(0, CONFIG.batchSize);

            await Promise.all(
                batch.map(({ img, postId }) =>
                    loadHighResImage(img, postId, this)
                )
            );

            this.processingBatch = false;

            // Process next batch if queue isn't empty and not scrolling
            if (
                this.batchQueue.length > 0 &&
                (!CONFIG.enableScrollPause || !this.isScrolling)
            ) {
                setTimeout(() => this.processBatch(), CONFIG.batchDelay);
            }
        }

        abortImageLoading(postId) {
            if (this.activeLoaders.has(postId)) {
                const controller = this.activeLoaders.get(postId);
                controller.abort();
                this.activeLoaders.delete(postId);
                this.loadingCache.delete(postId);
            }
        }

        async fetchImageUrl(postId) {
            if (this.urlCache.has(postId)) {
                return this.urlCache.get(postId);
            }

            if (this.loadingCache.has(postId)) {
                return null; // Already loading
            }

            this.loadingCache.add(postId);

            try {
                // Create controller for this specific request
                const controller = new AbortController();
                this.activeLoaders.set(postId, controller);

                const response = await fetch(
                    `https://danbooru.donmai.us/posts/${postId}.json`,
                    { signal: controller.signal }
                );

                if (!response.ok) {
                    throw new Error(
                        `HTTP ${response.status}: ${response.statusText}`
                    );
                }

                const data = await response.json();
                const imageUrl = data.large_file_url || data.file_url;

                if (imageUrl) {
                    this.urlCache.set(postId, imageUrl);
                    return imageUrl;
                }

                throw new Error("No image URL found in response");
            } catch (error) {
                // if (error.name === "AbortError") {
                //     return null; // Request was cancelled
                // }
                // console.warn(
                //     `Failed to fetch image for post ${postId}:`,
                //     error
                // );
                // return null;
            } finally {
                this.loadingCache.delete(postId);
                this.activeLoaders.delete(postId);
            }
        }
    }

    function createIntersectionObserver(imageManager) {
        const observer = new IntersectionObserver(
            (entries) => {
                entries.forEach((entry) => {
                    const img = entry.target;
                    const link = img.closest("a");

                    if (entry.isIntersecting) {
                        // Image entered viewport
                        if (link && !img.dataset.highResLoaded) {
                            const postId = extractPostIdFromUrl(link.href);
                            if (postId) {
                                // Remove from queue if it was there (in case it was added before)
                                imageManager.batchQueue =
                                    imageManager.batchQueue.filter(
                                        (item) => item.postId !== postId
                                    );

                                // Add to batch queue
                                imageManager.batchQueue.push({ img, postId });
                                imageManager.processBatch();
                            }
                        }
                    } else {
                        // Image left viewport - abort loading if in progress
                        if (link) {
                            const postId = extractPostIdFromUrl(link.href);
                            if (postId) {
                                // Remove from queue if it's there
                                imageManager.batchQueue =
                                    imageManager.batchQueue.filter(
                                        (item) => item.postId !== postId
                                    );

                                // Abort active loading
                                imageManager.abortImageLoading(postId);
                            }
                        }
                    }
                });
            },
            {
                // rootMargin: `${CONFIG.viewportBuffer}px`, // preload all side
                rootMargin: `0px 0px ${CONFIG.viewportBuffer}px 0px`, // preload only bottom
                threshold: 0.1,
            }
        );

        return observer;
    }

    function extractPostIdFromUrl(url) {
        const match = url.match(/\/posts\/(\d+)/);
        return match ? match[1] : null;
    }

    async function loadHighResImage(img, postId, imageManager) {
        if (
            img.dataset.highResLoaded === "true" ||
            img.dataset.highResLoading === "true"
        ) {
            return;
        }

        img.dataset.highResLoading = "true";

        try {
            const highResUrl = await imageManager.fetchImageUrl(postId);

            if (highResUrl && highResUrl !== img.src) {
                await replaceImageSrc(img, highResUrl);
                img.dataset.highResLoaded = "true";
            }
        } catch (error) {
            // console.warn(
            //     `Failed to load high-res image for post ${postId}:`,
            //     error
            // );
        } finally {
            img.dataset.highResLoading = "false";
        }
    }

    function replaceImageSrc(img, newSrc) {
        return new Promise((resolve, reject) => {
            const tempImg = new Image();

            tempImg.onload = () => {
                // Store original dimensions to maintain layout
                const originalWidth = img.offsetWidth;
                const originalHeight = img.offsetHeight;

                img.src = newSrc;

                // Ensure size doesn't change
                img.style.width = originalWidth + "px";
                img.style.height = originalHeight + "px";
                img.style.objectFit = "cover";

                resolve();
            };

            tempImg.onerror = () => {
                reject(new Error("Failed to load high-res image"));
            };

            tempImg.src = newSrc;
        });
    }

    function cleanup() {
        const justifiedContainer = document.getElementById(
            "justified-gallery-container"
        );
        if (justifiedContainer) {
            // Disconnect observer
            if (justifiedContainer.intersectionObserver) {
                justifiedContainer.intersectionObserver.disconnect();
            }

            // Clear caches
            if (justifiedContainer.imageManager) {
                // Abort all active loaders
                justifiedContainer.imageManager.activeLoaders.forEach(
                    (controller, postId) => {
                        controller.abort();
                    }
                );

                justifiedContainer.imageManager.urlCache.clear();
                justifiedContainer.imageManager.loadingCache.clear();
                justifiedContainer.imageManager.batchQueue = [];
                justifiedContainer.imageManager.processingBatch = false;
                justifiedContainer.imageManager.activeLoaders.clear();

                // Clear scroll timeout
                if (justifiedContainer.imageManager.scrollTimeout) {
                    clearTimeout(justifiedContainer.imageManager.scrollTimeout);
                }

                // Clean up hover preview
                if (justifiedContainer.imageManager.hoverContainer) {
                    justifiedContainer.imageManager.hidePreview();
                    justifiedContainer.imageManager.hoverContainer.remove();
                }
                if (justifiedContainer.imageManager.previewAbortController) {
                    justifiedContainer.imageManager.previewAbortController.abort();
                }
            }

            const existingButton = document.getElementById(
                "preview-toggle-button"
            );
            if (existingButton) {
                existingButton.remove();
            }

            // Remove toggle button
            if (justifiedContainer.toggleButton) {
                justifiedContainer.toggleButton.remove();
            }

            // Destroy the justified gallery instance
            $(justifiedContainer).justifiedGallery("destroy");

            // Remove the container entirely
            justifiedContainer.remove();

            // Also remove any stray toggle button
        }
    }

    // Separate event handler function
    async function handleCaptionClick(e) {
        e.preventDefault();
        // Prevent multiple clicks
        if (e.target.classList.contains("processing")) return;
        const isFavBtn = e.target.classList.contains("favorite-btn");
        const isFavorited = e.target.classList.contains("favorited");

        const isUpvBtn = e.target.classList.contains("upvote-btn");
        const isUpvoted = e.target.classList.contains("upvoted");

        const csrfToken = document
            .querySelector('meta[name="csrf-token"]')
            ?.getAttribute("content");
        if (!csrfToken) {
            console.warn("CSRF token not found");
            return;
        }

        if (isUpvBtn) {
            e.preventDefault();
            e.stopPropagation();

            const postId = e.target.getAttribute("data-post-id");
            const scoreElement = e.target.nextElementSibling;

            // Prevent multiple clicks
            e.target.classList.add("processing");

            try {
                const rMethod = !isUpvoted ? "POST" : "DELETE";
                const rURL = `/posts/${postId}/votes`;
                const response = await fetch(rURL, {
                    method: rMethod,
                    headers: {
                        "Content-Type": "application/json",
                        "X-CSRF-Token": csrfToken,
                        "X-Requested-With": "XMLHttpRequest",
                    },
                    body: JSON.stringify({ score: 1 }),
                });

                if (response.ok) {
                    // Only increment by 1, don't use server response
                    let currentScore = parseInt(scoreElement.textContent) || 0;
                    if (!isUpvoted) {
                        e.target.classList.add("upvoted");
                        scoreElement.textContent = currentScore + 1;
                        if (
                            scoreElement.classList.contains("neutral") &&
                            parseInt(scoreElement.textContent) > 0
                        ) {
                            scoreElement.classList.remove("neutral");
                            scoreElement.classList.add("positive");
                        }
                        e.target.classList.add("active");
                    } else {
                        e.target.classList.remove("upvoted");
                        scoreElement.textContent = currentScore - 1;
                        if (
                            scoreElement.classList.contains("positive") &&
                            parseInt(scoreElement.textContent) === 0
                        ) {
                            scoreElement.classList.remove("positive");
                            scoreElement.classList.add("neutral");
                        }
                        e.target.classList.remove("active");
                    }
                }
            } catch (error) {
                console.error("Error voting:", error);
            } finally {
                e.target.classList.remove("processing");
            }
        }

        // FAVORITE BUTTON
        if (isFavBtn) {
            e.preventDefault();
            e.stopPropagation();

            const postId = e.target.getAttribute("data-post-id");

            // Prevent multiple clicks
            e.target.classList.add("processing");

            try {
                const rMethod = !isFavorited ? "POST" : "DELETE";
                const rURL = !isFavorited
                    ? "/favorites.json"
                    : `/favorites/${postId}.json`;
                const response = await fetch(rURL, {
                    method: rMethod,
                    headers: {
                        "Content-Type": "application/json",
                        "X-CSRF-Token": csrfToken,
                        "X-Requested-With": "XMLHttpRequest",
                    },
                    body: JSON.stringify({ post_id: postId }),
                });

                if (response.ok) {
                    if (!isFavorited) {
                        e.target.classList.add("active");
                        e.target.classList.add("favorited");
                    } else {
                        e.target.classList.remove("active");
                        e.target.classList.remove("favorited");
                    }
                }
            } catch (error) {
                console.error("Error favoriting:", error);
            } finally {
                e.target.classList.remove("processing");
            }
        }
    }

    function addHoverListeners(img, imageManager) {
        if (!CONFIG.enableHoverPreview) return;

        const link = img.closest("a");
        if (!link) return;

        const postId = extractPostIdFromUrl(link.href);
        if (!postId) return;

        let previewTimeout;

        const handleMouseEnter = async (event) => {
            clearTimeout(previewTimeout);
            previewTimeout = setTimeout(async () => {
                const imageUrl = await imageManager.fetchImageUrlForPreview(
                    postId
                );
                if (imageUrl && !imageManager.currentPreview) {
                    imageManager.showPreview(imageUrl, event);
                }
            }, CONFIG.hoverDelay);
        };

        const handleMouseLeave = () => {
            clearTimeout(previewTimeout);
            imageManager.hidePreview();
        };

        const handleMouseMove = (event) => {
            if (imageManager.currentPreview) {
                imageManager.positionPreview(event);
            }
        };

        img.addEventListener("mouseenter", handleMouseEnter);
        img.addEventListener("mouseleave", handleMouseLeave);
        img.addEventListener("mousemove", handleMouseMove);
    }

    function createToggleButton(imageManager) {
        const button = document.createElement("button");
        button.id = "preview-toggle-button";
        button.textContent = CONFIG.enableHoverPreview
            ? "Preview: ON"
            : "Preview: OFF";
        if (!CONFIG.enableHoverPreview) button.classList.add("disabled");

        button.addEventListener("click", () => {
            CONFIG.enableHoverPreview = !CONFIG.enableHoverPreview;
            button.textContent = CONFIG.enableHoverPreview
                ? "Preview: ON"
                : "Preview: OFF";
            if (
                !CONFIG.enableHoverPreview &&
                !button.classList.contains("disabled")
            )
                button.classList.add("disabled");
            else button.classList.remove("disabled");

            if (!CONFIG.enableHoverPreview) {
                // Hide any current preview
                imageManager.hidePreview();
            }

            // Update all existing images with/without hover listeners
            updateHoverListeners(imageManager);
        });

        // document.body.appendChild(button);

        // Insert after the show-posts-link element
        const postsSection = document.querySelector("#post-sections li");
        if (postsSection) {
            postsSection.appendChild(button);
        } else {
            // Fallback to body if show-posts-link not found
            document.body.appendChild(button);
        }
        return button;
    }

    function updateHoverListeners(imageManager) {
        const justifiedContainer = document.getElementById(
            "justified-gallery-container"
        );
        if (!justifiedContainer) return;

        const images = justifiedContainer.querySelectorAll("img");

        if (CONFIG.enableHoverPreview) {
            // Add hover listeners to images that don't have them
            images.forEach((img) => {
                if (!img.dataset.hoverListeners) {
                    addHoverListeners(img, imageManager);
                    img.dataset.hoverListeners = "true";
                }
            });

            // Create hover container if it doesn't exist
            if (!imageManager.hoverContainer) {
                imageManager.createHoverContainer();
            }
        } else {
            // Remove hover listeners by cloning and replacing elements
            images.forEach((img) => {
                if (img.dataset.hoverListeners) {
                    const newImg = img.cloneNode(true);
                    delete newImg.dataset.hoverListeners;
                    img.parentNode.replaceChild(newImg, img);
                }
            });
        }
    }

    // Post info tooltip functions
    // Add this after creating the tooltip or in your existing event listeners
    function preventPageScrollOnTooltip(tooltip) {
        tooltip.addEventListener(
            "wheel",
            function (e) {
                const scrollTop = tooltip.scrollTop;
                const scrollHeight = tooltip.scrollHeight;
                const height = tooltip.clientHeight;

                const delta = e.deltaY;
                const isScrollingUp = delta < 0;
                const isScrollingDown = delta > 0;

                // Prevent scrolling up when already at the top
                if (isScrollingUp && scrollTop === 0) {
                    e.preventDefault();
                    return;
                }

                // Prevent scrolling down when already at the bottom
                if (isScrollingDown && scrollTop + height >= scrollHeight) {
                    e.preventDefault();
                    return;
                }
            },
            { passive: false }
        );
    }

    function createTooltip() {
        const tooltip = document.createElement("div");
        tooltip.className = "post-info-tooltip";
        tooltip.id = "post-info-tooltip";
        document.body.appendChild(tooltip);

        // Add scroll containment
        preventPageScrollOnTooltip(tooltip);

        return tooltip;
    }

    function positionTooltip(tooltip, button) {
        const buttonRect = button.getBoundingClientRect();
        const tooltipRect = tooltip.getBoundingClientRect();
        const viewport = {
            width: window.innerWidth,
            height: window.innerHeight,
        };

        // Add scroll offsets to convert viewport coordinates to document coordinates
        const scrollX =
            window.pageXOffset || document.documentElement.scrollLeft;
        const scrollY =
            window.pageYOffset || document.documentElement.scrollTop;

        let left, top;

        // Try to position to the right first
        if (buttonRect.right + tooltipRect.width + 20 < viewport.width) {
            left = buttonRect.right + 10 + scrollX;
        } else {
            // Position to the left
            left = buttonRect.left - tooltipRect.width - 10 + scrollX;
        }

        // Vertical centering with bounds checking
        top =
            buttonRect.top +
            buttonRect.height / 2 -
            tooltipRect.height / 2 +
            scrollY;

        // Keep within viewport bounds (adjust bounds checking to account for scroll)
        const minTop = 10 + scrollY;
        const maxTop = scrollY + viewport.height - tooltipRect.height - 10;
        const minLeft = 10 + scrollX;
        const maxLeft = scrollX + viewport.width - tooltipRect.width - 10;

        if (top < minTop) top = minTop;
        if (top > maxTop) top = maxTop;
        if (left < minLeft) left = minLeft;
        if (left > maxLeft) left = maxLeft;

        tooltip.style.left = left + "px";
        tooltip.style.top = top + "px";
    }

    function parsePostInfo(html) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, "text/html");

        const result = {
            tags: {
                artist: [],
                copyright: [],
                character: [],
                general: [],
                meta: [],
            },
            info: [],
            options: [],
        };

        // Parse tags
        const tagLists = {
            "artist-tag-list": "artist",
            "copyright-tag-list": "copyright",
            "character-tag-list": "character",
            "general-tag-list": "general",
            "meta-tag-list": "meta",
        };

        Object.entries(tagLists).forEach(([className, type]) => {
            const list = doc.querySelector(`ul.${className}`);
            if (list) {
                const items = list.querySelectorAll("li");
                items.forEach((item) => {
                    const link = item.querySelector(".search-tag");
                    const count = item.querySelector(".post-count");
                    if (link) {
                        result.tags[type].push({
                            name: link.textContent.trim(),
                            href: link.getAttribute("href"),
                            count: count ? count.textContent.trim() : "",
                            tagType:
                                item.className.match(/tag-type-(\d+)/)?.[1] ||
                                "0",
                        });
                    }
                });
            }
        });

        // Parse information section
        const infoSection = doc.querySelector("#post-information ul");
        if (infoSection) {
            const items = infoSection.querySelectorAll("li");
            items.forEach((item) => {
                const text = item.textContent.trim();
                const links = Array.from(item.querySelectorAll("a")).map(
                    (a) => ({
                        text: a.textContent.trim(),
                        href: a.getAttribute("href"),
                    })
                );
                result.info.push({ text, links, html: item.innerHTML });
            });
        }

        // Parse options section
        const optionsSection = doc.querySelector("#post-options ul");
        if (optionsSection) {
            const items = optionsSection.querySelectorAll("li");
            items.forEach((item) => {
                const text = item.textContent.trim();
                const links = Array.from(item.querySelectorAll("a")).map(
                    (a) => ({
                        text: a.textContent.trim(),
                        href: a.getAttribute("href"),
                    })
                );
                result.options.push({ text, links, html: item.innerHTML });
            });
        }

        return result;
    }

    function renderTooltipContent(data) {
        let html = "";
        // Add cancel/exit button
        html += `
        <div class="tooltip-exit-button-container">
            <button id="tooltip-exit-button" class="tooltip-exit-button" >×</button>
        </div>
    `;
        // Render tags by category
        const tagOrder = [
            "artist",
            "copyright",
            "character",
            "general",
            "meta",
        ];
        const tagLabels = {
            artist: "Artist",
            copyright: "Copyright",
            character: "Character",
            general: "General",
            meta: "Meta",
        };
        tagOrder.forEach((type) => {
            if (data.tags[type].length > 0) {
                html += `<div class="tooltip-section">`;
                html += `<h3>${tagLabels[type]}</h3>`;
                html += `<div class="tooltip-tags">`;
                data.tags[type].forEach((tag) => {
                    html += `<a href="${tag.href}" class="tooltip-tag tag-type-${tag.tagType}">${tag.name}</a>`;
                });
                html += `</div></div>`;
            }
        });
        // Render information section
        if (data.info.length > 0) {
            html += `<div class="tooltip-section tooltip-info">`;
            html += `<h3>Information</h3>`;
            html += `<ul>`;
            data.info.forEach((item) => {
                html += `<li>${item.html}</li>`;
            });
            html += `</ul></div>`;
        }
        // Render options section
        if (data.options && data.options.length > 0) {
            html += `<div class="tooltip-section tooltip-options">`;
            html += `<h3>Options</h3>`;
            html += `<ul>`;
            data.options.forEach((option) => {
                html += `<li>${option.html}</li>`;
            });
            html += `</ul></div>`;
        }
        return html;
    }

    function showPostInfo(postId, button) {
        let tooltip = document.getElementById("post-info-tooltip");
        if (!tooltip) {
            tooltip = createTooltip();
        }

        // Hide tooltip if clicking the same button
        if (
            tooltip.classList.contains("visible") &&
            tooltip.dataset.currentPostId === postId
        ) {
            tooltip.classList.remove("visible");
            return;
        }

        tooltip.dataset.currentPostId = postId;
        tooltip.innerHTML =
            '<div class="tooltip-loading">Loading post information...</div>';
        tooltip.classList.add("visible");
        positionTooltip(tooltip, button);

        // Fetch post information
        fetch(`https://danbooru.donmai.us/posts/${postId}`)
            .then((response) => response.text())
            .then((html) => {
                const data = parsePostInfo(html);
                tooltip.innerHTML = renderTooltipContent(data);
                positionTooltip(tooltip, button);
            })
            .catch((error) => {
                console.error("Error fetching post info:", error);
                tooltip.innerHTML =
                    '<div class="tooltip-loading">Error loading post information</div>';
            });
    }

    function addInfoBtnHandlers() {
        document.addEventListener("click", function (e) {
            if (e.target.classList.contains("info-button")) {
                e.preventDefault();
                e.stopPropagation();
                const postId = e.target.getAttribute("data-post-id");
                showPostInfo(postId, e.target);
            }
        });

        // Hide tooltip when clicking outside or on exit button
        function addGlobalTooltipHandler() {
            document.addEventListener("click", function (e) {
                const tooltip = document.getElementById("post-info-tooltip");

                // If tooltip-exit-button clicked, hide tooltip
                if (e.target.id === "tooltip-exit-button") {
                    tooltip?.classList.remove("visible");
                    return;
                }

                // If click is outside tooltip and info-button, hide tooltip
                if (
                    tooltip &&
                    !e.target.closest(".info-button") &&
                    !e.target.closest(".post-info-tooltip")
                ) {
                    tooltip.classList.remove("visible");
                }
            });

            // Hide tooltip on Escape key press
            document.addEventListener("keydown", function (e) {
                if (e.key === "Escape" || e.key === "Esc") {
                    tooltip?.classList.remove("visible");
                }
            });
        }

        addGlobalTooltipHandler();
    }

    function initializeJustifiedGallery() {
        const postsContainer = document.querySelector(".posts-container");
        if (!postsContainer) return;

        // Create justified gallery container
        const justifiedContainer = document.createElement("div");
        justifiedContainer.className = "justified-gallery";
        justifiedContainer.id = "justified-gallery-container";

        // Convert posts to justified gallery format
        const posts = postsContainer.querySelectorAll("article.post-preview");

        posts.forEach((post) => {
            const link = post.querySelector(".post-preview-link");
            const img = post.querySelector(".post-preview-image");

            if (link && img) {
                //
                const postId = post.getAttribute("data-id");
                const score = post.getAttribute("data-score") || "0";

                // Create new anchor element for justified gallery
                const newLink = document.createElement("a");
                newLink.href = link.href;
                // newLink.title = img.getAttribute("data-title") || img.alt;

                // Create new image element
                const newImg = document.createElement("img");
                newImg.src = img.src;
                newImg.alt = img.alt;
                // newImg.title = img.title;

                // Set data attributes for justified gallery
                newImg.setAttribute("data-safe-src", img.src);

                // Get original dimensions if available
                const width =
                    img.getAttribute("width") || img.naturalWidth || 180;
                const height =
                    img.getAttribute("height") || img.naturalHeight || 180;

                newImg.setAttribute("width", width);
                newImg.setAttribute("height", height);

                // Create caption element
                const caption = document.createElement("div");
                caption.className = "jg-caption";
                caption.addEventListener("click", handleCaptionClick);

                let scoreState;
                if (parseInt(score) > 0) scoreState = "positive";
                else if (parseInt(score) < 0) scoreState = "negative";
                else scoreState = "neutral";

                let html = `
                    <span class="caption-button upvote-btn" data-post-id="${postId}">🡅</span>
                    <span class="score-display ${scoreState}">${score}</span>
                    <span class="caption-button favorite-btn" data-post-id="${postId}">❤︎</span>
                    <div class="right-side-wrapper">
                `;

                // get content duration
                let contentDuration = post.querySelector(
                    "a.post-preview-link div.post-animation-icon .post-duration"
                );
                if (contentDuration) {
                    contentDuration = contentDuration.textContent.trim();
                    const durLabelHTML = `<span class="content-duration">${contentDuration}</span>`;
                    html += durLabelHTML;
                }

                html += `<span class="info-button" data-post-id="${postId}">🛈</span>`;
                html += "</div>";
                caption.innerHTML += html;

                newLink.appendChild(newImg);
                newLink.appendChild(caption);
                justifiedContainer.appendChild(newLink);
            }
        });

        // Insert justified gallery container
        postsContainer.parentNode.insertBefore(
            justifiedContainer,
            postsContainer
        );

        // Initialize justified gallery
        $(justifiedContainer).justifiedGallery(JG_CONFIG);

        // Initialize high-res loading
        if (CONFIG.enableHighRes) {
            const imageManager = new ImageManager();
            const observer = createIntersectionObserver(imageManager);

            // Store references for cleanup
            justifiedContainer.imageManager = imageManager;
            justifiedContainer.intersectionObserver = observer;

            // Create toggle button
            if (!document.getElementById("preview-toggle-button")) {
                justifiedContainer.toggleButton =
                    createToggleButton(imageManager);
            }

            // Observe all images and add hover listeners
            const images = justifiedContainer.querySelectorAll("img");
            images.forEach((img) => {
                observer.observe(img);
                if (CONFIG.enableHoverPreview) {
                    addHoverListeners(img, imageManager);
                    img.dataset.hoverListeners = "true";
                }
            });
        }
    }

    function handleDynamicContent() {
        // Watch for dynamically loaded content
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === "childList") {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType === 1) {
                            // Element node
                            // Check if new posts were added
                            if (
                                node.matches &&
                                node.matches("article.post-preview")
                            ) {
                                updateJustifiedGallery();
                            } else if (
                                node.querySelector &&
                                node.querySelector("article.post-preview")
                            ) {
                                updateJustifiedGallery();
                            }
                        }
                    });
                }
            });
        });

        const postsContainer = document.querySelector(".posts-container");
        if (postsContainer) {
            observer.observe(postsContainer, {
                childList: true,
                subtree: true,
            });
        }
    }

    function updateJustifiedGallery() {
        const justifiedContainer = document.getElementById(
            "justified-gallery-container"
        );
        const postsContainer = document.querySelector(".posts-container");

        if (!justifiedContainer || !postsContainer) return;

        // Get new posts that aren't in the justified gallery yet
        const existingLinks = justifiedContainer.querySelectorAll("a");
        const existingHrefs = Array.from(existingLinks).map(
            (link) => link.href
        );

        const newPosts = postsContainer.querySelectorAll(
            "article.post-preview"
        );

        newPosts.forEach((post) => {
            const link = post.querySelector(".post-preview-link");
            const img = post.querySelector(".post-preview-image");

            if (link && img && !existingHrefs.includes(link.href)) {
                //
                const postId = post.getAttribute("data-id");
                const score = post.getAttribute("data-score") || "0";
                // Create new anchor element for justified gallery
                const newLink = document.createElement("a");
                newLink.href = link.href;
                // newLink.title = img.getAttribute("data-title") || img.alt;

                // Create new image element
                const newImg = document.createElement("img");
                newImg.src = img.src;
                newImg.alt = img.alt;
                // newImg.title = img.title;

                // Set data attributes for justified gallery
                newImg.setAttribute("data-safe-src", img.src);

                // Get original dimensions if available
                const width =
                    img.getAttribute("width") || img.naturalWidth || 180;
                const height =
                    img.getAttribute("height") || img.naturalHeight || 180;

                newImg.setAttribute("width", width);
                newImg.setAttribute("height", height);

                // Create caption element
                const caption = document.createElement("div");
                caption.className = "jg-caption";
                caption.addEventListener("click", handleCaptionClick);

                let scoreState;
                if (parseInt(score) > 0) scoreState = "positive";
                else if (parseInt(score) < 0) scoreState = "negative";
                else scoreState = "neutral";

                let html = `
                    <span class="caption-button upvote-btn" data-post-id="${postId}">🡅</span>
                    <span class="score-display ${scoreState}">${score}</span>
                    <span class="caption-button favorite-btn" data-post-id="${postId}">❤︎</span>
                    <div class="right-side-wrapper">
                `;

                // get content duration
                let contentDuration = post.querySelector(
                    "a.post-preview-link div.post-animation-icon .post-duration"
                );
                if (contentDuration) {
                    contentDuration = contentDuration.textContent.trim();
                    const durLabelHTML = `<span class="content-duration">${contentDuration}</span>`;
                    html += durLabelHTML;
                }

                html += `<span class="info-button" data-post-id="${postId}">🛈</span>`;
                html += "</div>";
                caption.innerHTML += html;

                newLink.appendChild(newImg);
                newLink.appendChild(caption);
                justifiedContainer.appendChild(newLink);

                // Observe new images for high-res loading
                if (
                    CONFIG.enableHighRes &&
                    justifiedContainer.intersectionObserver
                ) {
                    const newImages = justifiedContainer.querySelectorAll(
                        "img:not([data-high-res-loading])"
                    );
                    newImages.forEach((img) => {
                        justifiedContainer.intersectionObserver.observe(img);
                    });
                }

                // Add hover listeners to new images
                if (
                    CONFIG.enableHoverPreview &&
                    justifiedContainer.imageManager
                ) {
                    const newImages = justifiedContainer.querySelectorAll(
                        "img:not([data-hover-listeners])"
                    );
                    newImages.forEach((img) => {
                        addHoverListeners(img, justifiedContainer.imageManager);
                        img.dataset.hoverListeners = "true";
                    });
                }
            }
        });

        // Refresh justified gallery
        $(justifiedContainer).justifiedGallery("norewind");
    }

    function createReloadButton() {
        // Create the reload button
        const reloadButton = document.createElement("button");
        reloadButton.textContent = "↻";
        reloadButton.id = "jg-reload-btn";

        const postsSection = document.querySelector("#post-sections li");

        if (postsSection) {
            postsSection.appendChild(reloadButton);
        }

        // reloadButton.addEventListener("click", (e) => {
        //     e.preventDefault();
        //     cleanup();
        //     initializeJustifiedGallery();
        //     handleDynamicContent();
        // });
    }

    function relocateSearchBar() {
        // Find the search form
        const searchForm = document.getElementById("search-box-form");
        const contentSection = document.getElementById("content");

        if (!searchForm || !contentSection) {
            // console.log(
            //     "Search form or content section not found, retrying..."
            // );
            return false;
        }

        // Hide the original search form container to prevent empty space
        const originalContainer = searchForm.closest(
            '.search-box, #search-box, [class*="search"]'
        );
        if (originalContainer && originalContainer !== searchForm) {
            originalContainer.style.display = "none";
        }

        // Create a new container for the relocated search bar
        const searchContainer = document.createElement("div");
        searchContainer.id = "relocated-search-container";
        searchContainer.style.cssText = `
            display: flex;
            justify-content: stretch;
            margin-bottom: 5px;
            padding: 0;
        `;

        // Style the search form
        searchForm.style.cssText = `
            display: flex;
            align-items: center;
            width: 100%;
            background: #1a1a1a;
            border: 1px solid #333;
            border-radius: 4px;
            overflow: hidden;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        `;

        // Style the search input
        const searchInput = searchForm.querySelector("#tags");
        if (searchInput) {
            searchInput.style.cssText = `
                flex: 1;
                padding: 8px 12px;
                border: none;
                background: transparent;
                color: #fff;
                font-size: 14px;
                outline: none;
                min-width: 0;
            `;

            // Add placeholder styling
            searchInput.setAttribute(
                "placeholder",
                searchInput.getAttribute("placeholder") || "Search tags..."
            );
        }

        // Style the search button
        const searchButton = searchForm.querySelector("#search-box-submit");
        if (searchButton) {
            searchButton.style.cssText = `
                padding: 8px 12px;
                border: none;
                background: #0066cc;
                color: white;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: background-color 0.2s;
                min-width: 40px;
            `;

            // Add hover effect
            searchButton.addEventListener("mouseenter", function () {
                this.style.backgroundColor = "#0052a3";
            });

            searchButton.addEventListener("mouseleave", function () {
                this.style.backgroundColor = "#0066cc";
            });
        }

        // Style the search icon
        const searchIcon = searchForm.querySelector(".search-icon");
        if (searchIcon) {
            searchIcon.style.cssText = `
                width: 16px;
                height: 16px;
            `;
        }

        // Move the search form to the new container
        searchContainer.appendChild(searchForm);

        // Insert the container at the top of the content section
        contentSection.insertBefore(searchContainer, contentSection.firstChild);

        // console.log('Search bar successfully relocated!');
        return true;
    }

    function addJgResetHandlers(cssSelectorArray) {
        // add event handlers to reset justified gallery
        setTimeout(() => {
            if (cssSelectorArray.length > 0) {
                cssSelectorArray.forEach((el_sel) => {
                    const btn = document.querySelector(el_sel);
                    if (btn) {
                        btn.addEventListener("click", () => {
                            setTimeout(() => {
                                cleanup();
                                initializeJustifiedGallery();
                                handleDynamicContent();
                            }, 0);
                        });
                    }
                });
            }
        }, 500);
    }

    // ================

    // Inject Justified Gallery CSS
    let css = `
            @import url("https://cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.8.1/css/justifiedGallery.min.css");

            /* Custom styles for Danbooru integration */
            .justified-gallery {
                margin: 0;
                padding: 0;
            }

            .justified-gallery > a {
                border: 2px solid transparent;
                border-radius: 6px;
                background: none !important;
                transition: border 0.2s ease, box-shadow 0.2s ease;
            }

            .justified-gallery > a:hover {
                border: 2px solid #faf9f6;
                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
            }

            .justified-gallery > a > img {
                transition: transform 0.2s ease;
            }

            .justified-gallery > a:hover > img {
                transform: scale(1.02);
            }

            /* Hide original gallery */
            .post-gallery-grid .posts-container {
                display: none;
            }

            /* Ensure proper spacing */
            #posts {
                padding: 10px 0px;
            }

            /* Loading indicator */
            .jg-loading {
                opacity: 0.6;
            }

            .justified-gallery > a > img {
                border-radius: 4px;
                transition: transform 0.2s ease, opacity 0.3s ease;
            }

            .justified-gallery > a > img[data-high-res-loading="true"] {
                opacity: 1;
            }

            .justified-gallery > a > img[data-high-res-loaded="true"] {
                opacity: 1;
            }

            /* Caption styling */

            .jg-caption {
                transition: border 0.2s ease, box-shadow 0.2s ease !important;
                padding: 4px !important;
                font-size: 14px !important;
                font-weight: 500 !important;
            }

            .caption-button {
                opacity: 1;
                /*font-size: 14px;
                                font-weight: 600;*/
                cursor: pointer;
                padding: 2px 4px;
                border-radius: 2px;
                transition: background-color 0.2s ease;
                user-select: none;
            }

            .caption-button:hover {
                background-color: rgba(255, 255, 255, 0.2);
            }

            /* .caption-button.active {
                        background-color: rgba(255, 255, 255, 0.3);
                    } */

            .score-display {
                /*font-size: 14px;
                                font-weight: 600;*/
                padding: 2px 2px 2px 0px;
            }

            .caption-button.processing {
                opacity: 0.5;
            }

            .caption-button.processing:hover {
                background-color: transparent;
            }

            .jg-caption .score-display.positive {
                color: green;
            }

            .jg-caption .score-display.negative {
                color: red;
            }

            .jg-caption .favorite-btn.active {
                color: red;
            }

            .jg-caption .upvote-btn.active {
                color: green;
            }

            .jg-caption .content-duration {
                padding: 2px 4px;
                right: 8px;
            }

            #danbooru-hover-preview {
                position: fixed;
                z-index: 10000;
                pointer-events: none;
                opacity: 0;
                transition: opacity 0.2s ease;
                overflow: visible; /* Changed from hidden to visible */
            }

            #danbooru-hover-preview.visible {
                opacity: 1;
            }

            #danbooru-hover-preview img {
                display: block;
                /* Removed max-height and max-width constraints */
                /* Size will be controlled by JavaScript and transform scaling */
                height: auto;
                width: auto;
                /* Ensure smooth scaling */
                image-rendering: -webkit-optimize-contrast;
                image-rendering: crisp-edges;
                border: 4px solid #faf9f6;
                border-radius: 4px;
                box-shadow: rgba(0, 0, 0, 0.5) 1px 1px 5px, rgba(0, 0, 0, 0.5) -1px 1px 5px,
                    rgba(0, 0, 0, 0.5) 1px -1px 5px, rgba(0, 0, 0, 0.5) -1px -1px 5px;
            }

            #preview-toggle-button {
                background: rgb(0, 116, 172);
                color: white;
                border: none;
                border-radius: 4px;
                padding: 3px 6px;
                font-size: 13px;
                font-weight: 500;
                cursor: pointer;
                margin-left: 10px;
                transition: all 0.2s ease;
                user-select: none;
                vertical-align: middle;
            }

            #preview-toggle-button {
                background: rgb(0, 116, 172);
                color: white;
                border: none;
                border-radius: 4px;
                padding: 3px 6px;
                font-size: 13px;
                font-weight: 500;
                cursor: pointer;
                margin-left: 10px;
                transition: all 0.2s ease;
                user-select: none;
                vertical-align: middle;
            }

            #preview-toggle-button:hover {
                background: #23598fff;
            }

            #preview-toggle-button:active {
                transform: scale(0.98);
            }

            #preview-toggle-button.disabled {
                background: #666;
                cursor: not-allowed;
            }

            #preview-toggle-button.disabled:hover {
                background: #666;
            }

            #jg-reload-btn {
                background-color: rgb(0, 116, 172);
                color: white;
                border: none;
                padding: 3px 6px;
                border-radius: 4px;
                font-size: 13px;
                cursor: pointer;
                font-weight: 500;
                margin-left: 10px;
                transition: all 0.2s ease;
                user-select: none;
                vertical-align: middle;
            }

            #jg-reload-btn:hover {
                background-color: #23598fff;
            }

            /* Info tooltip styles */
            .info-button {
                cursor: pointer;
                padding: 2px 4px;
                border-radius: 2px;
                transition: background-color 0.2s ease;
                user-select: none;
            }

            .info-button:hover {
                background-color: rgba(255, 255, 255, 0.2);
            }

            .post-info-tooltip {
                position: absolute;
                z-index: 1000;
                background: rgba(20, 20, 20, 0.95);
                border: 1px solid #444;
                border-radius: 6px;
                padding: 8px;
                max-width: 200px;
                min-width: 180px;
                max-height: 300px; /* Enables vertical scrolling when content is too long */
                overflow-y: auto; /* Scrollbar appears only when needed */
                box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
                color: #fff;
                font-size: 11px;
                line-height: 1.3;
                backdrop-filter: blur(8px);
                display: none;
                scrollbar-width: thin; /* For Firefox */
            }

            /* Optional scrollbar styling for WebKit browsers (Chrome, Edge, Safari) */
            .post-info-tooltip::-webkit-scrollbar {
                width: 6px;
            }
            .post-info-tooltip::-webkit-scrollbar-thumb {
                background-color: rgba(255, 255, 255, 0.2);
                border-radius: 4px;
            }

            .post-info-tooltip.visible {
                display: block;
            }

            .tooltip-section {
                margin-bottom: 16px;
            }

            .tooltip-section:last-child {
                margin-bottom: 0;
            }

            .tooltip-section h3 {
                margin: 0 0 8px 0;
                font-size: 14px;
                font-weight: bold;
                color: #ccc;
                border-bottom: 1px solid #444;
                padding-bottom: 4px;
            }

            .tooltip-tags {
                display: flex;
                flex-wrap: wrap;
                gap: 4px;
                margin-bottom: 8px;
            }

            .tooltip-tag {
                padding: 2px 6px;
                border-radius: 3px;
                font-size: 12px;
                text-decoration: none;
                color: white;
                transition: opacity 0.2s ease;
            }

            .tooltip-tag:hover {
                opacity: 0.8;
            }

            .tooltip-info ul {
                list-style: none;
                padding: 0;
                margin: 0;
            }

            .tooltip-info li {
                margin-bottom: 6px;
                font-size: 12px;
                color: #ccc;
            }

            .tooltip-info a {
                color: #66b3ff;
                text-decoration: none;
            }

            .tooltip-info a:hover {
                text-decoration: underline;
            }

            .tooltip-loading {
                text-align: center;
                color: #888;
                padding: 20px;
            }

            .tooltip-exit-button-container {
                position: sticky;
                top: 0;
                z-index: 1001;
            }

            .tooltip-exit-button {
                position: absolute;
                top: 8px;
                right: 8px;
                position: absolute;
                top: 0px;
                right: 0px;
                border: none;
                background: transparent;
                font-size: 20px;
                font-weight: bold;
                cursor: pointer;
                color: #888;
                padding: 0px 4px;
            }

            .tooltip-exit-button:hover {
                color: #000;
            }

            .jg-caption .right-side-wrapper {
            position:absolute;
            right: 4px;
            bottom: 4px;
            }
    `;

    function init() {
        // add styles
        const style = document.createElement("style");
        if (CONFIG.enableHiddenSidebar) css += "#sidebar {display: none;}"; // hide sidebar
        style.textContent = css;
        document.head.appendChild(style);

        // Add reload button to reset JG
        createReloadButton();

        // main fuctions
        initializeJustifiedGallery();
        handleDynamicContent();

        // Add JG reset calls to other buttons on the page

        addJgResetHandlers(JG_RESET_SELECTORS);
        // relocate search bar
        if (CONFIG.enableRelocateSearchBar) relocateSearchBar();

        // add info btn handlers
        addInfoBtnHandlers();

        // Handle page navigation (for single-page apps)
        let currentUrl = window.location.href;
        new MutationObserver(() => {
            if (currentUrl !== window.location.href) {
                currentUrl = window.location.href;
                cleanup();
                setTimeout(() => {
                    initializeJustifiedGallery();
                    handleDynamicContent();
                }, 500);
            }
        }).observe(document.body, { childList: true, subtree: true });

        // Reset on zoom/resize events to avoid HRT bug
        let resizeTimeout;
        window.addEventListener("resize", () => {
            clearTimeout(resizeTimeout); // Clear previous timeout
            resizeTimeout = setTimeout(() => {
                cleanup();
                initializeJustifiedGallery();
                handleDynamicContent();
            }, 300);
        });

        // Prevent error message by defining resetImgSrc as a no-op on images
        if (
            typeof HTMLImageElement !== "undefined" &&
            !HTMLImageElement.prototype.resetImgSrc
        )
            HTMLImageElement.prototype.resetImgSrc = function () {};
    }

    // Initialize when DOM is ready
    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", () => {
            setTimeout(() => {
                init();
            }, 100);
        });
    } else {
        setTimeout(() => {
            init();
        }, 100);
    }
})();