Danbooru Gacha: Ultimate Edition

Adds a premium "gacha" minigame to Danbooru to discover random posts in a fun way.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Danbooru Gacha: Ultimate Edition
// @namespace    https://greasyfork.org/en/users/your-username
// @version      2.1
// @description  Adds a premium "gacha" minigame to Danbooru to discover random posts in a fun way.
// @author       Gemini & You
// @match        https://danbooru.donmai.us/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION (Rarity Colors as Gradients) ---
    const RARITY_CONFIG = {
        UR:  { name: '🌟🌟🌟🌟UR',  style: 'background: linear-gradient(135deg, #ff00cc, #3333ff); color: #fff; text-shadow: 0 0 5px #ff00cc;', border: 'linear-gradient(135deg, #ff00cc, #3333ff)' },
        SSR: { name: '🌟🌟🌟🌟SSR', style: 'background: linear-gradient(135deg, #ffcc00, #ff6600); color: #fff; text-shadow: 0 0 5px #ffcc00;', border: 'linear-gradient(135deg, #ffcc00, #ff6600)' },
        SR:  { name: '🌟🌟🌟SR',  style: 'background: linear-gradient(135deg, #9933ff, #cc66ff); color: #fff; text-shadow: 0 0 5px #9933ff;', border: 'linear-gradient(135deg, #9933ff, #cc66ff)' },
        R:   { name: '🌟🌟R',   style: 'background: linear-gradient(135deg, #00ccff, #0066ff); color: #fff; text-shadow: 0 0 5px #00ccff;', border: 'linear-gradient(135deg, #00ccff, #0066ff)' },
        N:   { name: '🌟N',    style: 'background: linear-gradient(135deg, #b0b0b0, #808080); color: #fff;', border: 'linear-gradient(135deg, #b0b0b0, #808080)' }
    };

    // --- STYLES ---
    GM_addStyle(`
        @import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;700&display=swap');

        :root {
            --gacha-bg: #1a1b26;
            --gacha-panel: #24283b;
            --gacha-accent: #7aa2f7;
            --gacha-text: #c0caf5;
        }

        /* --- Floating Summon Button (Gem style) --- */
        .gacha-float {
            position: fixed;
            width: 70px;
            height: 70px;
            bottom: 30px;
            right: 30px;
            background: linear-gradient(135deg, #3d5afe, #7aa2f7);
            color: #FFF;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 30px;
            z-index: 10000;
            cursor: pointer;
            box-shadow: 0 0 0 0 rgba(61, 90, 254, 0.7);
            animation: gacha-pulse 2s infinite;
            transition: transform 0.2s;
            border: 2px solid rgba(255,255,255,0.3);
            user-select: none;
        }

        .gacha-float::after {
            content: '💎';
            filter: drop-shadow(0 2px 3px rgba(0,0,0,0.3));
        }

        .gacha-float:hover {
            transform: scale(1.1) rotate(10deg);
            background: linear-gradient(135deg, #536dfe, #8cb1ff);
        }

        @keyframes gacha-pulse {
            0% { box-shadow: 0 0 0 0 rgba(61, 90, 254, 0.7); }
            70% { box-shadow: 0 0 0 15px rgba(61, 90, 254, 0); }
            100% { box-shadow: 0 0 0 0 rgba(61, 90, 254, 0); }
        }

        /* --- Modal Overlay (Glassmorphism) --- */
        .gacha-modal {
            display: none;
            position: fixed;
            z-index: 10001;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            background-color: rgba(10, 10, 15, 0.85);
            backdrop-filter: blur(8px); /* Blurs the site behind */
            align-items: center;
            justify-content: center;
            font-family: 'Rajdhani', sans-serif, system-ui;
        }

        /* --- Modal Content --- */
        .gacha-modal-content {
            background: linear-gradient(160deg, var(--gacha-panel) 0%, var(--gacha-bg) 100%);
            padding: 25px;
            border: 1px solid rgba(122, 162, 247, 0.3);
            width: 90%;
            max-width: 1200px;
            max-height: 95vh;
            overflow-y: auto;
            border-radius: 20px;
            color: var(--gacha-text);
            box-shadow: 0 20px 50px rgba(0,0,0,0.8), inset 0 1px 1px rgba(255,255,255,0.1);
            text-align: center;
            position: relative;
            animation: gacha-modal-entry 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); /* Bouncy entry */
        }

        @keyframes gacha-modal-entry {
            from { opacity: 0; transform: scale(0.8) translateY(20px); }
            to { opacity: 1; transform: scale(1) translateY(0); }
        }

        .gacha-modal h2 {
            margin-top: 0;
            font-size: 2.5em;
            text-transform: uppercase;
            letter-spacing: 2px;
            background: linear-gradient(to right, #fff, #7aa2f7);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            text-shadow: 0 2px 10px rgba(122, 162, 247, 0.3);
        }

        /* Close Button */
        .gacha-close {
            position: absolute;
            right: 20px;
            top: 15px;
            color: #666;
            font-size: 30px;
            line-height: 1;
            transition: color 0.2s, transform 0.2s;
            cursor: pointer;
            z-index: 10;
        }

        .gacha-close:hover {
            color: #fff;
            transform: rotate(90deg);
        }

        /* --- Gacha Stage / Result Area --- */
        #gacha-result {
            min-height: 450px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 20px 0;
            /* Subtle hex pattern background */
            background-color: #13141f;
            background-image: radial-gradient(#24283b 1px, transparent 1px);
            background-size: 20px 20px;
            border-radius: 15px;
            padding: 20px;
            flex-wrap: wrap;
            position: relative;
            box-shadow: inset 0 0 20px rgba(0,0,0,0.5);
            perspective: 1500px; /* Required for 3D hover */
            /* overflow: hidden; */
        }

        /* --- DYNAMIC SUMMONING ANIMATION --- */
        .summoning-stage {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            display: flex; align-items: center; justify-content: center;
            pointer-events: none;
            z-index: 50;
        }

        /* The magical circle */
        .magic-circle {
            width: 200px; height: 200px;
            border-radius: 50%;
            border: 4px dotted #7aa2f7;
            box-shadow: 0 0 20px #3d5afe, inset 0 0 20px #3d5afe;
            animation: circle-spin 6s linear infinite;
            position: relative;
            display: flex; align-items: center; justify-content: center;
        }
        .magic-circle::before {
            content: ''; position: absolute;
            width: 140px; height: 140px; border: 2px solid #fff;
            border-radius: 30%; transform: rotate(45deg);
            animation: circle-pulse 1.5s ease-in-out infinite alternate;
        }
        .magic-circle::after {
            content: ''; position: absolute;
            width: 140px; height: 140px; border: 2px solid #fff;
            border-radius: 30%;
            animation: circle-pulse 1.5s ease-in-out infinite alternate-reverse;
        }

        @keyframes circle-spin { 100% { transform: rotate(360deg); } }
        @keyframes circle-pulse {
            0% { transform: rotate(45deg) scale(0.8); opacity: 0.5; box-shadow: 0 0 5px #fff; }
            100% { transform: rotate(45deg) scale(1.1); opacity: 1; box-shadow: 0 0 20px #fff; }
        }

        /* The Flash effect */
        .summon-flash {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background: white;
            opacity: 0;
            pointer-events: none;
            z-index: 100;
        }
        .flash-active { animation: flash-anim 0.8s ease-out forwards; }

        @keyframes flash-anim {
            0% { opacity: 0; }
            10% { opacity: 1; }
            100% { opacity: 0; }
        }

        /* --- CARD STYLES (Keeping structure for JS hover) --- */

        /* Shared entry animation */
        @keyframes gacha-card-reveal {
            from { transform: translateY(50px) scale(0.9); opacity: 0; filter: brightness(5); }
            to { transform: translateY(0) scale(1) translateZ(0); opacity: 1; filter: brightness(1); }
        }

        /* Wrapper to handle the gradient border */
        .gacha-card-wrapper {
            position: relative;
            border-radius: 12px;
            padding: 3px; /* Thickness of the gradient border */
            box-shadow: 0 10px 30px rgba(0,0,0,0.5);
            transform-style: preserve-3d;
            transform: translateZ(0);
            animation: gacha-card-reveal 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
            opacity: 0; /* Hidden until animation starts */
            transition: transform 300ms ease-out, box-shadow 300ms ease-out;
        }

        /* The actual card content */
        .gacha-card {
            background-color: #1a1b26;
            border-radius: 10px;
            overflow: hidden;
            position: relative;
            height: 100%;
            display: flex;
            flex-direction: column;
        }

        .gacha-card-wrapper:hover {
            box-shadow: 0 20px 40px rgba(0,0,0,0.6);
        }

        .gacha-rarity-header {
            padding: 8px;
            font-weight: 700;
            font-size: 1.4em;
            text-align: center;
            text-transform: uppercase;
            letter-spacing: 1px;
        }

        /* Image Placeholder / Container */
        .gacha-image-container {
            position: relative;
            background-color: #000;
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
        }

        .gacha-image {
            width: 100%;
            height: 100%;
            object-fit: contain;
            display: block;
        }

        /* Placeholder skeleton shimmer */
        .gacha-skeleton {
            width: 100%; height: 100%;
            background: #24283b;
            background-image: linear-gradient(to right, #24283b 0%, #2f354b 20%, #24283b 40%, #24283b 100%);
            background-repeat: no-repeat;
            background-size: 800px 100%;
            animation: gacha-shimmer 1.5s infinite linear;
        }
        @keyframes gacha-shimmer { 0% { background-position: -800px 0; } 100% { background-position: 800px 0; } }

        .gacha-info {
            padding: 15px;
            background-color: #1f2335;
            border-top: 1px solid rgba(255,255,255,0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .gacha-info span { font-weight: bold; color: #fff; }
        .gacha-btn-link {
            background: rgba(255,255,255,0.1);
            padding: 5px 10px; border-radius: 4px;
            color: var(--gacha-accent); text-decoration: none;
            font-size: 0.9em; transition: all 0.2s;
        }
        .gacha-btn-link:hover { background: var(--gacha-accent); color: #fff; }

        /* --- Multi-Summon Grid --- */
        .gacha-grid-container {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
            gap: 20px;
            width: 100%;
            padding: 10px;
            perspective: 1500px;
        }

        .gacha-grid-item-wrapper {
            position: relative;
            border-radius: 10px;
            padding: 2px; /* Thinner border for grid */
            transform-style: preserve-3d;
            transform: translateZ(0);
            animation: gacha-card-reveal 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
            opacity: 0;
            transition: transform 300ms ease-out, box-shadow 300ms ease-out;
            box-shadow: 0 5px 15px rgba(0,0,0,0.4);
        }

        .gacha-grid-item {
            background-color: #1a1b26;
            border-radius: 8px;
            overflow: hidden;
            position: relative;
            height: 100%;
            display: flex; flex-direction: column;
        }

        .gacha-grid-item-wrapper:hover { z-index: 10; box-shadow: 0 15px 30px rgba(0,0,0,0.6); }

        .gacha-grid-item .gacha-rarity-header { font-size: 1em; padding: 4px; }
        .gacha-grid-item img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block; transition: opacity 0.3s; }
        .gacha-grid-item a:hover img { opacity: 0.8; }

        /* --- The GLOW (from original script) --- */
        .gacha-card-wrapper .glow, .gacha-grid-item-wrapper .glow {
            position: absolute;
            width: 100%;
            height: 100%;
            left: 0;
            top: 0;
            border-radius: inherit;
            background-image: radial-gradient(circle at 50% -20%, #ffffff22, #0000000f);
            pointer-events: none;
        }

        /* --- Control Panel --- */
        .gacha-controls { margin-top: 25px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 20px; }
        #gacha-tags-info { color: #7aa2f7; font-family: monospace; background: rgba(0,0,0,0.2); display: inline-block; padding: 2px 8px; border-radius: 4px; margin-bottom: 15px;}

        .gacha-button-container { display: flex; justify-content: center; gap: 20px; }

        /* Stylish Buttons */
        .gacha-summon-button {
            position: relative;
            color: white;
            padding: 12px 30px;
            border: none;
            border-radius: 50px;
            cursor: pointer;
            font-size: 18px; font-weight: 700; font-family: 'Rajdhani', sans-serif;
            text-transform: uppercase; letter-spacing: 1px;
            overflow: hidden; transition: all 0.2s;
            box-shadow: 0 4px #00000044, 0 0 10px rgba(0,0,0,0.2);
            top: 0;
        }

        .gacha-summon-button::before {
            content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
            transition: 0.4s;
        }
        .gacha-summon-button:hover::before { left: 100%; }

        .gacha-summon-button:active:not(:disabled) { top: 4px; box-shadow: 0 0px #00000044; }

        #gacha-summon-button { background: linear-gradient(to bottom, #4ade80, #16a34a); text-shadow: 0 1px 2px rgba(0,0,0,0.3); }
        #gacha-summon-multi-button { background: linear-gradient(to bottom, #60a5fa, #2563eb); text-shadow: 0 1px 2px rgba(0,0,0,0.3); }

        .gacha-summon-button:hover:not(:disabled) { filter: brightness(1.15); transform: translateY(-2px); box-shadow: 0 6px #00000044, 0 0 15px rgba(255,255,255,0.3); }

        .gacha-summon-button:disabled {
            background: #333 !important; color: #777;
            box-shadow: none; cursor: not-allowed; top: 0; transform: none;
        }

        /* Scrollbar styling for modal */
        .gacha-modal-content::-webkit-scrollbar { width: 8px; }
        .gacha-modal-content::-webkit-scrollbar-track { background: #1a1b26; }
        .gacha-modal-content::-webkit-scrollbar-thumb { background-color: #3d5afe; border-radius: 10px; }
    `);

    // --- HTML SETUP ---
    const floatingButton = document.createElement('div');
    floatingButton.className = 'gacha-float';
    floatingButton.title = "Open Gacha";
    document.body.appendChild(floatingButton);

    const modal = document.createElement('div');
    modal.className = 'gacha-modal';
    modal.innerHTML = `
        <div class="gacha-modal-content">
            <span class="gacha-close">&times;</span>
            <h2>Danbooru Gacha</h2>
            <div class="gacha-controls">
                <p id="gacha-tags-info">Tags: None</p>
                <div class="gacha-button-container">
                    <button id="gacha-summon-button" class="gacha-summon-button">Summon x1</button>
                    <button id="gacha-summon-multi-button" class="gacha-summon-button">Summon x10</button>
                </div>
            </div>
            <div id="gacha-result">
                <div id="gacha-flash-overlay" class="summon-flash"></div>
                <div id="gacha-placeholder-text" style="font-size: 1.2em; color: #7aa2f7; opacity: 0.7;">
                    Press a button to initiate summoning sequence.
                </div>
            </div>
        </div>
    `;
    document.body.appendChild(modal);

    // --- LOGIC ---
    const closeButton = modal.querySelector('.gacha-close');
    const summonButton = document.getElementById('gacha-summon-button');
    const summonMultiButton = document.getElementById('gacha-summon-multi-button');
    const resultContainer = document.getElementById('gacha-result');
    const flashOverlay = document.getElementById('gacha-flash-overlay');
    const tagsInfo = document.getElementById('gacha-tags-info');

    let isSummoning = false;

    const openModal = () => {
        document.body.style.overflow = 'hidden';
        const currentTags = document.getElementById('tags')?.value?.trim() || '';
        tagsInfo.textContent = currentTags ? `Tags: ${currentTags}` : 'Tags: None (All Posts)';
        modal.style.display = 'flex';
    };

    const closeModal = () => {
        if (isSummoning) return;
        document.body.style.overflow = '';
        modal.style.display = 'none';
    };

    const getRarityParams = (score) => {
        if (score >= 200) return RARITY_CONFIG.UR;
        if (score >= 80)  return RARITY_CONFIG.SSR;
        if (score >= 30)  return RARITY_CONFIG.SR;
        if (score >= 10)  return RARITY_CONFIG.R;
        return RARITY_CONFIG.N;
    };

    const setUIState = (summoning) => {
        isSummoning = summoning;
        summonButton.disabled = summoning;
        summonMultiButton.disabled = summoning;
        closeButton.style.opacity = summoning ? '0.3' : '1';
        closeButton.style.pointerEvents = summoning ? 'none' : 'auto';
    };

    const fetchRandomPost = (tags = '') => {
        return new Promise((resolve, reject) => {
            let url = '/posts/random.json';
            const trimmedTags = tags.trim();
            if (trimmedTags) url += `?tags=${encodeURIComponent(trimmedTags)}`;

            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const post = JSON.parse(response.responseText);
                            if (post && post.id && (post.file_url || post.preview_file_url)) {
                                resolve(post);
                            } else {
                                resolve(fetchRandomPost(tags));
                            }
                        } catch (e) { reject('Parsing error.'); }
                    } else { reject(`Error: ${response.status}`); }
                },
                onerror: () => reject('Network error.')
            });
        });
    };

    const showSummoningAnimation = () => {
        Array.from(resultContainer.children).forEach(child => {
            if (child.id !== 'gacha-flash-overlay') child.remove();
        });
        const stage = document.createElement('div');
        stage.className = 'summoning-stage';
        stage.innerHTML = '<div class="magic-circle"></div>';
        resultContainer.appendChild(stage);
        return stage;
    };

    const triggerReveal = (contentNode, summoningStage) => {
        return new Promise(resolve => {
            flashOverlay.classList.add('flash-active');
            setTimeout(() => {
                if (summoningStage) summoningStage.remove();
                resultContainer.appendChild(contentNode);
                addHoverEffects(resultContainer); // Apply hover to the whole result area
                setTimeout(() => {
                    flashOverlay.classList.remove('flash-active');
                    resolve();
                }, 700);
            }, 150);
        });
    };

    const performSingleSummon = async () => {
        if (isSummoning) return;
        setUIState(true);
        const tags = document.getElementById('tags')?.value || '';
        const stage = showSummoningAnimation();
        const minTimePromise = new Promise(r => setTimeout(r, 1500));
        const fetchPromise = fetchRandomPost(tags).catch(e => ({ error: e }));
        const [_, postData] = await Promise.all([minTimePromise, fetchPromise]);

        if (postData.error) {
            resultContainer.innerHTML = `<p style="color: #ff5555; font-size: 1.2em;">Summon Failed: ${postData.error}</p>`;
            setUIState(false);
            return;
        }

        const post = postData;
        const rarity = getRarityParams(post.score);
        const fixedHeightVh = 50;
        const cleanH = post.image_height || 1000;
        const cleanW = post.image_width || 1000;
        const aspectRatio = cleanW / cleanH;
        const fixedHeightPx = window.innerHeight * (fixedHeightVh / 100);
        const calculatedWidth = fixedHeightPx * aspectRatio;

        const cardWrapper = document.createElement('div');
        cardWrapper.className = 'gacha-card-wrapper js-tilt';
        cardWrapper.style.background = rarity.border;
        cardWrapper.innerHTML = `
            <div class="gacha-card">
                <div class="glow"></div>
                <div class="gacha-rarity-header" style="${rarity.style}">${rarity.name}</div>
                <div class="gacha-image-container" style="height: ${fixedHeightVh}vh; width: ${calculatedWidth}px; max-width: 100%;">
                    <div class="gacha-skeleton" id="gacha-skel-${post.id}"></div>
                    <a href="/posts/${post.id}" target="_blank" style="display:none;" id="gacha-link-${post.id}">
                        <img class="gacha-image" alt="Loading..." id="gacha-img-${post.id}">
                    </a>
                </div>
                <div class="gacha-info">
                    <span>Score: ${post.score}</span>
                    <a href="/posts/${post.id}" target="_blank" class="gacha-btn-link">View Post ↗</a>
                </div>
            </div>`;

        const img = new Image();
        img.src = post.large_file_url || post.file_url;
        img.onload = () => {
            const skel = cardWrapper.querySelector(`#gacha-skel-${post.id}`);
            const link = cardWrapper.querySelector(`#gacha-link-${post.id}`);
            const htmlImg = cardWrapper.querySelector(`#gacha-img-${post.id}`);
            if (skel && link && htmlImg) {
                htmlImg.src = img.src;
                skel.style.display = 'none';
                link.style.display = 'block';
                htmlImg.animate([{opacity:0}, {opacity:1}], {duration: 300, fill: 'forwards'});
            }
        };

        await triggerReveal(cardWrapper, stage);
        setUIState(false);
    };

    const performMultiSummon = async () => {
        if (isSummoning) return;
        setUIState(true);
        const tags = document.getElementById('tags')?.value || '';
        const stage = showSummoningAnimation();
        const minTimePromise = new Promise(r => setTimeout(r, 2000));
        const promises = Array(10).fill(null).map(() => fetchRandomPost(tags).catch(() => null));
        const [_, results] = await Promise.all([minTimePromise, Promise.all(promises)]);
        const validPosts = results.filter(p => p !== null);

        if (validPosts.length === 0) {
            resultContainer.innerHTML = `<p style="color: #ff5555;">Failed to summon any posts.</p>`;
            setUIState(false);
            return;
        }

        const grid = document.createElement('div');
        grid.className = 'gacha-grid-container';
        validPosts.forEach((post, index) => {
            const rarity = getRarityParams(post.score);
            const itemWrapper = document.createElement('div');
            itemWrapper.className = 'gacha-grid-item-wrapper js-tilt';
            itemWrapper.style.background = rarity.border;
            itemWrapper.style.animationDelay = `${index * 0.08}s`;
            const imgUrl = post.preview_file_url || post.file_url;
            itemWrapper.innerHTML = `
                <div class="gacha-grid-item">
                    <div class="glow"></div>
                    <div class="gacha-rarity-header" style="${rarity.style}">${rarity.name}</div>
                    <a href="/posts/${post.id}" target="_blank" style="flex-grow: 1; background:#000; display:flex;">
                         <img src="${imgUrl}" alt="${post.id}" loading="lazy">
                    </a>
                </div>`;
            grid.appendChild(itemWrapper);
        });

        await triggerReveal(grid, stage);
        setUIState(false);
    };

    // --- 3D HOVER EFFECT LOGIC (RESTORED FROM ORIGINAL SCRIPT) ---
    function addHoverEffects(container) {
        const cards = container.querySelectorAll('.js-tilt');
        cards.forEach(card => {
            card.addEventListener('animationend', (e) => {
                if (e.animationName === 'gacha-card-reveal') {
                    card.style.animation = 'none';
                    card.style.opacity = '1';
                }
            }, { once: true });

            let bounds;

            function rotateToMouse(e) {
                const mouseX = e.clientX;
                const mouseY = e.clientY;
                const leftX = mouseX - bounds.x;
                const topY = mouseY - bounds.y;
                const center = {
                    x: leftX - bounds.width / 2,
                    y: topY - bounds.height / 2
                };
                // Re-calculate distance from center for the original log-based rotation effect.
                const distance = Math.sqrt(center.x ** 2 + center.y ** 2);

                // Restore the original transform logic for the requested hover effect.
                card.style.transform = `
                    scale3d(1.07, 1.07, 1.07)
                    rotate3d(
                        ${-center.y / 100},
                        ${center.x / 100},
                        0,
                        ${Math.log(distance) * 2}deg
                    )`;

                const glow = card.querySelector('.glow');
                if (glow) {
                    // Restore the original glow effect logic.
                    glow.style.backgroundImage = `
                        radial-gradient(
                            circle at
                            ${center.x * 2 + bounds.width / 2}px
                            ${center.y * 2 + bounds.height / 2}px,
                            #ffffff55,
                            #0000000f
                        )`;
                }
            }

            card.addEventListener('mouseenter', () => {
                bounds = card.getBoundingClientRect();
                document.addEventListener('mousemove', rotateToMouse);
            });

            card.addEventListener('mouseleave', () => {
                document.removeEventListener('mousemove', rotateToMouse);
                card.style.transform = ''; // Reset transform
                const glow = card.querySelector('.glow');
                if (glow) {
                    glow.style.backgroundImage = ''; // Reset glow
                }
            });
        });
    }

    // --- EVENTS ---
    floatingButton.addEventListener('click', openModal);
    closeButton.addEventListener('click', closeModal);
    summonButton.addEventListener('click', performSingleSummon);
    summonMultiButton.addEventListener('click', performMultiSummon);

    modal.addEventListener('mousedown', (event) => {
        if (event.target === modal) closeModal();
    });

    document.addEventListener('keydown', (event) => {
        if (event.key === 'Escape' && modal.style.display === 'flex') closeModal();
    });

})();