<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
    <title>塔罗·时光</title>
    
    <!-- PWA WebApp 沉浸式声明 -->
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <meta name="apple-mobile-web-app-title" content="塔罗时光">
    <meta name="theme-color" content="#0d0d0f" media="(prefers-color-scheme: dark)">
    <meta name="theme-color" content="#f4f4f6" media="(prefers-color-scheme: light)">
    
    <link rel="manifest" href="./manifest.json">
    <link rel="apple-touch-icon" href="./assets/app-icon.png?v=13">

    <!-- 引入高端中文字体栈 -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@200;300;400&display=swap" rel="stylesheet">
    
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        :root { 
            --gold: #d4af37; 
            --font-occult: 'Didot', 'Bodoni MT', 'Noto Serif SC', 'Songti SC', serif;
            --font-sans: system-ui, -apple-system, sans-serif;
        }
        .dark { 
            --bg: #0d0d0f; --text: #e0e0e0; --card-bg: #1a1a1c; 
            --particle-base: #ffffff; --particle-gold: #d4af37;
        }
        .light { 
            --bg: #f4f4f6; --text: #2c2c2c; --card-bg: #ffffff; 
            --particle-base: #6b6b6b; --particle-gold: #b89324;
        }
        body {
            background-color: var(--bg); color: var(--text);
            font-family: var(--font-sans);
            transition: background-color 0.4s ease, color 0.4s ease;
            user-select: none; -webkit-tap-highlight-color: transparent;
            overflow: hidden; touch-action: none; 
        }
        
        h1, h2, h3, .font-occult, #ai-text-container {
            font-family: var(--font-occult) !important;
            font-weight: 300;
        }

        .frosted {
            backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
            border: 1px solid rgba(212, 175, 55, 0.15); background: rgba(20, 20, 22, 0.65);
            transition: background 0.4s ease, border-color 0.4s ease;
        }
        .light .frosted { background: rgba(255, 255, 255, 0.75); border-color: rgba(212, 175, 55, 0.35); }
        
        canvas { position: absolute; top: 0; left: 0; z-index: 5; width: 100vw; height: 100vh; pointer-events: auto; }
        .layer-10 { z-index: 10; } .layer-20 { z-index: 20; } .layer-40 { z-index: 40; }
        
        .no-scrollbar::-webkit-scrollbar { display: none; }
        .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
        
        .btn-hidden { transform: translate(-50%, 20px) scale(0.9); opacity: 0; pointer-events: none; }
        .btn-reveal {
            transform: translate(-50%, 0) scale(1); opacity: 1; pointer-events: auto;
            transition: transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.6s ease-out;
        }
        
        .mist-reveal {
            opacity: 0;
            animation: mistFade 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
            will-change: filter, opacity, transform;
        }
        @keyframes mistFade {
            0% { filter: blur(10px); opacity: 0; transform: translateY(8px); }
            99% { filter: blur(0px); opacity: 1; transform: translateY(0); }
            100% { filter: none; opacity: 1; transform: translateY(0); }
        }

        /* 🌟 为首页的箭头定制水平横向跳动动画 */
        @keyframes pointLeft {
            0%, 100% { transform: translateX(0); }
            50% { transform: translateX(-6px); }
        }
        .animate-point-left {
            display: inline-block;
            animation: pointLeft 1.5s ease-in-out infinite;
        }

        .safe-top-nav {
            top: max(2rem, env(safe-area-inset-top));
        }
    </style>
</head>
<body class="dark h-screen w-screen relative">

    <canvas id="tarotCanvas"></canvas>

    <!-- 首页容器 -->
    <div id="page-1" class="absolute inset-0 layer-10 flex flex-col items-center justify-center pointer-events-auto transition-opacity duration-500">
        <div class="absolute left-6 right-6 flex justify-between items-start pointer-events-none safe-top-nav" id="top-nav">
            <button id="theme-toggle" class="w-12 h-12 rounded-full frosted shadow-[0_0_15px_rgba(212,175,55,0.2)] active:scale-95 transition-transform pointer-events-auto flex items-center justify-center">
                <span id="theme-icon" class="text-xl leading-none">🌙</span>
            </button>
            
            <div id="profile-btn" class="w-12 h-12 rounded-full frosted flex items-center justify-center shadow-[0_0_15px_rgba(212,175,55,0.3)] active:scale-95 transition-transform cursor-pointer pointer-events-auto">
                <svg class="w-6 h-6 text-[var(--gold)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2">
                    <circle cx="12" cy="12" r="8"></circle>
                    <path d="M12 2v20M2 12h20M4.93 4.93l14.14 14.14M4.93 19.07L19.07 4.93"></path>
                </svg>
            </div>
        </div>

        <div id="start-drawing-area" class="w-64 h-80 rounded-2xl flex flex-col items-center justify-center cursor-pointer active:scale-95 transition-transform mt-10">
            <h1 class="text-4xl tracking-[0.2em] mb-2 text-[var(--gold)] font-occult" id="preview-spread-name">每日指引</h1>
            <p class="text-[13px] opacity-60 tracking-widest mb-10 font-occult">点击中心唤醒牌阵</p>
            <div class="w-14 h-14 rounded-full border border-[var(--gold)] flex items-center justify-center animate-pulse shadow-[0_0_20px_rgba(212,175,55,0.2)]">
                <span class="text-[var(--gold)] text-xl font-light leading-none">✧</span>
            </div>
        </div>
        
        <!-- 🌟 重新设计的超强视觉焦点提示:字号微大、呼吸闪烁、水平箭头动态指引,且文案返璞归真 -->
        <div class="absolute bottom-12 flex items-center text-[12px] text-[var(--gold)] opacity-70 tracking-[0.2em] font-occult animate-pulse">
            <span class="animate-point-left mr-1 font-sans">←</span>
            <span>向左滑动浏览牌阵</span>
        </div>
    </div>

    <!-- 解牌模块 -->
    <div id="decode-module" class="fixed bottom-10 left-[50%] layer-10 flex flex-col items-center btn-hidden w-[90%] max-w-sm">
        <input type="text" id="user-question" placeholder="心中所惑,皆可发问" class="w-full mb-4 px-6 py-4 rounded-2xl frosted border border-[var(--gold)]/30 focus:border-[var(--gold)] text-center text-sm font-occult outline-none transition-all placeholder:text-[var(--text)] placeholder:opacity-30 shadow-lg">
        <button id="decode-btn" class="bg-gradient-to-b from-[var(--card-bg)] to-black border border-[var(--gold)]/60 text-[var(--gold)] px-14 py-3.5 rounded-full text-sm font-bold tracking-[0.2em] cursor-pointer whitespace-nowrap shadow-[0_0_25px_rgba(212,175,55,0.25)] active:scale-95 transition-transform font-occult">
            ✧ 唤醒神谕 ✧
        </button>
        <!-- 🌟 文案去比喻化:“星盘”还原为“牌阵” -->
        <button id="skip-btn" class="mt-5 text-[11px] text-[var(--gold)] opacity-50 tracking-[0.2em] font-occult cursor-pointer active:scale-95 transition-transform">
            ← 收起牌阵,返回大厅
        </button>
    </div>

    <!-- 牌阵选择库 -->
    <div id="page-2c" class="fixed inset-0 bg-[var(--bg)] translate-x-full transition-transform duration-500 layer-20 p-6 flex flex-col pointer-events-auto">
        <div class="absolute left-6 z-50 pointer-events-auto safe-top-nav">
            <button id="back-from-2c" class="text-[var(--gold)] text-lg tracking-widest cursor-pointer font-occult flex items-center h-12 leading-none p-2 -ml-2">← 归还</button>
        </div>
        <!-- 🌟 文案去比喻化:“星轨图鉴”还原为“牌阵图鉴” -->
        <h2 class="text-3xl tracking-[0.1em] mb-8 text-[var(--gold)] mt-20 font-occult">牌阵图鉴</h2>
        <div class="flex-1 overflow-y-auto space-y-4 pb-10 no-scrollbar" id="spread-list"></div>
    </div>

    <!-- 我的牌灵界面 -->
    <div id="page-2a" class="fixed inset-0 translate-y-full transition-transform duration-500 layer-20 flex flex-col pointer-events-none opacity-0">
        <div class="absolute left-6 z-50 pointer-events-auto safe-top-nav">
            <button id="back-from-2a" class="text-[var(--gold)] text-lg tracking-widest cursor-pointer font-occult flex items-center h-12 leading-none p-2 -ml-2">← 大厅</button>
        </div>
        <div id="spirit-ui-text" class="absolute bottom-24 left-0 right-0 text-center pointer-events-none transition-opacity duration-500">
            <p class="text-[var(--gold)] text-2xl tracking-[0.3em] mb-3 font-occult">灵境羁绊</p>
            <p class="text-xs opacity-50 tracking-[0.1em] font-occult" id="spirit-subtitle">点击中心唤醒未知,或向左滑动自选</p>
        </div>
    </div>

    <!-- AI 解牌结果页 -->
    <div id="page-4b" class="fixed inset-0 bg-black/80 backdrop-blur-md translate-y-full transition-transform duration-700 layer-40 p-5 flex flex-col justify-center pointer-events-auto">
        <div class="absolute left-6 z-50 pointer-events-auto safe-top-nav">
            <button id="back-from-4b" class="text-[var(--gold)] text-lg tracking-widest cursor-pointer font-occult flex items-center h-12 leading-none p-2 -ml-2">← 闭卷</button>
        </div>
        <div class="w-full max-h-[80vh] mt-12 rounded-3xl frosted p-6 md:p-8 overflow-y-auto relative flex flex-col shadow-2xl border border-[var(--gold)]/30 no-scrollbar" id="ai-scroll-view">
            <h3 class="text-[var(--gold)] text-sm tracking-[0.2em] mb-6 border-b border-[var(--gold)]/20 pb-4 shrink-0 text-center font-occult">命运密语 · 神谕</h3>
            <div id="ai-text-container" class="text-[15px] opacity-90 leading-loose font-light whitespace-pre-wrap flex-1 pb-8 tracking-wide text-justify font-occult"></div>
        </div>
    </div>

    <script>
        const BACKEND_URL = "https://uz8tmj4pw4.sealoshzh.site/TARRT";
        const FONT_OCCULT = "'Didot', 'Bodoni MT', 'Noto Serif SC', 'Songti SC', serif";

        const RAW_TAROT_DATA = [
            { "name": "The Fool", "number": "0", "arcana": "Major Arcana", "suit": null, "img": "m00.jpg" },
            { "name": "The Magician", "number": "1", "arcana": "Major Arcana", "suit": null, "img": "m01.jpg" },
            { "name": "The High Priestess", "number": "2", "arcana": "Major Arcana", "suit": null, "img": "m02.jpg" },
            { "name": "The Empress", "number": "3", "arcana": "Major Arcana", "suit": null, "img": "m03.jpg" },
            { "name": "The Emperor", "number": "4", "arcana": "Major Arcana", "suit": null, "img": "m04.jpg" },
            { "name": "The Hierophant", "number": "5", "arcana": "Major Arcana", "suit": null, "img": "m05.jpg" },
            { "name": "The Lovers", "number": "6", "arcana": "Major Arcana", "suit": null, "img": "m06.jpg" },
            { "name": "The Chariot", "number": "7", "arcana": "Major Arcana", "suit": null, "img": "m07.jpg" },
            { "name": "Strength", "number": "8", "arcana": "Major Arcana", "suit": null, "img": "m08.jpg" },
            { "name": "The Hermit", "number": "9", "arcana": "Major Arcana", "suit": null, "img": "m09.jpg" },
            { "name": "Wheel of Fortune", "number": "10", "arcana": "Major Arcana", "suit": null, "img": "m10.jpg" },
            { "name": "Justice", "number": "11", "arcana": "Major Arcana", "suit": null, "img": "m11.jpg" },
            { "name": "The Hanged Man", "number": "12", "arcana": "Major Arcana", "suit": null, "img": "m12.jpg" },
            { "name": "Death", "number": "13", "arcana": "Major Arcana", "suit": null, "img": "m13.jpg" },
            { "name": "Temperance", "number": "14", "arcana": "Major Arcana", "suit": null, "img": "m14.jpg" },
            { "name": "The Devil", "number": "15", "arcana": "Major Arcana", "suit": null, "img": "m15.jpg" },
            { "name": "The Tower", "number": "16", "arcana": "Major Arcana", "suit": null, "img": "m16.jpg" },
            { "name": "The Star", "number": "17", "arcana": "Major Arcana", "suit": null, "img": "m17.jpg" },
            { "name": "The Moon", "number": "18", "arcana": "Major Arcana", "suit": null, "img": "m18.jpg" },
            { "name": "The Sun", "number": "19", "arcana": "Major Arcana", "suit": null, "img": "m19.jpg" },
            { "name": "Judgement", "number": "20", "arcana": "Major Arcana", "suit": null, "img": "m20.jpg" },
            { "name": "The World", "number": "21", "arcana": "Major Arcana", "suit": null, "img": "m21.jpg" },
            { "name": "Ace of Cups", "number": "1", "arcana": "Minor Arcana", "suit": "Cups", "img": "c01.jpg" },
            { "name": "Two of Cups", "number": "2", "arcana": "Minor Arcana", "suit": "Cups", "img": "c02.jpg" }
        ];

        function toRoman(num) {
            const roman = { 1: 'I', 2: 'II', 3: 'III', 4: 'IV', 5: 'V', 6: 'VI', 7: 'VII', 8: 'VIII', 9: 'IX', 10: 'X' };
            return roman[num] || num;
        }

        const TAROT_DICT = RAW_TAROT_DATA.map((card, index) => {
            let cardNum = parseInt(card.number);
            let numLabel = "";
            if (card.arcana === "Major Arcana") {
                numLabel = card.name; 
            } else {
                if (cardNum <= 10) numLabel = `${toRoman(cardNum)} · ${card.suit[0]}`;
                else numLabel = card.name;
            }
            let cnName = ["愚者","魔术师","女祭司","皇后","皇帝","教皇","恋人","战车","力量","隐士","命运之轮","正义","倒吊人","死神","节制","恶魔","高塔","星星","月亮","太阳","审判","世界"][index] || card.name;
            return { id: index, num: numLabel, name: cnName, imgUrl: `./assets/cards/${card.img}` };
        });

        let mySpiritCard = null;
        const savedSpiritId = localStorage.getItem('tarot_spirit_id');
        if (savedSpiritId !== null) {
            mySpiritCard = TAROT_DICT.find(c => c.id === parseInt(savedSpiritId)) || null;
        }

        const imageCache = {};
        const fallbackImg = "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?auto=format&fit=crop&w=300&q=80"; 
        function preloadTarotImages() {
            TAROT_DICT.forEach(card => {
                const img = new Image(); img.src = card.imgUrl;
                img.onload = () => { imageCache[card.id] = img; };
                img.onerror = () => { 
                    const fallback = new Image(); fallback.crossOrigin = "Anonymous"; fallback.src = fallbackImg;
                    fallback.onload = () => { imageCache[card.id] = fallback; };
                }; 
            });
        }
        preloadTarotImages();

        const PREDEFINED_SPREADS = [
            { 
                id: 'daily_one', name: '每日指引', desc: '单张牌阵,每日高频使用,获取今日核心启示。', 
                slots: [{ id: 0, x: 0, y: 0, name: '今日核心' }],
                aiPrompt: "这是一个「单张牌阵」。请直接给出今天最核心的运势指引,简明扼要,直击灵魂。"
            },
            { 
                id: 'body_mind_spirit', name: '身心灵', desc: '三张牌阵,探寻身体、头脑与灵魂的内在状态。', 
                slots: [{ id: 0, x: -1.2, y: 0.5, name: '身体(外在)' }, { id: 1, x: 1.2, y: 0.5, name: '头脑(思维)' }, { id: 2, x: 0, y: -0.8, name: '灵魂(潜意识)' }],
                aiPrompt: "这是一个「身心灵」牌阵。请分别解析用户的外在身体行动、内在逻辑思维、以及深层的灵魂潜意识,指出三者的协调或冲突之处。"
            },
            { 
                id: 'time_flow', name: '时光之流', desc: '三张牌阵,洞察过去、现在与未来的因果轨迹。', 
                slots: [{ id: 0, x: -1.2, y: 0, name: '过去之因' }, { id: 1, x: 0, y: 0, name: '当下之果' }, { id: 2, x: 1.2, y: 0, name: '未来之势' }],
                aiPrompt: "这是一个「时光之流」牌阵。请按时间线推演:从过去的执念或因果,过渡到当下的真实境遇,最后精准预判未来的发展趋势。"
            },
            { 
                id: 'relationship', name: '情感羁绊', desc: '四张牌阵,深度剖析两人之间的关系现状与未来走向。', 
                slots: [{ id: 0, x: -1.2, y: 0, name: '本我状态' }, { id: 1, x: 1.2, y: 0, name: '对方视角' }, { id: 2, x: 0, y: -1.0, name: '关系核心' }, { id: 3, x: 0, y: 1.1, name: '未来演变' }],
                aiPrompt: "这是一个「情感羁绊」牌阵。请先分析问卜者自身的感情状态,接着剖析对方的真实想法与视角,随后指出这段关系的核心症结或纽带,最后预测关系未来的演变趋势。"
            },
            { 
                id: 'career_compass', name: '事业罗盘', desc: '四张牌阵,迷茫时为你指明职场破局之路。', 
                slots: [{ id: 0, x: 0, y: -1.2, name: '当前局势' }, { id: 1, x: -1.2, y: 0, name: '潜在挑战' }, { id: 2, x: 1.2, y: 0, name: '突破机遇' }, { id: 3, x: 0, y: 1.2, name: '最终成就' }],
                aiPrompt: "这是一个「事业罗盘」牌阵。请一针见血地指出用户当前的职场或学业局势,分析隐藏的挑战与阻碍,点明破局的机遇所在,最后描绘出顺势而为能达成的最终成就。"
            },
            { 
                id: 'cross', name: '灵感十字', desc: '五张牌阵,全面解析问题的核心、阻碍、过去与未来。', 
                slots: [{ id: 0, x: 0, y: 0, name: '核心现状' }, { id: 1, x: -1.2, y: 0, name: '内部阻碍' }, { id: 2, x: 1.2, y: 0, name: '外部助力' }, { id: 3, x: 0, y: -1.2, name: '过去成因' }, { id: 4, x: 0, y: 1.2, name: '最终结果' }],
                aiPrompt: "这是一个「灵感十字」五张牌阵。请综合分析用户的核心现状,点明阻力与助力,追溯成因,并最终给出解决问题的终极指引。"
            },
            { 
                id: 'two_choices', name: '二选一', desc: '五张牌阵,面临两条道路时,指引不同选择的走向。', 
                slots: [{ id: 0, x: 0, y: 1.2, name: '当前处境' }, { id: 1, x: -1.3, y: 0.1, name: '选择A:现状' }, { id: 2, x: -1.3, y: -1.1, name: '选择A:结果' }, { id: 3, x: 1.3, y: 0.1, name: '选择B:现状' }, { id: 4, x: 1.3, y: -1.1, name: '选择B:结果' }],
                aiPrompt: "这是一个「二选一」牌阵。请先分析用户的当前处境,然后严谨客观地对比【选择A】与【选择B】的现状及最终走向,最后给出高维视角的倾向性建议。"
            },
            {
                id: 'hexagram', name: '六芒星神谕', desc: '七张牌阵,探寻宿命的深度因果与终极奥义。',
                slots: [
                    { id: 0, x: -1.0, y: -0.7, name: '过去(执念)' }, { id: 1, x: 1.0, y: -0.7, name: '现在(实相)' }, { id: 2, x: -1.0, y: 0.7, name: '未来(幻象)' }, 
                    { id: 3, x: 1.0, y: 0.7, name: '环境(外缘)' }, { id: 4, x: 0, y: -1.4, name: '阻碍(业力)' }, { id: 5, x: 0, y: 1.4, name: '建议(解药)' }, { id: 6, x: 0, y: 0, name: '终极神谕' }
                ],
                aiPrompt: "这是一个最高阶的「六芒星神谕」七张牌阵。请进行深度灵性解读:串联过去执念、现在实相与未来幻象的时间轴;分析外部环境与内在业力(阻碍)的撕扯;给出化解困境的灵性解药;最终由位于中心的卡牌给出命运的终极神谕结论。请以大师的口吻,语言需极具穿透力。"
            }
        ];
        
        let currentActiveSpread = PREDEFINED_SPREADS[0]; 
        let lastDrawTime = 0; 

        const dom = {
            themeBtn: document.getElementById('theme-toggle'), themeIcon: document.getElementById('theme-icon'),
            p1: document.getElementById('page-1'), p2c: document.getElementById('page-2c'),
            p2a: document.getElementById('page-2a'), p4b: document.getElementById('page-4b'), 
            decodeModule: document.getElementById('decode-module'),
            previewName: document.getElementById('preview-spread-name'), spreadList: document.getElementById('spread-list'),
            spiritText: document.getElementById('spirit-ui-text'), spiritSub: document.getElementById('spirit-subtitle')
        };

        let isDarkMode = true;
        
        dom.themeBtn.onclick = () => {
            isDarkMode = !isDarkMode;
            document.body.className = isDarkMode ? 'dark h-screen w-screen relative' : 'light h-screen w-screen relative';
            dom.themeIcon.innerText = isDarkMode ? '🌙' : '☀️';
            particles.forEach(p => p.syncThemeColor(isDarkMode));
        };

        let globalDrag = false, globalStartX = 0;
        const startGlobalDrag = (e) => {
            if (e.target.closest('button') || e.target.closest('input') || e.target.closest('#profile-btn') || e.target.closest('#page-2c')) return;
            globalDrag = true; globalStartX = e.clientX || (e.touches && e.touches[0].clientX);
        };
        const endGlobalDrag = (e) => {
            if (!globalDrag) return; globalDrag = false;
            let endX = e.clientX || (e.changedTouches && e.changedTouches[0].clientX);
            if (appPhase === 'idle' && globalStartX - endX > 60) dom.p2c.style.transform = 'translateX(0)';
            if ((appPhase === 'spirit_intro' || appPhase === 'spirit_revealed') && globalStartX - endX > 60) { 
                dom.spiritText.style.opacity = '0'; 
                startSpiritCarousel(); 
            }
        };
        document.addEventListener('touchstart', startGlobalDrag); document.addEventListener('mousedown', startGlobalDrag);
        document.addEventListener('touchend', endGlobalDrag); document.addEventListener('mouseup', endGlobalDrag);
        
        document.getElementById('back-from-2c').onclick = () => dom.p2c.style.transform = 'translateX(100%)';
        
        document.getElementById('skip-btn').onclick = () => {
            dom.decodeModule.classList.remove('btn-reveal');
            dom.decodeModule.classList.add('btn-hidden');
            dom.p1.style.opacity = '1';
            dom.p1.style.pointerEvents = 'auto';
            appPhase = 'idle';
            spreadCards = []; 
            movingCards = [];
            particles = [];
            coreEnergy = 0;
            coreRadius = 0;
        };
        
        let spiritScale = 0; 
        let spiritFlipProgress = 0;

        document.getElementById('profile-btn').onclick = () => {
            dom.p1.style.opacity = '0';
            dom.p1.style.pointerEvents = 'none';
            dom.p2a.classList.remove('translate-y-full'); 
            dom.p2a.style.opacity = '1'; 
            dom.spiritText.style.opacity = '1'; 
            
            spiritScale = 0; 
            spiritFlipProgress = 0;
            
            if (mySpiritCard) {
                appPhase = 'spirit_revealed';
                dom.spiritSub.innerText = "灵境已连结,向左滑动可重铸契约";
            } else {
                appPhase = 'spirit_intro'; 
                dom.spiritSub.innerText = "点击中心唤醒未知,或向左滑动自选";
            }
        };
        
        document.getElementById('back-from-2a').onclick = () => {
            dom.p2a.style.opacity = '0';
            appPhase = 'spirit_exit'; 
        };

        function renderSpreadList() {
            dom.spreadList.innerHTML = PREDEFINED_SPREADS.map(spread => `<div class="frosted p-5 rounded-2xl cursor-pointer active:scale-95 transition-transform border border-transparent ${currentActiveSpread.id === spread.id ? '!border-[var(--gold)]' : ''}" onclick="selectSpread('${spread.id}')"><h3 class="text-[var(--gold)] text-xl mb-1 font-occult">${spread.name}</h3><p class="text-[13px] opacity-60 font-occult">${spread.desc}</p></div>`).join('');
        }
        window.selectSpread = (id) => { currentActiveSpread = PREDEFINED_SPREADS.find(s => s.id === id); dom.previewName.innerText = currentActiveSpread.name; dom.p2c.style.transform = 'translateX(100%)'; renderSpreadList(); };
        renderSpreadList();

        document.getElementById('start-drawing-area').onclick = () => {
            dom.p1.style.opacity = '0';
            setTimeout(() => { dom.p1.style.pointerEvents = 'none'; startDrawingPhase(); }, 300);
        };

        async function fetchAIResponse(prompt, container, scrollView) {
            // 🌟 移除多余的比喻
            container.innerHTML = `<div class="mist-reveal text-center mt-10 font-occult">正在解析牌阵,聆听命运的启示...</div>`;
            try {
                const response = await fetch(BACKEND_URL, {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ prompt: prompt })
                });

                const result = await response.json();
                if (result.choices && result.choices[0].message) {
                    const fullText = result.choices[0].message.content;
                    const paragraphs = fullText.split('\n').filter(p => p.trim() !== "");
                    container.innerHTML = "";
                    
                    paragraphs.forEach((p, index) => {
                        const pEl = document.createElement('div');
                        pEl.className = "mist-reveal mb-5 font-occult";
                        pEl.style.animationDelay = `${index * 0.3}s`; 
                        pEl.innerText = p;
                        container.appendChild(pEl);
                    });
                    
                    setTimeout(() => { scrollView.scrollTop = scrollView.scrollHeight; }, paragraphs.length * 300 + 500);
                } else {
                    container.innerHTML = "能量连接异常,未获得有效启示。";
                }
            } catch (error) {
                container.innerHTML = "连接中断,请检查网络或后端配置。";
            }
        }

        document.getElementById('decode-btn').onclick = () => {
            dom.p4b.style.transform = 'translateY(0)';
            dom.decodeModule.classList.remove('btn-reveal');
            dom.decodeModule.classList.add('btn-hidden');

            const userQuestion = document.getElementById('user-question').value.trim();
            
            let prompt = "";
            if (mySpiritCard) {
                prompt += `【系统最高指令:你的身份是我的专属牌灵“${mySpiritCard.name}”。请完全进入角色,以该牌灵的独特第一人称口吻与性格为我解牌,语气要精简、具有神秘学深度。】\n\n`;
            } else {
                prompt += `【提示:要求回答精炼,直指要害,语气充满诗意和神秘感。】\n\n`;
            }
            
            prompt += `【牌阵专属解读法则】:${currentActiveSpread.aiPrompt}\n\n`;
            
            prompt += `用户意图:${userQuestion ? userQuestion : '未发问,请直接给出现阶段的命运启示'}\n牌面内容:\n`;
            spreadCards.forEach((c, index) => {
                prompt += `- [位置:${currentActiveSpread.slots[index].name}]${c.tarotData.name} (${c.isReversed ? '逆位' : '正位'})\n`;
            });

            fetchAIResponse(prompt, document.getElementById('ai-text-container'), document.getElementById('ai-scroll-view'));
        };
        
        document.getElementById('back-from-4b').onclick = () => location.reload(); 

        const canvas = document.getElementById('tarotCanvas');
        const ctx = canvas.getContext('2d');
        let width, height, appPhase = 'idle'; 
        let CARD_WIDTH, CARD_HEIGHT, particles = [], convergeStartTime = 0; 
        let coreEnergy = 0, coreRadius = 0, targetCoreY = 0;  
        const CONVERGE_DURATION = 1500;

        function resize() {
            width = window.innerWidth; height = window.innerHeight;
            const dpr = window.devicePixelRatio || 1;
            canvas.width = width * dpr; canvas.height = height * dpr;
            ctx.scale(dpr, dpr);
            CARD_WIDTH = Math.min(width * 0.18, 80); 
            CARD_HEIGHT = CARD_WIDTH * 1.75;
            targetCoreY = height * 0.85; 
        }
        window.addEventListener('resize', resize); resize();

        const easeOutBack = (x) => { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); };

        class LightParticle {
            constructor(x, y, isExplosion = false) {
                this.x = x + (Math.random() - 0.5) * CARD_WIDTH * (isExplosion ? 0.5 : 1.5); 
                this.y = y + (Math.random() - 0.5) * CARD_HEIGHT * (isExplosion ? 0.5 : 1.5);
                let speedMulti = isExplosion ? 8 : 1;
                this.vx = (Math.random() - 0.5) * 1.5 * speedMulti; this.vy = ((Math.random() - 0.5) * 1.5 - (isExplosion ? 0 : 0.5)) * speedMulti; 
                this.baseVx = this.vx; this.baseVy = this.vy; this.life = 1.0; this.absorbed = false; 
                this.size = Math.random() > 0.95 ? Math.random() * 2.0 + 1.0 : Math.random() * 0.8 + 0.2; 
                this.isGold = Math.random() > 0.7; this.phaseOffset = Math.random() * 100;
                this.isExplosion = isExplosion; 
                this.syncThemeColor(isDarkMode); 
            }
            syncThemeColor(isDark) { 
                this.colorBase = isDark ? '#ffffff' : '#6b6b6b';
                this.colorGold = isDark ? '#d4af37' : '#b89324';
            }
            update(appPhase) {
                if (this.absorbed) return;
                this.phaseOffset += 0.03; 
                if (appPhase === 'converging') {
                    let dx = width / 2 - this.x; let dy = targetCoreY - this.y; let dist = Math.sqrt(dx*dx + dy*dy);
                    if (dist < 15) { this.absorbed = true; this.life = 0; coreEnergy += this.size * 2; return; }
                    let progress = Math.min(1, (Date.now() - convergeStartTime) / CONVERGE_DURATION);
                    let exponentialPull = Math.pow(progress, 3) * 1.5; let swirlX = -dy / (dist || 1); let swirlY = dx / (dist || 1);
                    let pull = 0.01 + exponentialPull;
                    this.vx += dx * pull + swirlX * (1 - progress) * 1.5; this.vy += dy * pull + swirlY * (1 - progress) * 1.5;
                    let maxSpeed = 15 * progress + 2; let currentSpeed = Math.sqrt(this.vx*this.vx + this.vy*this.vy);
                    if (currentSpeed > maxSpeed) { this.vx = (this.vx / currentSpeed) * maxSpeed; this.vy = (this.vy / currentSpeed) * maxSpeed; }
                    this.vx *= 0.94; this.vy *= 0.94;
                } else if (this.isExplosion) { this.vx *= 0.92; this.vy *= 0.92; this.life -= 0.02; } 
                else { this.vx = this.baseVx + Math.sin(this.phaseOffset) * 0.2; this.vy = this.baseVy + Math.cos(this.phaseOffset * 0.7) * 0.2; if (this.x < width * 0.1) this.vx += 0.02; if (this.x > width * 0.9) this.vx -= 0.02; if (this.y < height * 0.1) this.vy += 0.02; if (this.y > height * 0.9) this.vy -= 0.02; this.life -= 0.003; }
                this.x += this.vx; this.y += this.vy;
            }
            draw(ctx) { if (this.life <= 0 || this.absorbed) return; ctx.globalAlpha = this.life * (appPhase === 'converging' ? 0.9 : 0.6); ctx.fillStyle = this.isGold ? this.colorGold : this.colorBase; ctx.fillRect(this.x, this.y, this.size, this.size); }
        }

        let carouselRotation = 0, targetRotation = 0;
        let availableCards = [], spreadCards = [], movingCards = [];

        function startDrawingPhase() { appPhase = 'drawing'; spreadCards = []; movingCards = []; particles = []; coreEnergy = 0; coreRadius = 0; carouselRotation = 0; targetRotation = 0; let shuffledDict = [...TAROT_DICT].sort(() => Math.random() - 0.5); availableCards = Array.from({length: 22}, (_, i) => ({ id: i, selected: false, tarotData: shuffledDict[i] })); }
        function startSpiritCarousel() { appPhase = 'spirit_carousel'; carouselRotation = 0; targetRotation = 0; availableCards = TAROT_DICT.slice(0, 22).map((tarotData, i) => ({ id: i, selected: false, tarotData: tarotData })); }
        
        function getSlotCoords(index, isExpanded) { 
            const slot = currentActiveSpread.slots[index]; 
            const slotCount = currentActiveSpread.slots.length;
            
            let responsiveFactor = 1;
            if (slotCount === 4) responsiveFactor = 0.85;
            else if (slotCount === 5) responsiveFactor = 0.75;
            else if (slotCount >= 6) responsiveFactor = 0.65;
            
            if (width < 400 && slotCount >= 3) responsiveFactor *= 0.85; 
            
            const spreadScale = (isExpanded ? 1.4 : 0.9) * responsiveFactor; 
            const baseY = isExpanded ? height * 0.4 : height * 0.35; 
            const spacingX = CARD_WIDTH * 1.35 * spreadScale; 
            const spacingY = CARD_HEIGHT * 1.15 * spreadScale; 
            return { x: width / 2 + slot.x * spacingX, y: baseY + slot.y * spacingY, scale: spreadScale }; 
        }

        function getRenderedCarouselCards(isBig = false) {
            let cardsData = []; const radius = isBig ? width * 0.65 : width * 0.45; const centerY = isBig ? height * 0.6 : height * 0.85;
            availableCards.forEach((card, i) => {
                if (card.selected) return;
                const angle = (i / availableCards.length) * Math.PI * 2 + carouselRotation;
                const z = Math.cos(angle); const x = width / 2 + Math.sin(angle) * radius; const y = centerY + z * (isBig ? 20 : 10); 
                const scale = (isBig ? 1.2 : 0.6) + ((z + 1) / 2) * (isBig ? 0.7 : 0.4);
                cardsData.push({ index: i, x, y, scale, z, id: card.id, tarotData: card.tarotData });
            });
            return cardsData.sort((a, b) => a.z - b.z);
        }

        let isCanvasDragging = false, lastX = 0, clickStartTime = 0, hasMoved = false;
        const canvasPointerDown = (e) => { 
            if(appPhase === 'idle' || appPhase === 'finished' || appPhase === 'expanding' || appPhase === 'converging' || appPhase === 'spirit_revealing') return; 
            isCanvasDragging = true; hasMoved = false; lastX = e.clientX || (e.touches && e.touches[0].clientX); clickStartTime = Date.now(); 
        };
        const canvasPointerMove = (e) => { 
            if(!isCanvasDragging) return; let currentX = e.clientX || (e.touches && e.touches[0].clientX); if(Math.abs(currentX - lastX) > 5) hasMoved = true; 
            if((appPhase === 'drawing' || appPhase === 'spirit_carousel') && hasMoved) { targetRotation += (currentX - lastX) * 0.012; lastX = currentX; } 
        };
        const canvasPointerUp = (e) => { 
            if(!isCanvasDragging) return; isCanvasDragging = false; 
            if(!hasMoved && Date.now() - clickStartTime < 300) { checkClick(e.clientX || (e.changedTouches && e.changedTouches[0].clientX), e.clientY || (e.changedTouches && e.changedTouches[0].clientY)); } 
        };
        canvas.addEventListener('touchstart', canvasPointerDown, {passive:false}); canvas.addEventListener('touchmove', canvasPointerMove, {passive:false}); canvas.addEventListener('touchend', canvasPointerUp);
        canvas.addEventListener('mousedown', canvasPointerDown); canvas.addEventListener('mousemove', canvasPointerMove); canvas.addEventListener('mouseup', canvasPointerUp);

        function checkClick(x, y) {
            if (appPhase === 'drawing') {
                if (Date.now() - lastDrawTime < 500) return; 
                if (spreadCards.length >= currentActiveSpread.slots.length) return;
                const renderedCards = getRenderedCarouselCards();
                for (let i = renderedCards.length - 1; i >= 0; i--) { 
                    const c = renderedCards[i]; 
                    if (x > c.x - (CARD_WIDTH*c.scale)/2 && x < c.x + (CARD_WIDTH*c.scale)/2 && y > c.y - (CARD_HEIGHT*c.scale)/2 && y < c.y + (CARD_HEIGHT*c.scale)/2) { 
                        lastDrawTime = Date.now(); drawCard(c.id, c.x, c.y, c.scale); break; 
                    } 
                }
            } else if (appPhase === 'interacting') {
                spreadCards.forEach((sc, i) => { const coords = getSlotCoords(i, true); if (sc.flipProgress === 0 && Math.abs(x - coords.x) < CARD_WIDTH && Math.abs(y - coords.y) < CARD_HEIGHT) sc.isFlipping = true; });
            } else if (appPhase === 'spirit_intro') {
                const cx = width / 2, cy = height / 2 - 40; 
                if (Math.abs(x - cx) < CARD_WIDTH && Math.abs(y - cy) < CARD_HEIGHT) { 
                    if (!mySpiritCard) { 
                        mySpiritCard = TAROT_DICT[Math.floor(Math.random() * 22)]; 
                        localStorage.setItem('tarot_spirit_id', mySpiritCard.id);
                        appPhase = 'spirit_revealing'; 
                        for(let i=0; i<80; i++) particles.push(new LightParticle(cx, cy, true)); 
                    } 
                }
            } else if (appPhase === 'spirit_carousel') {
                if (Date.now() - lastDrawTime < 500) return;
                const renderedCards = getRenderedCarouselCards(true);
                for (let i = renderedCards.length - 1; i >= 0; i--) { 
                    const c = renderedCards[i]; 
                    if (x > c.x - (CARD_WIDTH*c.scale)/2 && x < c.x + (CARD_WIDTH*c.scale)/2 && y > c.y - (CARD_HEIGHT*c.scale)/2 && y < c.y + (CARD_HEIGHT*c.scale)/2) { 
                        lastDrawTime = Date.now();
                        mySpiritCard = c.tarotData; 
                        localStorage.setItem('tarot_spirit_id', mySpiritCard.id);
                        appPhase = 'spirit_revealing'; 
                        for(let k=0; k<80; k++) particles.push(new LightParticle(width/2, height/2 - 40, true)); 
                        break; 
                    } 
                }
            }
        }

        function drawCard(id, startX, startY, startScale) {
            const card = availableCards.find(c => c.id === id); card.selected = true;
            const slotIndex = spreadCards.length; const target = getSlotCoords(slotIndex, false);
            movingCards.push({ id: card.id, startX: startX, startY: startY, startScale: startScale, targetX: target.x, targetY: target.y, targetScale: target.scale, progress: 0 });
            spreadCards.push({ id: card.id, tarotData: card.tarotData, isReversed: Math.random() > 0.5, flipProgress: 0, isFlipping: false, textProgress: 0 });
            if(spreadCards.length === currentActiveSpread.slots.length) setTimeout(startExpansion, 500);
        }

        function startExpansion() { 
            appPhase = 'expanding'; 
            spreadCards.forEach((sc, i) => { const current = getSlotCoords(i, false); const target = getSlotCoords(i, true); movingCards.push({ id: sc.id, startX: current.x, startY: current.y, startScale: current.scale, targetX: target.x, targetY: target.y, targetScale: target.scale, progress: 0 }); }); 
            setTimeout(() => { appPhase = 'interacting'; }, 800); 
        }

        function drawGraphic(x, y, scale, isPlaceholder = false, flipProgress = 0, textProgress = 0, tarotData = null, isReversed = false, hideBackDecor = false, isFocused = false) {
            ctx.save(); ctx.translate(x, y); 
            
            let breathe = 0;
            if (isFocused && isPlaceholder) {
                breathe = (Math.sin(Date.now() / 150) + 1) / 2;
            }

            const flipScale = Math.cos(flipProgress * Math.PI); 
            ctx.scale(scale * flipScale, scale);
            const w = CARD_WIDTH, h = CARD_HEIGHT; const isDark = isDarkMode;
            
            if (isPlaceholder) { 
                ctx.fillStyle = isDark ? '#111114' : '#e5e5e5'; 
                ctx.beginPath(); ctx.roundRect(-w/2, -h/2, w, h, 8); ctx.fill(); 
                
                if (isFocused) {
                    ctx.save();
                    ctx.shadowBlur = 10 + 15 * breathe;
                    ctx.shadowColor = 'rgba(212, 175, 55, 0.8)';
                    ctx.strokeStyle = `rgba(212, 175, 55, ${0.5 + 0.5 * breathe})`;
                    ctx.lineWidth = 2;
                    ctx.stroke();
                    ctx.restore();
                } else {
                    ctx.setLineDash([5, 5]); 
                    ctx.strokeStyle = isDark ? 'rgba(212, 175, 55, 0.3)' : 'rgba(212, 175, 55, 0.6)'; 
                    ctx.stroke(); 
                }
            } 
            else if (flipScale >= 0) { 
                const grad = ctx.createLinearGradient(-w/2, -h/2, w/2, h/2); grad.addColorStop(0, isDark ? '#1c1c24' : '#ffffff'); grad.addColorStop(1, isDark ? '#0a0a0c' : '#f0f0f0'); ctx.fillStyle = grad; ctx.beginPath(); ctx.roundRect(-w/2, -h/2, w, h, 8); ctx.fill(); 
                
                ctx.beginPath(); ctx.roundRect(-w/2, -h/2, w, h, 8);
                ctx.strokeStyle = '#d4af37'; ctx.lineWidth = 1.5; ctx.stroke(); 
                
                if(hideBackDecor) { ctx.fillStyle = 'var(--gold)'; ctx.font = `lighter ${Math.floor(CARD_WIDTH * 0.8)}px 'Didot', serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('O', 0, 0); } 
            } else { 
                ctx.scale(-1, 1); 
                
                ctx.beginPath(); ctx.roundRect(-w/2, -h/2, w, h, 8);
                
                if (tarotData && imageCache[tarotData.id]) { 
                    ctx.save(); ctx.clip(); 
                    if (isReversed) ctx.rotate(Math.PI); 
                    
                    const zoom = 1.18; 
                    const dw = w * zoom, dh = h * zoom;
                    ctx.drawImage(imageCache[tarotData.id], -dw/2, -dh/2, dw, dh); 
                    
                    ctx.beginPath();
                    ctx.roundRect(-w/2 + 5, -h/2 + 5, w - 10, h - 10, 4);
                    ctx.strokeStyle = 'rgba(212, 175, 55, 0.6)';
                    ctx.lineWidth = 1;
                    ctx.stroke();
                    
                    ctx.restore(); 
                } 
                else { ctx.fillStyle = isDark ? '#111114' : '#e5e5e5'; ctx.fill(); }
                
                ctx.beginPath(); ctx.roundRect(-w/2, -h/2, w, h, 8);
                ctx.strokeStyle = '#d4af37'; ctx.lineWidth = 1.5; ctx.stroke();

                if (tarotData && textProgress > 0.01) { 
                    ctx.save(); let t = easeOutBack(textProgress); let startX = w/2 - 20; let endX = w/2 + 8; let currentX = startX + (endX - startX) * t; 
                    ctx.translate(currentX, 0); ctx.rotate(Math.PI / 2); 
                    let label = tarotData.num; if(isReversed) label += " ()"; 
                    
                    let fontSize = 20;
                    ctx.font = `lighter ${fontSize}px ${FONT_OCCULT}`; 
                    let textMetrics = ctx.measureText(label);
                    let maxLen = h * 0.85; 
                    if (textMetrics.width > maxLen) {
                        fontSize = fontSize * (maxLen / textMetrics.width);
                        ctx.font = `lighter ${fontSize}px ${FONT_OCCULT}`;
                    }

                    let blurAmt = Math.max(0, (1 - textProgress) * 5); 
                    ctx.filter = `blur(${blurAmt}px)`; ctx.globalAlpha = Math.min(1, textProgress * 1.5); 
                    
                    let textGrad = ctx.createLinearGradient(0, 10, 0, -20); 
                    textGrad.addColorStop(0, 'rgba(212, 175, 55, 0)'); 
                    textGrad.addColorStop(0.5, isDark ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.8)'); 
                    textGrad.addColorStop(1, isDark ? 'rgba(255, 255, 255, 1)' : 'rgba(0, 0, 0, 1)'); 
                    ctx.fillStyle = textGrad; 
                    
                    ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(label, 0, 0); 
                    ctx.restore(); 
                }
            }
            ctx.restore();
        }

        function render() {
            carouselRotation += (targetRotation - carouselRotation) * 0.08; ctx.clearRect(0, 0, width, height);
            
            if (appPhase.startsWith('spirit_')) {
                if (Math.random() > 0.8 && particles.length < 100) particles.push(new LightParticle(width/2, height/2 - 40));
                
                ctx.save();
                ctx.globalCompositeOperation = isDarkMode ? 'screen' : 'source-over';
                particles.forEach((p, index) => { p.update(appPhase); p.draw(ctx); if (p.life <= 0 || p.absorbed) particles.splice(index, 1); });
                ctx.restore();

                const cy = height/2 - 40;

                if (appPhase === 'spirit_intro') {
                    spiritScale += (2.0 - spiritScale) * 0.1; 
                    drawGraphic(width/2, cy, spiritScale, false, 0, 0, null, false, true); 
                } 
                else if (appPhase === 'spirit_carousel') { 
                    getRenderedCarouselCards(true).forEach(c => drawGraphic(c.x, c.y, c.scale, false, 1, 1, c.tarotData, false)); 
                } 
                else if (appPhase === 'spirit_revealing') { 
                    spiritFlipProgress += 0.02; 
                    let flip = Math.min(spiritFlipProgress * 2, 1);
                    drawGraphic(width/2, cy, 2.0, false, flip, 1, mySpiritCard, false, flip < 0.5);
                    if (spiritFlipProgress >= 0.5) appPhase = 'spirit_revealed';
                }
                else if (appPhase === 'spirit_revealed') { 
                    drawGraphic(width/2, cy, 2.0, false, 1, 1, mySpiritCard, false); 
                    ctx.fillStyle = 'var(--gold)'; ctx.font = `300 24px ${FONT_OCCULT}`; ctx.textAlign = 'center'; 
                    ctx.fillText(mySpiritCard.name, width/2, cy + (CARD_HEIGHT * 2.0)/2 + 50); 
                }
                else if (appPhase === 'spirit_exit') {
                    spiritScale -= 0.15;
                    if (spiritScale > 0) {
                        drawGraphic(width/2, cy, Math.max(0, spiritScale), false, mySpiritCard ? 1 : 0, 1, mySpiritCard, false, !mySpiritCard);
                    } else {
                        dom.p2a.classList.add('translate-y-full');
                        dom.p1.style.opacity = '1'; 
                        dom.p1.style.pointerEvents = 'auto'; 
                        appPhase = 'idle'; 
                        particles = [];
                    }
                }
            } 
            else if (appPhase !== 'idle') {
                const isExpanded = appPhase !== 'drawing';
                
                currentActiveSpread.slots.forEach((s, i) => {
                    const coords = getSlotCoords(i, isExpanded); 
                    let isWaitingSlot = (appPhase === 'drawing' && i === spreadCards.length);
                    drawGraphic(coords.x, coords.y, coords.scale, true, 0, 0, null, false, false, isWaitingSlot); 
                    
                    let textOpacity = 1;
                    if (spreadCards[i]) {
                        textOpacity = Math.max(0, 1 - spreadCards[i].flipProgress * 2);
                    }
                    if (appPhase === 'converging' || appPhase === 'finished') textOpacity = 0;

                    if (textOpacity > 0) {
                        ctx.save();
                        ctx.fillStyle = isDarkMode ? `rgba(255,255,255,${0.6 * textOpacity})` : `rgba(0,0,0,${0.6 * textOpacity})`;
                        ctx.font = `300 12px ${FONT_OCCULT}`;
                        ctx.textAlign = 'center';
                        ctx.fillText(s.name, coords.x, coords.y + (CARD_HEIGHT * coords.scale)/2 + 25);
                        ctx.restore();
                    }
                });
                
                if (appPhase !== 'finished') {
                    ctx.save(); 
                    ctx.globalCompositeOperation = isDarkMode ? 'screen' : 'source-over'; 
                    
                    if (appPhase === 'converging' && coreEnergy > 0) {
                        let targetRadius = Math.min(80, Math.max(10, coreEnergy)); coreRadius += (targetRadius - coreRadius) * 0.1; 
                        if (coreRadius > 1) { let gradient = ctx.createRadialGradient(width/2, targetCoreY, 0, width/2, targetCoreY, coreRadius); gradient.addColorStop(0, isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(212, 175, 55, 0.8)'); gradient.addColorStop(0.3, 'rgba(212, 175, 55, 0.6)'); gradient.addColorStop(1, 'rgba(212, 175, 55, 0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(width/2, targetCoreY, coreRadius, 0, Math.PI*2); ctx.fill(); }
                    }
                    particles.forEach((p, index) => { p.update(appPhase); p.draw(ctx); if (p.life <= 0 || p.absorbed) particles.splice(index, 1); });
                    ctx.restore();
                }
                
                if (appPhase === 'drawing') getRenderedCarouselCards().forEach(c => drawGraphic(c.x, c.y, c.scale, false, 0));
                
                for (let i = movingCards.length - 1; i >= 0; i--) { let mc = movingCards[i]; mc.progress += 0.04; let t = Math.min(mc.progress, 1); let easeT = 1 - Math.pow(1 - t, 3); drawGraphic(mc.startX + (mc.targetX - mc.startX) * easeT, mc.startY + (mc.targetY - mc.startY) * easeT, mc.startScale + (mc.targetScale - mc.startScale) * easeT, false, 0); if (t === 1) movingCards.splice(i, 1); }
                let allFlipped = spreadCards.length > 0;
                
                spreadCards.forEach((sc, i) => {
                    const coords = getSlotCoords(i, isExpanded);
                    if (sc.isFlipping) { sc.flipProgress += 0.035; if(sc.flipProgress >= 1) sc.isFlipping = false; }
                    if (sc.flipProgress >= 0.5) { sc.textProgress += 0.015; if (sc.textProgress > 1) sc.textProgress = 1; if (appPhase === 'interacting' && Math.random() > 0.05) for(let k=0; k<3; k++) particles.push(new LightParticle(coords.x, coords.y)); }
                    if (sc.flipProgress < 1) allFlipped = false;
                    if (!movingCards.some(mc => mc.id === sc.id)) drawGraphic(coords.x, coords.y, coords.scale, false, sc.flipProgress, sc.textProgress, sc.tarotData, sc.isReversed);
                });
                
                if (appPhase === 'interacting' && allFlipped && !movingCards.length && !spreadCards.some(sc => sc.isFlipping)) { 
                    appPhase = 'converging'; 
                    convergeStartTime = Date.now(); 
                    setTimeout(() => { 
                        appPhase = 'finished'; 
                        coreEnergy = 0; 
                        coreRadius = 0; 
                        particles = []; 
                        dom.decodeModule.classList.replace('btn-hidden', 'btn-reveal'); 
                    }, CONVERGE_DURATION); 
                }
            }
            requestAnimationFrame(render);
        }
        render();
    </script>
</body>
</html>