{"asset_map": {"sunset_garden_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/sunset_garden_complete_8f7bb9bd-a76d-4be7-86dd-78878dd7d7ed.webp", "type": "image", "prompt": "Generate a stunning, professional-quality Art Nouveau stained-glass panel in a 9:16 tall aspect ratio. The scene depicts a breathtaking sunset garden with a radiant golden and orange sun dominating the upper sky. Foreground features intricate silhouettes of flowers, vines, and ornamental plants in deep jewel tones like emerald green, royal purple, and peacock teal. Incorporate flowing organic patterns and elegant decorative borders characteristic of Alphonse Mucha. Ensure high contrast, rich color gradients, and multiple layers of depth. The image must have clean, crisp edges with no white borders or artifacts, optimized for a high-end mobile puzzle game.", "description": "A breathtaking Art Nouveau stained-glass portrait of a sunset garden, serving as the game's high-quality hero image.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "ocean_depths_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ocean_depths_complete_842209d6-ae5a-4b1f-9ba0-74a7bfc9a25c.webp", "type": "image", "prompt": "Refine the Art Nouveau stained-glass artwork from image 1. Remove all white borders, white edges, and transparency halos. Ensure the stained-glass lead lines and vibrant glass textures extend fully to the edges of the frame. The artwork should be set against a solid deep black background to enhance the glowing glass effect, appearing as a complete, professional 9:16 tall stained-glass panel with no visible white gaps or artifacts.", "description": "A polished, portrait-oriented Art Nouveau stained-glass underwater scene with clean edges and no artifacts.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "cosmic_mandala_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cosmic_mandala_complete_ae457be7-e171-4704-bd46-eb0df40c36d1.webp", "type": "image", "prompt": "Refine and fix the square-formatted Art Nouveau stained-glass cosmic mandala in image 1. Remove all white borders, white edges, and transparency halos. Ensure the stained-glass artwork or a solid dark obsidian background extends perfectly to all four edges. The colors should be vibrant with deep lead cames (black lines) separating the glass pieces. High-fidelity restoration of the radial symmetry, ensuring no white gaps or fringing remain, creating a professional and clean game asset suitable for a puzzle.", "description": "A polished, square Art Nouveau stained-glass cosmic mandala with radial symmetry and clean edges, free of white artifacts.", "subtype": "object", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "tray_background_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/tray_background_texture_42341a52-cd4a-43aa-85c3-747b1c8db801.webp", "type": "image", "prompt": "Generate a seamless, tileable texture of a dark, rich surface resembling fine velvet or felt. Color is deep charcoal gray (#2B2D42) with very subtle grain and matte finish. Soft lighting with a slight vignette effect towards the edges. Designed to repeat seamlessly in both horizontal and vertical directions for use as a game UI background. No high-contrast patterns, keeping it subtle and sophisticated.", "description": "A seamless dark velvet texture for the bottom fragment tray.", "subtype": "ui", "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "ui_panel_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ui_panel_texture_a37dc332-d88d-40c7-9f3c-2a8e6f69dae8.webp", "type": "image", "prompt": "Generate an elegant square UI panel with a frosted glass effect and a translucent center. Features an Art Nouveau inspired decorative border with elegant curves and geometric patterns in metallic gold (#FFD60A). The main panel body is a semi-transparent cream color (#F8F9FA) with a slight iridescent shimmer. 60px wide decorative border around the edges. High resolution, professional game UI asset, transparent background.", "description": "An elegant frosted glass UI panel with an Art Nouveau decorative border.", "subtype": "ui", "aspect_ratio": [886, 889], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "fragment_pickup_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/fragment_pickup_sound_b0d29a58-836a-46f3-a595-dd603a6f6c06.mp3", "type": "audio", "prompt": "Short, gentle crystalline chime. High-pitched glass clink with a soft attack and 0.3s decay. Delicate, precious, and light with a subtle airy reverb. No harsh transients, sounds like a small glass ornament being touched.", "description": "A delicate glass chime sound for when a player picks up a puzzle piece.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0.75, "loop": false, "animation": null}, "fragment_snap_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/fragment_snap_sound_beffe80c-d007-4dd6-83a3-2a2aaaef1966.mp3", "type": "audio", "prompt": "Crisp and clear glass 'click' or 'snap' effect. Medium pitch with rich crystalline harmonics. Sounds like a perfect physical fit of two glass edges. Conclusive, rewarding, and precise with a 0.5s shimmering reverb tail. High-quality foley.", "description": "A satisfying, resonant snap sound for correct piece placement.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.07, "loop": false, "animation": null}, "fragment_return_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/fragment_return_sound_eec866e3-b122-4fec-803f-878c377fdd60.mp3", "type": "audio", "prompt": "Gentle descending wooden or glass tone, soft 'whoosh' followed by a light tinkle. Low-impact, neutral, and forgiving. 0.6s duration. Non-punishing, airy, and smooth transition back to the tray area.", "description": "A soft, forgiving sound for when a piece returns to the tray.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.6, "loop": false, "animation": null}, "puzzle_complete_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/puzzle_complete_sound_eae6434b-d02e-470b-881a-167d5be773ee.mp3", "type": "audio", "prompt": "Magical 3-second celebratory fanfare. Ascending melodic sequence of crystalline bells and warm glockenspiel. Builds into a harmonious major chord with a rich shimmer effect and long, graceful reverb fade. Triumphant, joyful, and high-quality production.", "description": "A magical and triumphant fanfare for completing a stained-glass image.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 3.12, "loop": false, "animation": null}, "ambient_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/music/ambient_music_056d5570-d950-4f0c-8620-5dfb73fa9804.mp3", "type": "audio", "prompt": "Meditative ambient background music, 70 BPM. Minimalist soft piano notes layered over warm, evolving synthesizer pads and very sparse, gentle wind chimes. Serene, contemplative, and zen-like atmosphere. No aggressive melodies. Designed for a seamless loop, low-intensity, high-transparency for a puzzle game.", "description": "A serene and meditative background track to enhance focus.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 90.04408163265306, "loop": true, "animation": null}, "butterfly_garden_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/butterfly_garden_complete_259f9ba9-f3be-4e52-8440-5f7ad62b7bc6.webp", "type": "image", "prompt": "Refine the stained-glass artwork in image 1 by removing all white borders, white edges, and transparency artifacts. Ensure the Art Nouveau butterfly and flower design extends fully to the edges or transitions into a solid black leaded-glass frame. Eliminate any white fringing or halos, ensuring a clean, professional finish suitable for a high-quality mobile puzzle game. Maintain the vibrant colors and intricate glass textures of the original.", "description": "A polished Art Nouveau stained-glass panel of a butterfly garden with clean, artifact-free edges.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "aurora_mountains_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/aurora_mountains_complete_83b5bf6c-93f8-4390-8113-751daf5d9e3b.webp", "type": "image", "prompt": "Refine and clean image 1: Remove all white borders, white edges, and transparency halos. Ensure the Art Nouveau stained-glass artwork of snowy mountains and aurora borealis extends fully to the edges or transitions into a solid dark/black background frame. The final image must be a 9:16 tall portrait-oriented glass panel with high contrast, vibrant colors, and absolutely no white fringing or gaps at the borders.", "description": "A cleaned-up, portrait-oriented Art Nouveau stained-glass artwork of snowy mountains under an aurora borealis, free of white artifacts.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "phoenix_rising_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/phoenix_rising_complete_471fbf69-093b-4cc7-a335-41efe1f2ccee.webp", "type": "image", "prompt": "Refine and clean the existing phoenix stained-glass artwork in image 1. Remove all white borders, white edges, and transparency halos. Ensure the Art Nouveau stained-glass design extends fully to the edges or sits against a solid, deep black background to mimic a real glass panel. Eliminate all white fringing and artifacts to ensure a polished, high-quality professional game asset with clean, sharp transitions.", "description": "A cleaned-up Art Nouveau stained-glass portrait of a rising phoenix with no white borders or artifacts.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "cherry_blossoms_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cherry_blossoms_complete_7fdb6021-2b14-4937-bfa4-464f8304f7e4.webp", "type": "image", "prompt": "Refine the Art Nouveau stained-glass artwork from image 1. Remove all white borders, white edges, and transparency artifacts. Ensure the Japanese-inspired cherry blossom design extends fully to the edges of the frame. The final image must have a solid, deep black background behind the glass leading and colored panes to create a perfect stained-glass look with no visible white fringing or halos. High resolution, 9:16 portrait orientation, professional quality restoration.", "description": "A polished Art Nouveau stained-glass artwork featuring Japanese cherry blossoms, fixed to remove all white borders and artifacts.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "egyptian_temple_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/egyptian_temple_complete_d8e0bbbc-7cff-4e5e-874e-8761cb654604.webp", "type": "image", "prompt": "Refine and fix the Egyptian temple stained-glass artwork from image 1. Remove all white borders, white edges, and transparency halos. Ensure the Art Nouveau stained-glass design extends fully to the edges or transitions into a solid black background. The final image must be a clean, portrait-oriented 9:16 panel with no white gaps, fringing, or visible artifacts, maintaining the elegant decorative lead lines and vibrant glass colors.", "description": "A polished Art Nouveau stained-glass artwork of an Egyptian temple with clean edges and no white artifacts.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "celtic_knot_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/celtic_knot_complete_98fac7de-a394-4359-bbeb-758a4539f529.webp", "type": "image", "prompt": "Modify image 1 to remove all white borders, white edges, and transparency artifacts. Transform the background into a solid, seamless dark black to eliminate any white fringing or halos. Ensure the Art Nouveau stained-glass Celtic knot artwork extends fully to the edges with no gaps. Maintain high fidelity to the original jewel tones and leaded glass patterns while ensuring the final image is a clean, square-formatted panel suitable for a mobile puzzle game.", "description": "A refined Art Nouveau stained-glass Celtic knot mandala with all white borders and artifacts removed for a clean, professional finish.", "subtype": "object", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "dragon_flight_complete": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/dragon_flight_complete_08ae6fa7-65b9-4e9b-a791-dd196db2a1db.webp", "type": "image", "prompt": "Refine and fix the Art Nouveau stained-glass artwork of the dragon from image 1. Remove all white borders, white edges, and transparency artifacts. Ensure the stained-glass lead lines and vibrant glass colors extend fully to the edges of the frame. The background should be a solid, deep black to enhance the glowing stained-glass effect, creating a clean, professional finish with no visible white gaps or halos. Maintain the intricate dragon silhouette and starry night aesthetic in a portrait orientation.", "description": "A polished Art Nouveau stained-glass artwork of a dragon soaring through a starry night sky, free of edge artifacts.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Fragmented Memories</title>\n    <link href=\"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Inter:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n    <style>\n        :root {\n            --gold: #FFD60A;\n            --gold-dark: #B8860B;\n            --charcoal: #2B2D42;\n            --navy: #1A1B2E;\n            --white: #F8F9FA;\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --transition-slow: 500ms;\n            --ease-out: cubic-bezier(0.0, 0, 0.2, 1);\n            --ease-bounce: cubic-bezier(0.68, -0.6, 0.32, 1.6);\n        }\n\n        body, html {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            -ms-touch-action: none;\n            font-family: 'Inter', sans-serif;\n            background: linear-gradient(135deg, #1A1B2E 0%, #2B2D42 100%);\n        }\n\n        #game-container {\n            width: 100%;\n            height: 100%;\n            position: relative;\n            display: flex;\n            flex-direction: column;\n        }\n\n        /* Header Section */\n        .header {\n            height: 120px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            position: relative;\n            background: linear-gradient(180deg, rgba(26, 27, 46, 0.95) 0%, rgba(26, 27, 46, 0.7) 100%);\n            z-index: 10;\n            padding: 12px 0;\n        }\n\n        .level-select-btn {\n            position: absolute;\n            left: 12px;\n            top: 50%;\n            transform: translateY(-50%);\n            background: rgba(255, 214, 10, 0.15);\n            border: 2px solid var(--gold);\n            border-radius: 50%;\n            width: 56px;\n            height: 56px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            color: var(--gold);\n            font-size: 28px;\n            transition: all var(--transition-fast) var(--ease-out);\n        }\n\n        .level-select-btn:hover, .level-select-btn:active {\n            background: rgba(255, 214, 10, 0.3);\n            transform: translateY(-50%) scale(1.05);\n        }\n\n        .preview-image {\n            height: 96px;\n            max-width: 96px;\n            border-radius: 8px;\n            border: 3px solid var(--gold);\n            box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);\n            object-fit: cover;\n        }\n\n        .level-indicator {\n            position: absolute;\n            right: 12px;\n            top: 50%;\n            transform: translateY(-50%);\n            font-size: 16px;\n            font-weight: 500;\n            color: var(--white);\n            opacity: 0.9;\n            background: rgba(255, 214, 10, 0.1);\n            padding: 8px 12px;\n            border-radius: 16px;\n            border: 1px solid rgba(255, 214, 10, 0.3);\n        }\n\n        /* Puzzle Area */\n        .puzzle-area {\n            flex: 1;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            position: relative;\n            padding: 16px;\n            box-sizing: border-box;\n        }\n\n        #puzzle-canvas {\n            border-radius: 8px;\n            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);\n        }\n\n        /* Fragment Tray */\n        .tray-area {\n            height: 320px;\n            background: var(--charcoal);\n            position: relative;\n            overflow: hidden;\n            border-top: 3px solid var(--gold);\n        }\n\n        .tray-label {\n            position: absolute;\n            top: 10px;\n            left: 50%;\n            transform: translateX(-50%);\n            font-family: 'Playfair Display', serif;\n            font-size: 18px;\n            color: var(--gold);\n            opacity: 0.9;\n            z-index: 5;\n            text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);\n            letter-spacing: 0.3px;\n        }\n\n        #tray-canvas {\n            width: 100%;\n            height: 100%;\n        }\n\n        /* Level Selection Panel */\n        .level-select-panel {\n            position: absolute;\n            top: 120px;\n            left: 0;\n            width: 100%;\n            max-height: 0;\n            overflow: hidden;\n            background: linear-gradient(135deg, #1A1B2E 0%, #2B2D42 100%);\n            z-index: 100;\n            transition: max-height var(--transition-normal) var(--ease-out);\n            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);\n        }\n\n        .level-select-panel.open {\n            max-height: 600px;\n            overflow-y: auto;\n        }\n\n        .level-select-content {\n            padding: 20px;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n        }\n\n        .level-select-panel h2 {\n            font-family: 'Playfair Display', serif;\n            font-size: 28px;\n            font-weight: 600;\n            color: var(--gold);\n            margin-bottom: 20px;\n            text-align: center;\n        }\n\n\n        .level-cards {\n            display: flex;\n            flex-direction: column;\n            gap: 24px;\n            width: 100%;\n            max-width: 400px;\n        }\n\n        .level-card {\n            background: rgba(255, 255, 255, 0.05);\n            border: 2px solid rgba(255, 214, 10, 0.3);\n            border-radius: 16px;\n            padding: 28px 24px;\n            cursor: pointer;\n            transition: all var(--transition-normal) var(--ease-out);\n            position: relative;\n            overflow: hidden;\n            min-height: 96px;\n            display: flex;\n            flex-direction: column;\n            justify-content: center;\n        }\n\n        .level-card:hover:not(.locked), .level-card:active:not(.locked) {\n            background: rgba(255, 214, 10, 0.12);\n            border-color: var(--gold);\n            transform: translateY(-2px);\n            box-shadow: 0 8px 24px rgba(255, 214, 10, 0.25);\n        }\n\n        .level-card.locked {\n            opacity: 0.5;\n            cursor: not-allowed;\n        }\n\n        .level-card.completed {\n            border-color: #4CAF50;\n        }\n\n        .level-card .level-name {\n            font-family: 'Playfair Display', serif;\n            font-size: 26px;\n            font-weight: 600;\n            color: var(--gold);\n            margin-bottom: 8px;\n        }\n\n        .level-card .level-info {\n            font-size: 16px;\n            color: var(--white);\n            opacity: 0.8;\n        }\n\n        .level-card .level-status {\n            position: absolute;\n            top: 16px;\n            right: 16px;\n            font-size: 24px;\n        }\n\n        .level-card .lock-icon {\n            position: absolute;\n            top: 50%;\n            right: 24px;\n            transform: translateY(-50%);\n            font-size: 32px;\n            opacity: 0.5;\n        }\n\n        /* Completion Overlay */\n        .completion-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(26, 27, 46, 0.95);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 200;\n            opacity: 0;\n            pointer-events: none;\n            transition: opacity var(--transition-slow) var(--ease-out);\n        }\n\n        .completion-overlay.visible {\n            opacity: 1;\n            pointer-events: auto;\n        }\n\n        .completion-overlay h2 {\n            font-family: 'Playfair Display', serif;\n            font-size: 44px;\n            font-weight: 700;\n            color: var(--gold);\n            text-shadow: 0 4px 20px rgba(255, 214, 10, 0.5);\n            margin-bottom: 16px;\n            text-align: center;\n            animation: pulse 2s ease-in-out infinite;\n        }\n\n        .completion-overlay .message {\n            font-size: 20px;\n            color: var(--white);\n            opacity: 0.85;\n            margin-bottom: 48px;\n            text-align: center;\n            padding: 0 24px;\n        }\n\n        .completion-overlay .btn {\n            background: linear-gradient(135deg, var(--gold) 0%, var(--gold-dark) 100%);\n            border: none;\n            border-radius: 30px;\n            padding: 18px 56px;\n            font-family: 'Playfair Display', serif;\n            font-size: 22px;\n            font-weight: 600;\n            color: var(--navy);\n            cursor: pointer;\n            transition: all var(--transition-fast) var(--ease-out);\n            margin: 10px;\n            min-height: 60px;\n            min-width: 200px;\n        }\n\n        .completion-overlay .btn:hover, .completion-overlay .btn:active {\n            transform: scale(1.03);\n            box-shadow: 0 8px 30px rgba(255, 214, 10, 0.4);\n        }\n\n        .completion-overlay .btn.secondary {\n            background: rgba(255, 214, 10, 0.1);\n            border: 2px solid var(--gold);\n            color: var(--gold);\n        }\n\n        .completion-overlay .btn.secondary:hover, .completion-overlay .btn.secondary:active {\n            background: rgba(255, 214, 10, 0.2);\n        }\n\n        /* Particles Canvas */\n        #particles-canvas {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n            z-index: 150;\n        }\n\n        /* Loading Bar */\n        .loading-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: linear-gradient(135deg, #1A1B2E 0%, #2B2D42 100%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 300;\n            opacity: 1;\n            transition: opacity var(--transition-normal) var(--ease-out);\n        }\n\n        .loading-overlay.hidden {\n            opacity: 0;\n            pointer-events: none;\n        }\n\n        .loading-title {\n            font-family: 'Playfair Display', serif;\n            font-size: 32px;\n            font-weight: 600;\n            color: var(--gold);\n            margin-bottom: 24px;\n            text-shadow: 0 2px 12px rgba(255, 214, 10, 0.4);\n        }\n\n        .loading-bar-container {\n            width: 280px;\n            height: 8px;\n            background: rgba(255, 255, 255, 0.1);\n            border-radius: 4px;\n            overflow: hidden;\n            border: 1px solid rgba(255, 214, 10, 0.3);\n        }\n\n        .loading-bar-fill {\n            height: 100%;\n            background: linear-gradient(90deg, var(--gold) 0%, var(--gold-dark) 100%);\n            width: 0%;\n            transition: width 0.3s ease-out;\n            box-shadow: 0 0 10px rgba(255, 214, 10, 0.5);\n        }\n\n        .loading-text {\n            margin-top: 12px;\n            font-size: 14px;\n            color: var(--white);\n            opacity: 0.7;\n        }\n\n        /* Edit Mode Indicator */\n        .edit-indicator {\n            position: absolute;\n            top: 130px;\n            left: 50%;\n            transform: translateX(-50%);\n            background: rgba(255, 214, 10, 0.2);\n            border: 1px solid var(--gold);\n            border-radius: 20px;\n            padding: 8px 20px;\n            font-size: 14px;\n            color: var(--gold);\n            z-index: 50;\n            display: none;\n        }\n\n        .edit-indicator.visible {\n            display: block;\n        }\n\n        @keyframes pulse {\n            0%, 100% { transform: scale(1); }\n            50% { transform: scale(1.05); }\n        }\n\n        @keyframes sparkle {\n            0% { opacity: 0; transform: scale(0) rotate(0deg); }\n            50% { opacity: 1; transform: scale(1) rotate(180deg); }\n            100% { opacity: 0; transform: scale(0) rotate(360deg); }\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-container\">\n        <!-- Game Screen -->\n        <div class=\"header\" id=\"game-header\">\n            <button class=\"level-select-btn\" id=\"level-select-btn\">\u2630</button>\n            <img id=\"preview-image\" class=\"preview-image\" src=\"\" alt=\"Preview\">\n            <span class=\"level-indicator\" id=\"level-indicator\">0 / 12</span>\n        </div>\n\n        <div class=\"level-select-panel\" id=\"level-select-panel\">\n            <div class=\"level-select-content\">\n                <h2>Select a Puzzle</h2>\n                <div class=\"level-cards\" id=\"level-cards\"></div>\n            </div>\n        </div>\n\n        <div class=\"puzzle-area\" id=\"puzzle-area\">\n            <canvas id=\"puzzle-canvas\"></canvas>\n        </div>\n\n        <div class=\"tray-area\" id=\"tray-area\">\n            <span class=\"tray-label\">Drag fragments to the puzzle</span>\n            <canvas id=\"tray-canvas\"></canvas>\n        </div>\n\n        <div class=\"edit-indicator\" id=\"edit-indicator\">Edit Mode - Drag to reposition</div>\n\n        <!-- Loading Overlay -->\n        <div class=\"loading-overlay\" id=\"loading-overlay\">\n            <div class=\"loading-title\">Fragmented Memories</div>\n            <div class=\"loading-bar-container\">\n                <div class=\"loading-bar-fill\" id=\"loading-bar-fill\"></div>\n            </div>\n            <div class=\"loading-text\" id=\"loading-text\">Loading...</div>\n        </div>\n\n        <!-- Completion Overlay -->\n        <div class=\"completion-overlay\" id=\"completion-overlay\">\n            <h2 id=\"completion-title\">Masterpiece Complete!</h2>\n            <p class=\"message\" id=\"completion-message\">You've revealed the Sunset Garden</p>\n            <button class=\"btn\" id=\"next-puzzle-btn\">Next Puzzle</button>\n            <button class=\"btn secondary\" id=\"back-to-menu-btn\">Back to Menu</button>\n        </div>\n\n        <!-- Particles Canvas -->\n        <canvas id=\"particles-canvas\"></canvas>\n    </div>\n\n    <script>\n        /* ==================================================\n         * GAME OVERVIEW: Fragmented Memories - A relaxing stained-glass puzzle game\n         * where players drag scattered glass fragments from a bottom tray and drop\n         * them into correct positions on a grid to reveal beautiful artworks.\n         * \n         * GAME STATE SHAPE: window.gameConfig = {\n         *   settings: {\n         *     gridBorderColor: string (hex),\n         *     gridBorderOpacity: number (0-100),\n         *     backgroundTint: string (hex),\n         *     fragmentGlowColor: string (hex),\n         *     soundVolume: number (0-100),\n         *     showGuideImage: boolean,\n         *     snapDistance: string ('tight'|'medium'|'loose')\n         *   },\n         *   progress: {\n         *     unlockedLevels: number[],\n         *     completedLevels: number[]\n         *   }\n         * }\n         * ==================================================\n         */\n\n        // Puzzle definitions\n        const PUZZLES = [\n            {\n                id: 0,\n                name: 'Sunset Garden',\n                assetId: 'sunset_garden_complete',\n                gridCols: 2,\n                gridRows: 2,\n                pieces: 4,\n                difficulty: 'Beginner'\n            },\n            {\n                id: 1,\n                name: 'Ocean Depths',\n                assetId: 'ocean_depths_complete',\n                gridCols: 3,\n                gridRows: 2,\n                pieces: 6,\n                difficulty: 'Beginner'\n            },\n            {\n                id: 2,\n                name: 'Cosmic Mandala',\n                assetId: 'cosmic_mandala_complete',\n                gridCols: 4,\n                gridRows: 2,\n                pieces: 8,\n                difficulty: 'Beginner'\n            },\n            {\n                id: 3,\n                name: 'Butterfly Garden',\n                assetId: 'butterfly_garden_complete',\n                gridCols: 5,\n                gridRows: 2,\n                pieces: 10,\n                difficulty: 'Easy'\n            },\n            {\n                id: 4,\n                name: 'Aurora Mountains',\n                assetId: 'aurora_mountains_complete',\n                gridCols: 4,\n                gridRows: 3,\n                pieces: 12,\n                difficulty: 'Easy'\n            },\n            {\n                id: 5,\n                name: 'Phoenix Rising',\n                assetId: 'phoenix_rising_complete',\n                gridCols: 7,\n                gridRows: 2,\n                pieces: 14,\n                difficulty: 'Intermediate'\n            },\n            {\n                id: 6,\n                name: 'Cherry Blossoms',\n                assetId: 'cherry_blossoms_complete',\n                gridCols: 4,\n                gridRows: 4,\n                pieces: 16,\n                difficulty: 'Intermediate'\n            },\n            {\n                id: 7,\n                name: 'Egyptian Temple',\n                assetId: 'egyptian_temple_complete',\n                gridCols: 6,\n                gridRows: 3,\n                pieces: 18,\n                difficulty: 'Intermediate'\n            },\n            {\n                id: 8,\n                name: 'Celtic Knot',\n                assetId: 'celtic_knot_complete',\n                gridCols: 5,\n                gridRows: 4,\n                pieces: 20,\n                difficulty: 'Advanced'\n            },\n            {\n                id: 9,\n                name: 'Dragon\\'s Flight',\n                assetId: 'dragon_flight_complete',\n                gridCols: 4,\n                gridRows: 6,\n                pieces: 24,\n                difficulty: 'Advanced'\n            }\n        ];\n\n        // Asset cache\n        const assetCache = {\n            images: {},\n            audio: {}\n        };\n\n        // Audio context\n        let audioContext = null;\n\n        // Runtime state\n        let currentMode = 'play';\n        let currentPuzzleIndex = 0;\n        let fragments = [];\n        let placedFragments = [];\n        let dragState = {\n            isDragging: false,\n            fragment: null,\n            offsetX: 0,\n            offsetY: 0,\n            touchId: null\n        };\n        let highlightedCell = null;\n        let particles = [];\n        let animationFrameId = null;\n\n        // Canvas references\n        let puzzleCanvas, puzzleCtx;\n        let trayCanvas, trayCtx;\n        let particlesCanvas, particlesCtx;\n\n        // Layout calculations\n        let puzzleGridRect = { x: 0, y: 0, width: 0, height: 0, cellWidth: 0, cellHeight: 0 };\n        let trayRect = { x: 0, y: 0, width: 0, height: 0 };\n\n        // Default config\n        function getDefaultConfig() {\n            return {\n                settings: {\n                    gridBorderColor: '#FFD60A',\n                    gridBorderOpacity: 30,\n                    backgroundTint: '#1A1B2E',\n                    fragmentGlowColor: '#FFD60A',\n                    soundVolume: 70,\n                    snapDistance: 'medium'\n                },\n                progress: {\n                    unlockedLevels: [0],\n                    completedLevels: []\n                }\n            };\n        }\n\n        // Initialize config\n        function initConfig() {\n            const defaults = getDefaultConfig();\n            if (!window.gameConfig) {\n                Object.assign(window.gameConfig || {}, defaults);\n            } else {\n                // Merge with defaults\n                if (!window.gameConfig.settings) window.gameConfig.settings = {};\n                if (!window.gameConfig.progress) window.gameConfig.progress = {};\n                \n                for (const key in defaults.settings) {\n                    if (window.gameConfig.settings[key] === undefined) {\n                        window.gameConfig.settings[key] = defaults.settings[key];\n                    }\n                }\n                for (const key in defaults.progress) {\n                    if (window.gameConfig.progress[key] === undefined) {\n                        window.gameConfig.progress[key] = defaults.progress[key];\n                    }\n                }\n            }\n        }\n\n        // Get snap distance in pixels\n        function getSnapDistance() {\n            const distances = { tight: 40, medium: 60, loose: 80 };\n            return distances[window.gameConfig.settings.snapDistance] || 60;\n        }\n\n        // Update loading bar\n        function updateLoadingBar(progress, text = 'Loading...') {\n            const fill = document.getElementById('loading-bar-fill');\n            const loadingText = document.getElementById('loading-text');\n            if (fill) fill.style.width = `${progress * 100}%`;\n            if (loadingText) loadingText.textContent = text;\n        }\n\n        // Load a single image asset\n        function loadImage(id) {\n            return new Promise((resolve) => {\n                const assetInfo = lib.getAsset(id);\n                if (assetInfo && assetInfo.url) {\n                    const img = new Image();\n                    img.crossOrigin = 'anonymous';\n                    img.onload = () => {\n                        assetCache.images[id] = img;\n                        resolve();\n                    };\n                    img.onerror = () => {\n                        lib.log('Failed to load image: ' + id);\n                        resolve();\n                    };\n                    img.src = assetInfo.url;\n                } else {\n                    resolve();\n                }\n            });\n        }\n\n        // Load a single audio asset\n        function loadAudio(id) {\n            return new Promise((resolve) => {\n                const assetInfo = lib.getAsset(id);\n                if (assetInfo && assetInfo.url && audioContext) {\n                    fetch(assetInfo.url)\n                        .then(response => response.arrayBuffer())\n                        .then(buffer => audioContext.decodeAudioData(buffer))\n                        .then(audioBuffer => {\n                            assetCache.audio[id] = {\n                                buffer: audioBuffer,\n                                loop: assetInfo.loop === 'true' || assetInfo.loop === true\n                            };\n                            resolve();\n                        })\n                        .catch(e => {\n                            lib.log('Failed to load audio: ' + id + ' - ' + e.message);\n                            resolve();\n                        });\n                } else {\n                    resolve();\n                }\n            });\n        }\n\n        // Preload essential assets for level 1\n        // Preload essential assets for level 1\n        async function preloadLevel1Assets() {\n            // Initialize audio context\n            try {\n                audioContext = new (window.AudioContext || window.webkitAudioContext)();\n            } catch (e) {\n                lib.log('Audio context not available: ' + e.message);\n            }\n\n            // Essential images for first level - BLOCK on these\n            const level1Images = ['sunset_garden_complete', 'tray_background_texture'];\n            \n            // Audio - load in BACKGROUND without blocking\n            const audioIds = ['fragment_pickup_sound', 'fragment_snap_sound', 'fragment_return_sound', 'puzzle_complete_sound', 'ambient_music'];\n            \n            const totalImages = level1Images.length;\n            let loadedCount = 0;\n\n            // Load images and wait for them\n            for (const id of level1Images) {\n                await loadImage(id);\n                loadedCount++;\n                updateLoadingBar(loadedCount / totalImages, `Loading images (${loadedCount}/${totalImages})...`);\n            }\n\n            // Start loading audio in background (don't await)\n            audioIds.forEach(id => {\n                loadAudio(id).then(() => {\n                    lib.log('Background loaded audio: ' + id);\n                });\n            });\n\n            lib.log('Level 1 images loaded, audio loading in background');\n        }\n\n        // Lazy load remaining level assets in background\n        async function lazyLoadRemainingAssets() {\n            const remainingImages = [\n                'ocean_depths_complete',\n                'cosmic_mandala_complete', \n                'butterfly_garden_complete', \n                'aurora_mountains_complete', \n                'phoenix_rising_complete', \n                'cherry_blossoms_complete', \n                'egyptian_temple_complete', \n                'celtic_knot_complete', \n                'dragon_flight_complete'\n            ];\n\n            lib.log('Starting lazy load of remaining assets...');\n            \n            for (const id of remainingImages) {\n                await loadImage(id);\n            }\n\n            lib.log('All assets loaded');\n        }\n\n        // Play sound\n        function playSound(id, volume = 1) {\n            if (!audioContext || !assetCache.audio[id]) return null;\n            \n            const soundData = assetCache.audio[id];\n            const source = audioContext.createBufferSource();\n            source.buffer = soundData.buffer;\n            \n            const gainNode = audioContext.createGain();\n            const masterVolume = (window.gameConfig.settings.soundVolume / 100) * volume;\n            gainNode.gain.value = masterVolume;\n            \n            source.connect(gainNode);\n            gainNode.connect(audioContext.destination);\n            \n            if (soundData.loop) {\n                source.loop = true;\n            }\n            \n            source.start(0);\n            return { source, gainNode };\n        }\n\n        // Resume audio context (needed for user interaction)\n        function resumeAudio() {\n            if (audioContext && audioContext.state === 'suspended') {\n                audioContext.resume();\n            }\n        }\n\n        // Generate fragments for current puzzle\n        function generateFragments(puzzle) {\n            fragments = [];\n            placedFragments = [];\n            \n            const img = assetCache.images[puzzle.assetId];\n            if (!img) {\n                lib.log('Puzzle image not loaded: ' + puzzle.assetId);\n                return;\n            }\n\n            const pieceWidth = img.width / puzzle.gridCols;\n            const pieceHeight = img.height / puzzle.gridRows;\n\n            for (let row = 0; row < puzzle.gridRows; row++) {\n                for (let col = 0; col < puzzle.gridCols; col++) {\n                    const fragment = {\n                        id: row * puzzle.gridCols + col,\n                        gridX: col,\n                        gridY: row,\n                        sourceX: col * pieceWidth,\n                        sourceY: row * pieceHeight,\n                        sourceWidth: pieceWidth,\n                        sourceHeight: pieceHeight,\n                        placed: false,\n                        // Position in tray (will be set later)\n                        x: 0,\n                        y: 0,\n                        width: 0,\n                        height: 0,\n                        rotation: (Math.random() - 0.5) * 10 // Slight random rotation\n                    };\n                    fragments.push(fragment);\n                }\n            }\n\n            // Shuffle fragments\n            for (let i = fragments.length - 1; i > 0; i--) {\n                const j = Math.floor(Math.random() * (i + 1));\n                [fragments[i], fragments[j]] = [fragments[j], fragments[i]];\n            }\n\n            // Position fragments in tray\n            positionFragmentsInTray();\n        }\n\n        // Position fragments in tray\n        // Position fragments in tray\n                function positionFragmentsInTray() {\n            const puzzle = PUZZLES[currentPuzzleIndex];\n            const unplacedFragments = fragments.filter(f => !f.placed);\n            \n            if (unplacedFragments.length === 0) return;\n\n            const trayPadding = 20;\n            \n            // Make fragments bigger than the grid cells (1.4x scale)\n            const fragWidth = puzzleGridRect.cellWidth * 1.4;\n            const fragHeight = puzzleGridRect.cellHeight * 1.4;\n\n            // Create a messy pile - very loose arrangement with lots of random variation\n            const availableWidth = trayRect.width - trayPadding * 2 - fragWidth;\n            const availableHeight = trayRect.height - 60 - fragHeight; // Leave space for label at top\n            \n            unplacedFragments.forEach((frag, index) => {\n                frag.width = fragWidth;\n                frag.height = fragHeight;\n                \n                // Completely random placement within the tray area for a messy pile effect\n                const randomX = Math.random() * availableWidth + trayPadding;\n                const randomY = Math.random() * availableHeight + 60; // Start below label\n                \n                frag.x = randomX;\n                frag.y = randomY;\n                \n                // More varied rotation for a natural scattered look\n                frag.rotation = (Math.random() - 0.5) * 30; // -15 to +15 degrees\n            });\n        }\n\n        // Calculate layout\n        function calculateLayout() {\n            const puzzle = PUZZLES[currentPuzzleIndex];\n            const puzzleArea = document.getElementById('puzzle-area');\n            const trayArea = document.getElementById('tray-area');\n            \n            // Puzzle canvas sizing\n            const areaWidth = puzzleArea.clientWidth - 40;\n            const areaHeight = puzzleArea.clientHeight - 40;\n            \n            const img = assetCache.images[puzzle.assetId];\n            let gridWidth, gridHeight;\n            \n            if (img) {\n                const imgAspect = img.width / img.height;\n                if (areaWidth / areaHeight > imgAspect) {\n                    gridHeight = Math.min(areaHeight, 700);\n                    gridWidth = gridHeight * imgAspect;\n                } else {\n                    gridWidth = Math.min(areaWidth, 600);\n                    gridHeight = gridWidth / imgAspect;\n                }\n            } else {\n                gridWidth = Math.min(areaWidth, 600);\n                gridHeight = gridWidth * (puzzle.gridRows / puzzle.gridCols);\n            }\n\n            puzzleCanvas.width = gridWidth;\n            puzzleCanvas.height = gridHeight;\n            \n            puzzleGridRect = {\n                x: 0,\n                y: 0,\n                width: gridWidth,\n                height: gridHeight,\n                cellWidth: gridWidth / puzzle.gridCols,\n                cellHeight: gridHeight / puzzle.gridRows\n            };\n\n            // Tray canvas sizing\n            trayCanvas.width = trayArea.clientWidth;\n            trayCanvas.height = trayArea.clientHeight;\n            \n            trayRect = {\n                x: 0,\n                y: 0,\n                width: trayCanvas.width,\n                height: trayCanvas.height\n            };\n\n            // Particles canvas\n            particlesCanvas.width = 720;\n            particlesCanvas.height = 1280;\n\n            // Reposition fragments\n            positionFragmentsInTray();\n        }\n\n        // Draw puzzle grid\n        function drawPuzzleGrid() {\n            const puzzle = PUZZLES[currentPuzzleIndex];\n            const img = assetCache.images[puzzle.assetId];\n            const settings = window.gameConfig.settings;\n\n            // Clear canvas\n            puzzleCtx.fillStyle = settings.backgroundTint;\n            puzzleCtx.fillRect(0, 0, puzzleCanvas.width, puzzleCanvas.height);\n\n            // Guide image removed - now shown as small preview in header\n\n            // Draw grid lines\n            const borderOpacity = settings.gridBorderOpacity / 100;\n            puzzleCtx.strokeStyle = settings.gridBorderColor;\n            puzzleCtx.globalAlpha = borderOpacity;\n            puzzleCtx.lineWidth = 2;\n\n            for (let col = 0; col <= puzzle.gridCols; col++) {\n                const x = col * puzzleGridRect.cellWidth;\n                puzzleCtx.beginPath();\n                puzzleCtx.moveTo(x, 0);\n                puzzleCtx.lineTo(x, puzzleCanvas.height);\n                puzzleCtx.stroke();\n            }\n\n            for (let row = 0; row <= puzzle.gridRows; row++) {\n                const y = row * puzzleGridRect.cellHeight;\n                puzzleCtx.beginPath();\n                puzzleCtx.moveTo(0, y);\n                puzzleCtx.lineTo(puzzleCanvas.width, y);\n                puzzleCtx.stroke();\n            }\n\n            puzzleCtx.globalAlpha = 1;\n\n            // Draw placed fragments\n            placedFragments.forEach(frag => {\n                drawFragmentOnGrid(frag);\n            });\n\n            // Draw highlight for potential drop zone\n            if (highlightedCell && dragState.isDragging) {\n                const cellX = highlightedCell.col * puzzleGridRect.cellWidth;\n                const cellY = highlightedCell.row * puzzleGridRect.cellHeight;\n                \n                puzzleCtx.fillStyle = settings.fragmentGlowColor;\n                puzzleCtx.globalAlpha = 0.3;\n                puzzleCtx.fillRect(cellX, cellY, puzzleGridRect.cellWidth, puzzleGridRect.cellHeight);\n                \n                // Glow effect\n                puzzleCtx.shadowColor = settings.fragmentGlowColor;\n                puzzleCtx.shadowBlur = 20;\n                puzzleCtx.strokeStyle = settings.fragmentGlowColor;\n                puzzleCtx.lineWidth = 3;\n                puzzleCtx.strokeRect(cellX + 2, cellY + 2, puzzleGridRect.cellWidth - 4, puzzleGridRect.cellHeight - 4);\n                puzzleCtx.shadowBlur = 0;\n                puzzleCtx.globalAlpha = 1;\n            }\n        }\n\n        // Draw fragment on grid\n        function drawFragmentOnGrid(frag) {\n            const puzzle = PUZZLES[currentPuzzleIndex];\n            const img = assetCache.images[puzzle.assetId];\n            if (!img) return;\n\n            const destX = frag.gridX * puzzleGridRect.cellWidth;\n            const destY = frag.gridY * puzzleGridRect.cellHeight;\n\n            puzzleCtx.drawImage(\n                img,\n                frag.sourceX, frag.sourceY, frag.sourceWidth, frag.sourceHeight,\n                destX, destY, puzzleGridRect.cellWidth, puzzleGridRect.cellHeight\n            );\n        }\n\n        // Draw tray\n        // Draw tray\n        function drawTray() {\n            const puzzle = PUZZLES[currentPuzzleIndex];\n            const img = assetCache.images[puzzle.assetId];\n\n            // Clear with solid base color\n            trayCtx.fillStyle = '#2B2D42';\n            trayCtx.fillRect(0, 0, trayCanvas.width, trayCanvas.height);\n\n            // Add a subtle top-to-bottom gradient for depth\n            const gradient = trayCtx.createLinearGradient(0, 0, 0, trayCanvas.height);\n            gradient.addColorStop(0, 'rgba(43, 45, 66, 0.95)');\n            gradient.addColorStop(0.3, 'rgba(43, 45, 66, 1)');\n            gradient.addColorStop(1, 'rgba(43, 45, 66, 1)');\n            trayCtx.fillStyle = gradient;\n            trayCtx.fillRect(0, 0, trayCanvas.width, trayCanvas.height);\n\n            // Draw unplaced fragments\n            const unplacedFragments = fragments.filter(f => !f.placed && f !== dragState.fragment);\n            \n            unplacedFragments.forEach(frag => {\n                if (!img) return;\n                \n                trayCtx.save();\n                \n                const centerX = frag.x + frag.width / 2;\n                const centerY = frag.y + frag.height / 2;\n                \n                trayCtx.translate(centerX, centerY);\n                trayCtx.rotate(frag.rotation * Math.PI / 180);\n                \n                // Shadow\n                trayCtx.shadowColor = 'rgba(0, 0, 0, 0.5)';\n                trayCtx.shadowBlur = 10;\n                trayCtx.shadowOffsetY = 5;\n                \n                trayCtx.drawImage(\n                    img,\n                    frag.sourceX, frag.sourceY, frag.sourceWidth, frag.sourceHeight,\n                    -frag.width / 2, -frag.height / 2, frag.width, frag.height\n                );\n                \n                // Border\n                trayCtx.strokeStyle = 'rgba(255, 214, 10, 0.3)';\n                trayCtx.lineWidth = 2;\n                trayCtx.strokeRect(-frag.width / 2, -frag.height / 2, frag.width, frag.height);\n                \n                trayCtx.restore();\n            });\n        }\n\n        // Draw dragged fragment\n        function drawDraggedFragment() {\n            if (!dragState.isDragging || !dragState.fragment) return;\n\n            const puzzle = PUZZLES[currentPuzzleIndex];\n            const img = assetCache.images[puzzle.assetId];\n            if (!img) return;\n\n            const frag = dragState.fragment;\n            \n            // Draw at the final grid cell size\n            const drawWidth = puzzleGridRect.cellWidth;\n            const drawHeight = puzzleGridRect.cellHeight;\n            \n            // Determine which canvas to draw on based on position\n            const puzzleRect = puzzleCanvas.getBoundingClientRect();\n            const trayRectBounds = trayCanvas.getBoundingClientRect();\n            \n            // Draw on both canvases for smooth transition\n            const drawOnCanvas = (ctx, offsetX, offsetY) => {\n                ctx.save();\n                \n                const drawX = dragState.currentX - offsetX;\n                const drawY = dragState.currentY - offsetY;\n                \n                ctx.shadowColor = 'rgba(255, 214, 10, 0.5)';\n                ctx.shadowBlur = 20;\n                \n                ctx.drawImage(\n                    img,\n                    frag.sourceX, frag.sourceY, frag.sourceWidth, frag.sourceHeight,\n                    drawX - drawWidth / 2, drawY - drawHeight / 2, drawWidth, drawHeight\n                );\n                \n                ctx.strokeStyle = window.gameConfig.settings.fragmentGlowColor;\n                ctx.lineWidth = 3;\n                ctx.strokeRect(drawX - drawWidth / 2, drawY - drawHeight / 2, drawWidth, drawHeight);\n                \n                ctx.restore();\n            };\n\n            // Draw on puzzle canvas\n            drawOnCanvas(puzzleCtx, puzzleRect.left, puzzleRect.top);\n            \n            // Draw on tray canvas\n            drawOnCanvas(trayCtx, trayRectBounds.left, trayRectBounds.top);\n        }\n\n        // Check if point is in fragment\n        function getFragmentAtPoint(x, y) {\n            const unplacedFragments = fragments.filter(f => !f.placed);\n            \n            for (let i = unplacedFragments.length - 1; i >= 0; i--) {\n                const frag = unplacedFragments[i];\n                if (x >= frag.x && x <= frag.x + frag.width &&\n                    y >= frag.y && y <= frag.y + frag.height) {\n                    return frag;\n                }\n            }\n            return null;\n        }\n\n        // Get grid cell at point\n        function getGridCellAtPoint(x, y) {\n            const puzzle = PUZZLES[currentPuzzleIndex];\n            const puzzleRect = puzzleCanvas.getBoundingClientRect();\n            \n            const localX = x - puzzleRect.left;\n            const localY = y - puzzleRect.top;\n            \n            if (localX < 0 || localX > puzzleCanvas.width || localY < 0 || localY > puzzleCanvas.height) {\n                return null;\n            }\n            \n            const col = Math.floor(localX / puzzleGridRect.cellWidth);\n            const row = Math.floor(localY / puzzleGridRect.cellHeight);\n            \n            if (col >= 0 && col < puzzle.gridCols && row >= 0 && row < puzzle.gridRows) {\n                return { col, row };\n            }\n            return null;\n        }\n\n        // Check if cell is already occupied\n        function isCellOccupied(col, row) {\n            return placedFragments.some(f => f.gridX === col && f.gridY === row);\n        }\n\n        // Handle drag start\n        function handleDragStart(x, y, touchId = null) {\n            resumeAudio();\n            \n            const trayRect = trayCanvas.getBoundingClientRect();\n            const localX = x - trayRect.left;\n            const localY = y - trayRect.top;\n            \n            const fragment = getFragmentAtPoint(localX, localY);\n            \n            if (fragment) {\n                dragState.isDragging = true;\n                dragState.fragment = fragment;\n                dragState.touchId = touchId;\n                dragState.currentX = x;\n                dragState.currentY = y;\n                \n                playSound('fragment_pickup_sound');\n            }\n        }\n\n        // Handle drag move\n        function handleDragMove(x, y) {\n            if (!dragState.isDragging) return;\n            \n            dragState.currentX = x;\n            dragState.currentY = y;\n            \n            // Check for highlight\n            const cell = getGridCellAtPoint(x, y);\n            const snapDist = getSnapDistance();\n            \n            if (cell && !isCellOccupied(cell.col, cell.row)) {\n                const puzzleRect = puzzleCanvas.getBoundingClientRect();\n                const cellCenterX = puzzleRect.left + (cell.col + 0.5) * puzzleGridRect.cellWidth;\n                const cellCenterY = puzzleRect.top + (cell.row + 0.5) * puzzleGridRect.cellHeight;\n                \n                const dist = Math.sqrt(Math.pow(x - cellCenterX, 2) + Math.pow(y - cellCenterY, 2));\n                \n                if (dist < snapDist && cell.col === dragState.fragment.gridX && cell.row === dragState.fragment.gridY) {\n                    highlightedCell = cell;\n                } else {\n                    highlightedCell = null;\n                }\n            } else {\n                highlightedCell = null;\n            }\n        }\n\n        // Handle drag end\n        function handleDragEnd() {\n            if (!dragState.isDragging) return;\n            \n            const frag = dragState.fragment;\n            \n            if (highlightedCell && highlightedCell.col === frag.gridX && highlightedCell.row === frag.gridY) {\n                // Correct placement!\n                frag.placed = true;\n                placedFragments.push(frag);\n                \n                playSound('fragment_snap_sound');\n                createSnapParticles(highlightedCell.col, highlightedCell.row);\n                \n                // Update progress indicator\n                updateProgressIndicator();\n                \n                // Check for completion\n                if (placedFragments.length === fragments.length) {\n                    setTimeout(() => handlePuzzleComplete(), 500);\n                }\n            } else {\n                // Return to tray\n                playSound('fragment_return_sound');\n            }\n            \n            // Reset drag state\n            dragState.isDragging = false;\n            dragState.fragment = null;\n            dragState.touchId = null;\n            highlightedCell = null;\n            \n            // Don't reposition - let pieces stay where they are for a natural pile\n        }\n\n        // Create snap particles\n        function createSnapParticles(col, row) {\n            const puzzleRect = puzzleCanvas.getBoundingClientRect();\n            const centerX = puzzleRect.left + (col + 0.5) * puzzleGridRect.cellWidth;\n            const centerY = puzzleRect.top + (row + 0.5) * puzzleGridRect.cellHeight;\n            \n            for (let i = 0; i < 15; i++) {\n                particles.push({\n                    x: centerX,\n                    y: centerY,\n                    vx: (Math.random() - 0.5) * 8,\n                    vy: (Math.random() - 0.5) * 8,\n                    life: 1,\n                    decay: 0.02 + Math.random() * 0.02,\n                    size: 3 + Math.random() * 5,\n                    color: window.gameConfig.settings.fragmentGlowColor\n                });\n            }\n        }\n\n        // Update particles\n        function updateParticles(dt) {\n            particles = particles.filter(p => {\n                p.x += p.vx;\n                p.y += p.vy;\n                p.life -= p.decay;\n                return p.life > 0;\n            });\n        }\n\n        // Draw particles\n        function drawParticles() {\n            particlesCtx.clearRect(0, 0, particlesCanvas.width, particlesCanvas.height);\n            \n            particles.forEach(p => {\n                particlesCtx.beginPath();\n                particlesCtx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);\n                particlesCtx.fillStyle = p.color;\n                particlesCtx.globalAlpha = p.life;\n                particlesCtx.fill();\n            });\n            \n            particlesCtx.globalAlpha = 1;\n        }\n\n        // Update progress indicator\n        function updateProgressIndicator() {\n            const indicator = document.getElementById('level-indicator');\n            indicator.textContent = `${placedFragments.length} / ${fragments.length}`;\n        }\n\n        // Handle puzzle complete\n        function handlePuzzleComplete() {\n            playSound('puzzle_complete_sound');\n            \n            // Create celebration particles\n            for (let i = 0; i < 50; i++) {\n                setTimeout(() => {\n                    particles.push({\n                        x: Math.random() * 720,\n                        y: Math.random() * 800 + 100,\n                        vx: (Math.random() - 0.5) * 4,\n                        vy: -Math.random() * 3 - 1,\n                        life: 1,\n                        decay: 0.01,\n                        size: 5 + Math.random() * 8,\n                        color: ['#FFD60A', '#FF6B35', '#00B4D8', '#C77DFF'][Math.floor(Math.random() * 4)]\n                    });\n                }, i * 30);\n            }\n            \n            // Mark level as completed\n            if (!window.gameConfig.progress.completedLevels.includes(currentPuzzleIndex)) {\n                window.gameConfig.progress.completedLevels.push(currentPuzzleIndex);\n            }\n            \n            // Unlock next level\n            const nextLevel = currentPuzzleIndex + 1;\n            if (nextLevel < PUZZLES.length && !window.gameConfig.progress.unlockedLevels.includes(nextLevel)) {\n                window.gameConfig.progress.unlockedLevels.push(nextLevel);\n            }\n            \n            // Save progress\n            saveProgress();\n            \n            // Auto-advance to next puzzle or show final completion\n            setTimeout(() => {\n                if (currentPuzzleIndex < PUZZLES.length - 1) {\n                    // Automatically start next puzzle\n                    startPuzzle(currentPuzzleIndex + 1);\n                } else {\n                    // Show completion overlay for final puzzle\n                    const overlay = document.getElementById('completion-overlay');\n                    const title = document.getElementById('completion-title');\n                    const message = document.getElementById('completion-message');\n                    const nextBtn = document.getElementById('next-puzzle-btn');\n                    \n                    title.textContent = 'All Puzzles Complete!';\n                    message.textContent = 'You\\'ve mastered every beautiful stained-glass artwork!';\n                    nextBtn.textContent = 'View Gallery';\n                    nextBtn.style.display = 'inline-block';\n                    \n                    overlay.classList.add('visible');\n                }\n            }, 2000);\n        }\n\n        // Save progress\n        async function saveProgress() {\n            try {\n                await lib.saveUserGameState({\n                    completedLevels: window.gameConfig.progress.completedLevels,\n                    unlockedLevels: window.gameConfig.progress.unlockedLevels\n                });\n            } catch (e) {\n                lib.log('Failed to save progress: ' + e.message);\n            }\n        }\n\n        // Load progress\n        async function loadProgress() {\n            try {\n                const saved = await lib.getUserGameState();\n                if (saved && saved.state) {\n                    if (saved.state.completedLevels) {\n                        window.gameConfig.progress.completedLevels = saved.state.completedLevels;\n                    }\n                    if (saved.state.unlockedLevels) {\n                        window.gameConfig.progress.unlockedLevels = saved.state.unlockedLevels;\n                    }\n                }\n            } catch (e) {\n                lib.log('Failed to load progress: ' + e.message);\n            }\n        }\n\n        // Toggle level select panel\n        function toggleLevelSelect() {\n            const panel = document.getElementById('level-select-panel');\n            panel.classList.toggle('open');\n            \n            if (panel.classList.contains('open')) {\n                renderLevelCards();\n            }\n        }\n\n        // Close level select panel\n        function closeLevelSelect() {\n            document.getElementById('level-select-panel').classList.remove('open');\n        }\n\n        // Render level cards\n        function renderLevelCards() {\n            const container = document.getElementById('level-cards');\n            container.innerHTML = '';\n            \n            PUZZLES.forEach((puzzle, index) => {\n                // In edit mode, unlock all levels\n                const isUnlocked = currentMode === 'edit' || window.gameConfig.progress.unlockedLevels.includes(index);\n                const isCompleted = window.gameConfig.progress.completedLevels.includes(index);\n                \n                const card = document.createElement('div');\n                card.className = `level-card ${!isUnlocked ? 'locked' : ''} ${isCompleted ? 'completed' : ''}`;\n                \n                card.innerHTML = `\n                    <div class=\"level-name\">${puzzle.name}</div>\n                    <div class=\"level-info\">${puzzle.difficulty} \u2022 ${puzzle.pieces} pieces</div>\n                    ${isCompleted ? '<span class=\"level-status\">\u2713</span>' : ''}\n                    ${!isUnlocked ? '<span class=\"lock-icon\">\ud83d\udd12</span>' : ''}\n                `;\n                \n                if (isUnlocked) {\n                    card.addEventListener('click', () => {\n                        closeLevelSelect();\n                        startPuzzle(index);\n                    });\n                }\n                \n                container.appendChild(card);\n            });\n        }\n\n        // Start puzzle\n        function startPuzzle(index) {\n            currentPuzzleIndex = index;\n            \n            const puzzle = PUZZLES[index];\n            \n            // Update preview image in header\n            const previewImg = document.getElementById('preview-image');\n            const assetInfo = lib.getAsset(puzzle.assetId);\n            if (assetInfo && assetInfo.url) {\n                previewImg.src = assetInfo.url;\n                previewImg.alt = puzzle.name;\n            }\n            \n            calculateLayout();\n            generateFragments(puzzle);\n            updateProgressIndicator();\n        }\n\n        // Setup input handlers\n        function setupInputHandlers() {\n            const trayArea = document.getElementById('tray-area');\n            \n            // Touch events\n            trayArea.addEventListener('touchstart', (e) => {\n                e.preventDefault();\n                const touch = e.touches[0];\n                handleDragStart(touch.clientX, touch.clientY, touch.identifier);\n            }, { passive: false });\n            \n            document.addEventListener('touchmove', (e) => {\n                if (dragState.isDragging) {\n                    e.preventDefault();\n                    const touch = Array.from(e.touches).find(t => t.identifier === dragState.touchId);\n                    if (touch) {\n                        handleDragMove(touch.clientX, touch.clientY);\n                    }\n                }\n            }, { passive: false });\n            \n            document.addEventListener('touchend', (e) => {\n                if (dragState.isDragging) {\n                    const touch = Array.from(e.changedTouches).find(t => t.identifier === dragState.touchId);\n                    if (touch) {\n                        handleDragEnd();\n                    }\n                }\n            });\n            \n            // Mouse events\n            trayArea.addEventListener('mousedown', (e) => {\n                handleDragStart(e.clientX, e.clientY);\n            });\n            \n            document.addEventListener('mousemove', (e) => {\n                if (dragState.isDragging) {\n                    handleDragMove(e.clientX, e.clientY);\n                }\n            });\n            \n            document.addEventListener('mouseup', () => {\n                if (dragState.isDragging) {\n                    handleDragEnd();\n                }\n            });\n            \n            // Prevent context menu\n            document.addEventListener('contextmenu', (e) => e.preventDefault());\n            \n            // Level select button\n            document.getElementById('level-select-btn').addEventListener('click', toggleLevelSelect);\n            \n            // Completion overlay buttons\n            document.getElementById('next-puzzle-btn').addEventListener('click', () => {\n                document.getElementById('completion-overlay').classList.remove('visible');\n                if (currentPuzzleIndex < PUZZLES.length - 1) {\n                    startPuzzle(currentPuzzleIndex + 1);\n                } else {\n                    toggleLevelSelect();\n                }\n            });\n            \n            document.getElementById('back-to-menu-btn').addEventListener('click', () => {\n                document.getElementById('completion-overlay').classList.remove('visible');\n                toggleLevelSelect();\n            });\n            \n            // Window resize\n            window.addEventListener('resize', () => {\n                calculateLayout();\n            });\n        }\n\n        // Game loop\n        let lastTime = 0;\n        function gameLoop(timestamp) {\n            const dt = Math.min((timestamp - lastTime) / 1000, 0.11);\n            lastTime = timestamp;\n            \n            // Update\n            updateParticles(dt);\n            \n            // Draw\n            drawPuzzleGrid();\n            drawTray();\n            drawDraggedFragment();\n            drawParticles();\n            \n            animationFrameId = requestAnimationFrame(gameLoop);\n        }\n\n        // Setup edit mode\n        function setupEditMode() {\n            document.getElementById('edit-indicator').classList.add('visible');\n            \n            lib.showGameParameters({\n                name: 'Visual Settings',\n                params: {\n                    'Grid Border Color': {\n                        key: 'gameConfig.settings.gridBorderColor',\n                        type: 'color',\n                        onChange: (value) => {\n                            window.gameConfig.settings.gridBorderColor = value;\n                        }\n                    },\n                    'Grid Border Opacity': {\n                        key: 'gameConfig.settings.gridBorderOpacity',\n                        type: 'slider',\n                        min: 0,\n                        max: 100,\n                        step: 5,\n                        onChange: (value) => {\n                            window.gameConfig.settings.gridBorderOpacity = value;\n                        }\n                    },\n                    'Fragment Glow Color': {\n                        key: 'gameConfig.settings.fragmentGlowColor',\n                        type: 'color',\n                        onChange: (value) => {\n                            window.gameConfig.settings.fragmentGlowColor = value;\n                        }\n                    },\n\n                    'Sound Volume': {\n                        key: 'gameConfig.settings.soundVolume',\n                        type: 'slider',\n                        min: 0,\n                        max: 100,\n                        step: 10,\n                        onChange: (value) => {\n                            window.gameConfig.settings.soundVolume = value;\n                        }\n                    },\n                    'Snap Distance': {\n                        key: 'gameConfig.settings.snapDistance',\n                        type: 'dropdown',\n                        options: [\n                            { label: 'Tight', value: 'tight' },\n                            { label: 'Medium', value: 'medium' },\n                            { label: 'Loose', value: 'loose' }\n                        ],\n                        onChange: (value) => {\n                            window.gameConfig.settings.snapDistance = value;\n                        }\n                    }\n                }\n            });\n        }\n\n        // Main run function\n        async function run(mode) {\n            lib.log('run() called. Mode: ' + mode);\n            currentMode = mode;\n            \n            // Initialize config\n            initConfig();\n            \n            // Get canvas references\n            puzzleCanvas = document.getElementById('puzzle-canvas');\n            puzzleCtx = puzzleCanvas.getContext('2d');\n            trayCanvas = document.getElementById('tray-canvas');\n            trayCtx = trayCanvas.getContext('2d');\n            particlesCanvas = document.getElementById('particles-canvas');\n            particlesCtx = particlesCanvas.getContext('2d');\n            \n            // Show loading bar\n            const loadingOverlay = document.getElementById('loading-overlay');\n            updateLoadingBar(0, 'Loading...');\n            \n            // Preload level 1 assets first\n            await preloadLevel1Assets();\n            \n            // Hide loading bar\n            loadingOverlay.classList.add('hidden');\n            \n            // Load player progress\n            await loadProgress();\n            \n            // Setup input handlers\n            setupInputHandlers();\n            \n            // Setup edit mode if applicable\n            if (mode === 'edit') {\n                setupEditMode();\n            }\n            \n            // Start with first puzzle immediately\n            startPuzzle(0);\n            \n            // Start game loop\n            lastTime = performance.now();\n            animationFrameId = requestAnimationFrame(gameLoop);\n            \n            // Lazy load remaining assets in background\n            lazyLoadRemainingAssets();\n        }\n    </script>\n</body>\n</html>", "game_config": "{\n  \"settings\": {\n    \"gridBorderColor\": \"#FFD60A\",\n    \"gridBorderOpacity\": 30,\n    \"backgroundTint\": \"#1A1B2E\",\n    \"fragmentGlowColor\": \"#FFD60A\",\n    \"soundVolume\": 70,\n    \"snapDistance\": \"medium\"\n  },\n  \"progress\": {\n    \"unlockedLevels\": [\n      0\n    ],\n    \"completedLevels\": []\n  }\n}", "game_design_document": "## Core Game Loop and Mechanics\n\n**Fragmented Memories** is a relaxing puzzle game where players reconstruct beautiful stained-glass images by dragging scattered glass fragments from a bottom tray into their correct positions on a grid.\n\n### Moment-to-Moment Gameplay\n1. Player sees a faint outline/silhouette of the complete stained-glass image on the grid\n2. Glass fragments are scattered randomly in a tray at the bottom of the screen\n3. Player drags a fragment from the tray toward the grid\n4. When the fragment is dragged near its correct position, it highlights/glows to indicate proximity\n5. Upon release in the correct spot, the fragment snaps into place with a satisfying visual and audio effect\n6. Incorrect placements cause the fragment to return to the tray with gentle feedback\n7. Process repeats until all fragments are placed and the image is complete\n8. Upon completion, the full stained-glass image is revealed with a celebratory animation and sound\n\n### Progression and Engagement\n- **Three Distinct Images**: Players complete three progressively complex stained-glass artworks:\n  1. **Sunset Garden** (Beginner - 12 pieces)\n  2. **Ocean Depths** (Intermediate - 16 pieces)\n  3. **Cosmic Mandala** (Advanced - 20 pieces)\n- **Unlocking**: Images unlock sequentially\u2014complete one to unlock the next\n- **Replayability**: Players can replay any completed puzzle at any time\n- **No Time Pressure**: Relaxing, meditative experience with no timers or penalties\n\n### Win Conditions\n- Complete all fragments of the current image\n- All three images can be completed for full game completion\n\n### Compelling Elements\n- **Satisfying Feedback**: Smooth drag mechanics, magnetic snap-to-grid, pleasant sound effects, and visual sparkles\n- **Beautiful Art**: Each completed image is a reward in itself\n- **Progressive Challenge**: Increasing piece count provides gentle difficulty curve\n- **Zen Experience**: No failure states, no pressure\u2014pure creative satisfaction\n\n## Starting Scope\n\nFor the initial implementation, focus on creating a polished, complete experience with these elements:\n\n**Must Have (First Iteration):**\n- Complete implementation of all three puzzles (Sunset Garden, Ocean Depths, Cosmic Mandala)\n- Smooth drag-and-drop mechanics for both touch and mouse\n- Snap-to-grid functionality with proximity detection\n- Visual feedback: highlight correct positions, snap animations, completion celebration\n- Audio feedback: drag sounds, snap sounds, completion fanfare\n- Fragment tray at bottom with all pieces visible\n- Faint outline/guide on the grid showing the target image\n- Sequential unlocking system (complete one to unlock next)\n- Level selection screen showing locked/unlocked states\n- Polished UI with clear visual hierarchy\n\n**Future Enhancements (Post-Launch):**\n- Additional puzzle images (seasonal themes, user-uploaded images)\n- Hint system (briefly flash correct position for a piece)\n- Rotation mechanic for fragments (if we want added complexity)\n- Daily puzzle challenges\n- Difficulty modes (more/fewer pieces for same image)\n- Gallery mode to view completed artworks\n- Undo/reset functionality\n\n## Game Perspective\n\n**Fixed-screen** perspective with a **top-down** view of the puzzle grid. The entire puzzle grid and fragment tray are visible simultaneously on screen, with no scrolling required.\n\n### Layout (720\u00d71280 portrait)\n- **Top section (0-100px)**: Title and level indicator\n- **Middle section (100-900px)**: Puzzle grid area where fragments are placed\n- **Bottom section (900-1280px)**: Fragment tray containing scattered pieces\n\n## Game World and Level Design\n\n### Puzzle Grid Structure\n- Grid is centered in the middle section of the screen\n- Each puzzle uses a **4\u00d73, 4\u00d74, or 5\u00d74 grid** depending on piece count\n- Grid cells have subtle borders/guides to show placement zones\n- Background shows a faint, desaturated version of the target image as a guide\n- Each cell is a drop zone for exactly one fragment\n\n### Three Puzzle Images\n\n**1. Sunset Garden (Beginner - 12 pieces, 4\u00d73 grid)**\n- Theme: A serene garden scene at sunset with flowers, trees, and warm orange/pink sky\n- Color palette: Warm oranges, pinks, purples, golden yellows, deep greens\n- Complexity: Simple, organic shapes with clear color boundaries\n- Visual elements: Large flowers, tree silhouettes, setting sun\n\n**2. Ocean Depths (Intermediate - 16 pieces, 4\u00d74 grid)**\n- Theme: Underwater scene with fish, coral, and rays of light filtering through water\n- Color palette: Deep blues, teals, aquamarines, hints of coral pink/orange\n- Complexity: Medium complexity with overlapping elements and gradients\n- Visual elements: Schools of fish, coral formations, light rays, bubbles\n\n**3. Cosmic Mandala (Advanced - 20 pieces, 5\u00d74 grid)**\n- Theme: Symmetrical mandala pattern with cosmic/celestial motifs\n- Color palette: Deep purples, blues, magentas, gold accents, starlight whites\n- Complexity: High complexity with intricate geometric patterns and symmetry\n- Visual elements: Mandala patterns, stars, cosmic swirls, sacred geometry\n\n### Fragment Tray Design\n- Displays all puzzle pieces in a scrollable or wrapped layout\n- Pieces are randomly positioned/rotated slightly for visual interest\n- Each fragment shows its actual appearance (not silhouette)\n- Tray has a subtle background to distinguish it from the puzzle area\n- Pieces scale slightly when picked up to indicate they're being dragged\n\n## Edit Mode Design\n\nEdit mode allows players to customize the visual experience and puzzle parameters while maintaining core gameplay integrity.\n\n### Editable Properties\n\n**Visual Customization:**\n- **Grid Border Color**: Change the color of grid cell borders (default: subtle gold)\n- **Grid Border Opacity**: Adjust visibility of grid guides (0-100%)\n- **Background Tint**: Apply a color tint to the puzzle background\n- **Fragment Glow Color**: Customize the highlight color when pieces are near correct position\n- **Completion Effect**: Choose celebration animation style (sparkles, light burst, gentle glow)\n\n**Audio Customization:**\n- **Sound Effects Volume**: Adjust volume of snap/drag sounds (0-100%)\n- **Music Volume**: Adjust background music volume if added (0-100%)\n- **Enable/Disable Haptic Feedback**: Toggle vibration on mobile devices\n\n**Gameplay Tweaks:**\n- **Show/Hide Guide Image**: Toggle the faint background guide image\n- **Snap Distance**: Adjust how close a piece must be to snap (tight/medium/loose)\n- **Piece Count Override**: For each puzzle, allow changing piece count (within reason: 9-25 pieces)\n\n### Constraints\n- Minimum 9 pieces, maximum 25 pieces per puzzle (to prevent broken gameplay)\n- Grid must remain visible and functional (no hiding essential UI)\n- At least one puzzle must remain unlocked at all times\n- Snap distance must be reasonable (can't be so loose that wrong pieces snap)\n\n### Edit Mode UI\n- Accessible via a settings/edit button in the top-right corner\n- Panel slides in from the right with organized sections\n- Changes preview in real-time\n- \"Reset to Default\" button for each section\n- Changes persist across sessions via window.gameConfig\n\n## Control Design\n\n**Fragmented Memories** uses simple, intuitive drag-and-drop controls optimized for touch-first interaction.\n\n### Mobile (Touch) Controls\n\n**Primary Interaction: Drag and Drop**\n- **Tap and Hold** on a fragment in the tray to pick it up\n- **Drag** the fragment across the screen toward the puzzle grid\n- **Release** to attempt placement\n  - If near correct position: fragment snaps into place with feedback\n  - If incorrect: fragment returns to tray with gentle bounce animation\n\n**Visual Feedback:**\n- Fragment scales up slightly (1.1x) when picked up\n- Correct drop zone highlights/glows when fragment is dragged nearby\n- Fragment follows finger smoothly with slight lag for natural feel\n- Subtle shadow appears under dragged fragment\n\n**Touch Handling:**\n- Track touch ID to prevent multi-touch interference\n- Only one fragment can be dragged at a time\n- Touches outside fragments don't interfere with gameplay\n- Tray can be scrolled if needed (swipe when not touching a fragment)\n\n### Desktop (Mouse/Keyboard) Controls\n\n**Primary Interaction: Mouse Drag and Drop**\n- **Click and Hold** on a fragment to pick it up\n- **Drag** with mouse to move fragment\n- **Release** mouse button to attempt placement\n  - Same snap/return behavior as touch\n\n**Visual Feedback:**\n- Cursor changes to \"grab\" when hovering over fragments\n- Cursor changes to \"grabbing\" when dragging\n- Same visual feedback as touch (scaling, highlighting, shadows)\n\n**Keyboard Support (Optional Enhancement):**\n- **Tab**: Cycle through fragments in tray\n- **Arrow Keys**: Move selected fragment\n- **Enter/Space**: Attempt to place selected fragment in highlighted position\n- **Esc**: Deselect/return fragment to tray\n\n### Control Implementation Details\n\n**Drag Mechanics:**\n- Smooth interpolation between touch/mouse position and fragment position (lerp factor: 0.3)\n- Fragment center follows pointer position\n- Z-index increases for dragged fragment (appears above all others)\n\n**Snap Detection:**\n- Calculate distance between fragment center and all grid cell centers\n- If distance < snap threshold (default: 60px), highlight that cell\n- On release, if within snap threshold of correct cell, snap into place\n- Otherwise, animate return to tray with easeOut curve\n\n**Prevent Browser Defaults:**\n- Prevent default drag behavior on images\n- Prevent text selection during drag\n- Prevent context menu on long press (mobile)\n- Clear all input states on window blur\n\n**Input State Management:**\n```javascript\nconst inputState = {\n  isDragging: false,\n  draggedFragment: null,\n  startPos: { x: 0, y: 0 },\n  currentPos: { x: 0, y: 0 },\n  touchId: null // for touch tracking\n};\n```\n\n### Accessibility Considerations\n- Large touch targets (minimum 96\u00d796px for fragments)\n- Clear visual feedback for all interactions\n- No time pressure allows for careful, deliberate movements\n- High contrast between fragments and background\n- Optional keyboard controls for desktop users who prefer them\n\n## Art Style and Visual Theme\n\n**Fragmented Memories** embraces a **luminous stained-glass aesthetic** with rich, saturated colors and elegant leading (the dark lines between glass pieces).\n\n### Overall Art Direction\n- **Style**: Realistic stained-glass with authentic leading, light transmission effects, and jewel-tone colors\n- **Mood**: Serene, meditative, and beautiful\u2014evoking the peaceful atmosphere of a cathedral or art gallery\n- **Color Philosophy**: Rich, saturated colors with subtle gradients within each glass piece to simulate light passing through\n- **Consistency**: All three puzzles share the same stained-glass technique but with distinct color palettes and subjects\n\n### Visual Elements\n\n**Stained-Glass Fragments:**\n- Each fragment is a section of the complete image with dark leading (borders) around edges\n- Glass has subtle texture/grain to simulate real stained glass\n- Light effects: slight glow/luminosity as if backlit\n- Shadows: Subtle drop shadows to create depth\n- Size: Fragments vary based on puzzle complexity but average 120-150px per piece\n\n**Puzzle Grid:**\n- Subtle grid lines in metallic gold/bronze (opacity: 30%)\n- Background shows desaturated, low-opacity version of target image as guide\n- Empty cells have slight inner shadow to create recessed appearance\n- Completed sections glow softly to show progress\n\n**Fragment Tray:**\n- Dark, neutral background (deep charcoal or navy) to make colorful fragments pop\n- Subtle texture (velvet or felt appearance)\n- Fragments cast soft shadows on tray surface\n- Tray has decorative border in art nouveau style\n\n**UI Elements:**\n- Elegant, serif font for title and level names (reminiscent of art nouveau)\n- Metallic gold accents for buttons and borders\n- Frosted glass effect for panels and overlays\n- Icons use stained-glass motif (geometric, colorful)\n\n### Color Palettes by Puzzle\n\n**Sunset Garden:**\n- Primary: Warm oranges (#FF6B35, #F7931E)\n- Secondary: Pink/magenta (#FF006E, #FB5607)\n- Accents: Golden yellow (#FFBE0B), deep green (#2D6A4F)\n- Leading: Dark bronze (#3D2817)\n\n**Ocean Depths:**\n- Primary: Deep blues (#023E8A, #0077B6)\n- Secondary: Teals/aqua (#00B4D8, #48CAE4)\n- Accents: Coral pink (#FF6B9D), seafoam (#90E0EF)\n- Leading: Deep navy (#03045E)\n\n**Cosmic Mandala:**\n- Primary: Deep purples (#5A189A, #7209B7)\n- Secondary: Magentas (#C77DFF, #E0AAFF)\n- Accents: Gold (#FFD60A), starlight white (#F8F9FA)\n- Leading: Deep indigo (#10002B)\n\n### Animation and Effects\n\n**Drag Effects:**\n- Fragment scales to 1.1x when picked up\n- Soft glow appears around fragment\n- Smooth follow with slight lag (easing)\n\n**Snap Effects:**\n- Quick scale animation (1.2x \u2192 1.0x over 0.3s)\n- Bright flash of light at edges\n- Particle sparkles emanating from piece\n- Satisfying \"clink\" sound\n\n**Completion Effects:**\n- Wave of light sweeps across completed image\n- All pieces glow in sequence\n- Gentle pulsing animation\n- Confetti or light particles\n- Image brightens and becomes fully saturated\n\n### Technical Rendering Approach\n\n**Use Generated Assets For:**\n- Complete stained-glass images (all three puzzles)\n- Individual fragment images (pre-cut from complete images)\n- Background textures for tray\n- Decorative UI elements\n\n**Use CSS/Canvas For:**\n- Grid lines and guides\n- Glow effects and highlights\n- Particle effects and animations\n- UI panels and overlays\n- Text and buttons\n\n### Asset Size Guidelines\n- Complete puzzle images: 1200\u00d7900px (high resolution for quality)\n- Individual fragments: Extracted from complete images programmatically\n- UI elements: 256\u00d7256px or smaller\n- Background textures: 512\u00d7512px (tileable)", "persistent_storage_config": null, "game_metadata": {"game_id": "01KHVP3SXB38M6W69ZTY3DPK8C", "title": "Glass Peace ", "description": "Drag colorful glass fragments to complete stunning stained-glass artworks\u2014each piece snaps satisfyingly into place!", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_aed2f0ba-fcff-4786-84ad-b1ea819d4930.webp", "created_at": 1771529574, "updated_at": 1773961703623.6843, "art_style": null}}