{"asset_map": {"cozy_study_backdrop": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cozy_study_backdrop_c4696d5a-bd6a-40ed-93fd-dd8df64c92fc.webp", "type": "image", "prompt": "Transform the existing cozy_study_backdrop into a tasteful Halloween-themed version. Maintain the high-end walnut furniture, forest-green and champagne-gold palette, and cinematic lighting from the original. Add a softly glowing moonlit arched window, elegant deep-plum bat silhouettes on the walls, and delicate spiderwebs in the upper corners. Incorporate warm amber fairy lights and a subtle scattering of autumn leaves. Ensure all shelf interiors and the desktop surface remain clear for gameplay. The style must be a polished high-end 3D illustration with rich textures and warm candlelight, creating a refined cozy mood without being scary or cluttered. Preserve the exact camera angle and composition of image 1.", "description": "A luxurious Halloween-themed 'Prestige Edition' cozy study background with moonlit windows and autumn accents.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "hardcover_book": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/hardcover_book_9b9d38fc-6352-4ac8-aeb1-09d9345e175e.webp", "type": "image", "prompt": "Upgrade the existing book in image 1 into a premium 3D collectible product render. Transform it into luxurious deep teal leather with fine grain texture, crisp gilded gold page edges, and delicate gold embossing on the cover. Maintain the exact same silhouette, view angle, and framing as the original. The spine should be dimensionally rounded with realistic highlights and soft contact shadows. Keep the design elegant and uncluttered with no readable text. Professional 3D game asset style, sharp details suitable for mobile, transparent background.", "description": "A premium, polished 3D-rendered teal leather book with gold embossing for the Prestige Edition.", "subtype": "object", "aspect_ratio": [841, 1264], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "pencil_cup": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/pencil_cup_073ba09a-bf5d-44ba-a3ae-aa2b0cc417d5.webp", "type": "image", "prompt": "Transform the pencil cup in image 1 into a high-end artisan porcelain version. Preserve the original silhouette, pencil arrangement, and view angle exactly. Apply a refined ivory-and-sage glaze finish with a delicate champagne-gold rim. Enhance with realistic soft reflections, crisp pencil tips with colored lacquer, and tasteful dimensional shading. Professional 3D product render style, clean edges, highly detailed even at small sizes, transparent background, premium aesthetic.", "description": "A premium, artisan porcelain pencil cup with ivory-and-sage glaze and champagne-gold trim for the prestige edition.", "subtype": "object", "aspect_ratio": [723, 1264], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "desk_lamp": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/desk_lamp_682b384b-9d2d-4c75-a179-8d3d5f7c3729.webp", "type": "image", "prompt": "Transform the desk lamp in image 1 into a high-end luxury version for the 'Tidy Sanctuary \u00b7 Prestige Edition'. Maintain the exact silhouette, pose, and framing of the original lamp. Change the materials to a sophisticated brushed antique-brass and deep sage enamel finish. Add realistic metallic highlights, clear material separation between the metal and enamel, and an elegant soft warm glow emanating from the bulb area. Ensure professional 3D product-render quality with soft shadows, crisp details, and a transparent background. Keep the object isolated with no additional props or text.", "description": "A luxury 'Prestige Edition' upgrade of the sage desk lamp with brushed brass accents and realistic 3D lighting.", "subtype": "object", "aspect_ratio": [726, 1249], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "closed_notebook": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/closed_notebook_e427712c-95e8-4588-8fc8-afbedadda19a.webp", "type": "image", "prompt": "Upgrade the notebook in image 1 to a 'Prestige Edition'. Transform it into a high-end, premium moss-green and ochre bookcloth leather notebook. Add subtly embossed fine cover textures, delicate gold corner tooling, and a small restrained gold emblem in the center. Ensure precise page edges, realistic soft specular highlights, and elegant dimensional shadows. Maintain the exact same closed notebook shape, side-view angle, and framing as image 1. Professional 3D rendered game asset style, crisp details, no text, transparent background.", "description": "A premium 'Prestige Edition' moss-green and ochre notebook with gold-leaf detailing and leather textures.", "subtype": "object", "aspect_ratio": [809, 1263], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "small_potted_plant": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/small_potted_plant_2f7e0a7c-2b16-4728-9918-de0665c037ab.webp", "type": "image", "prompt": "Transform the small_potted_plant into a high-fidelity 3D-styled version. Preserve the exact plant species, terracotta pot silhouette, and front-view perspective from image 1. Enhance the leaves with softly luminous green colors, natural layered depth, and elegant realistic highlights. Update the terracotta pot to have a refined artisan ceramic finish with a subtle, tasteful gold accent band around the rim. The asset must be isolated on a transparent background, crisp, and high-resolution, maintaining a warm and refined aesthetic suitable for 'Tidy Sanctuary \u00b7 Prestige Edition'. No text or extra objects.", "description": "An enhanced, prestige edition of the leafy houseplant in a terracotta pot with gold accents.", "subtype": "object", "aspect_ratio": [842, 1211], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "reading_glasses": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/reading_glasses_4cb7028a-0d2a-481c-8042-c40b9c19a34c.webp", "type": "image", "prompt": "Upgrade the reading glasses from image 1 into a premium 'Prestige Edition' asset. Preserve the exact shape, warm-brown color, and three-quarter view angle. Enhance the frames with a deep, polished tortoiseshell texture and subtle champagne-gold metallic hardware at the hinges. Add clean, realistic transparent lens reflections and sharp, high-end product photography highlights. The style should be a polished, stylized 3D render consistent with a high-quality hidden object game. Isolated on a transparent background, no text, no extra props.", "description": "Premium 3D-rendered tortoiseshell reading glasses with champagne-gold hardware for the Tidy Sanctuary Prestige Edition.", "subtype": "object", "aspect_ratio": [1263, 705], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "ceramic_mug": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/ceramic_mug_191c287f-ad38-40cb-a967-b06b07687883.webp", "type": "image", "prompt": "Transform the cream ceramic mug in image 1 into a luxurious Prestige Edition asset. Change the material to a high-end ivory porcelain glaze with a restrained, elegant antique-gold rim and a tiny tasteful gold decorative accent on the side. Maintain the exact same shape, handle position, and camera view angle as the original. Add realistic soft specular reflections and subtle dimensional contact shading to enhance the 3D feel. Keep the background transparent and do not add any text or additional objects. Professional, clean game asset quality suitable for mobile interface.", "description": "A luxury ivory porcelain version of the cream ceramic mug with antique-gold accents for the Prestige Edition.", "subtype": "object", "aspect_ratio": [1022, 975], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "paper_slide_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/paper_slide_sfx_8e958fa8-b926-4661-8f4c-5a1f93013a92.mp3", "type": "audio", "prompt": "Create a very short, quiet one-shot sound effect for picking up or sliding a small paper object. Soft paper shuffle with a faint cloth-like brush, intimate close-mic texture, gentle and warm, no voice, no music, no sharp transients. Tactile and satisfying foley, suitable for a cozy desk-organizing game.", "description": "A soft, intimate paper shuffle sound effect for interacting with documents in a cozy desk environment.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.36, "loop": false, "opus_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/paper_slide_sfx_8e958fa8-b926-4661-8f4c-5a1f93013a92.ogg", "animation": null}, "gentle_place_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/gentle_place_sfx_b18bbfb8-6014-4cf3-900e-8f9e0cb29e37.mp3", "type": "audio", "prompt": "A short, quiet one-shot sound effect for a small book or paper object snapping gently onto a wooden desk. Features a warm, soft wood tap followed by a very faint, airy page flutter. Calm and satisfying haptic-like feedback, low volume, intimate close-mic texture. No metallic clicks, no voices, and no background music. Matches the soft, organic aesthetic of a cozy organizing game.", "description": "A quiet, satisfying sound effect of a small book or paper being placed onto a wooden surface.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.62, "loop": false, "opus_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/gentle_place_sfx_b18bbfb8-6014-4cf3-900e-8f9e0cb29e37.ogg", "animation": null}, "halloween_pumpkin": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/halloween_pumpkin_4645b537-5e64-4061-8504-68e8781c13b9.webp", "type": "image", "prompt": "Upgrade the jack-o'-lantern in halloween_pumpkin to a premium 'Prestige Edition' 3D collectible. Maintain the exact same friendly identity, facial expression, silhouette, and view angle. Refine with richly textured burnt-orange skin, elegant carved details showing a soft golden inner glow, and a dark forest-green stem with subtle metallic-gold accents. Apply professional 3D rendering with realistic soft shadows, crisp dimensional highlights, and a polished stylized finish. Keep it charming and isolated on a transparent background, no text, no extra props.", "description": "A premium 3D-rendered friendly jack-o'-lantern collectible with textured skin and a golden inner glow.", "subtype": "object", "aspect_ratio": [1023, 984], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "halloween_ghost": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/halloween_ghost_84d5b892-2119-4718-b1dc-6ac4af77bd43.webp", "type": "image", "prompt": "Transform the existing character in image 1 into a high-end, luminous ivory porcelain figurine. Preserve the friendly expression, recognizable silhouette, and front-view angle exactly as seen in image 1. Apply a fine glossy finish with realistic specular highlights and a very subtle champagne-gold trim along the base. Use soft, refined 3D shading to create depth and a delicate contact shadow at the base. The asset must remain isolated on a transparent background, charming and crisp, with no additional props or text, maintaining a polished 3D game-ready aesthetic.", "description": "A polished, luminous ivory porcelain version of the friendly ceramic ghost figurine for the Prestige Edition.", "subtype": "object", "aspect_ratio": [797, 848], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "halloween_bat_garland": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/halloween_bat_garland_81418803-7d15-4567-97ad-823033a5a575.webp", "type": "image", "prompt": "Refine the existing halloween_bat_garland into a premium 3D-styled game asset. Maintain the exact silhouette, arrangement of bats on the string, and framing of image 1. The bats should be rendered as crisp, high-quality decor in deep plum and near-black tones with subtle violet highlights. Add tiny, restrained antique-gold filigree details to the bats. The string should be fine and elegant. Ensure the style is polished and 3D-rendered to match the 'Prestige Edition' aesthetic. Transparent background, high resolution, tasteful and decorative, no text.", "description": "A premium, dimensional Halloween bat garland decor in deep plum and antique gold for the study shelves.", "subtype": "object", "aspect_ratio": [1264, 393], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "completion_applause": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/completion_applause_25bee6b3-379c-4d85-a72e-fbb324cd56b6.mp3", "type": "audio", "prompt": "Small audience celebratory applause, short one-shot sound effect. A handful of people clapping warmly and tastefully in a quiet, indoor acoustic space like a library or study. Include 2-3 cheerful nonverbal whoops or gasps of joy. The sound should be polished, charming, and intimate, avoiding stadium-scale noise. Clean fade-out, no music, no speech, no clipping. High-fidelity recording suitable for a cozy game completion moment.", "description": "A short, warm, and intimate celebratory applause for finishing a cozy organization task.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.99, "loop": false, "opus_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/completion_applause_25bee6b3-379c-4d85-a72e-fbb324cd56b6.ogg", "animation": null}}, "game_code": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n    <title>Tidy Sanctuary</title>\n    <style>\n        /* Reset */\n        :root {\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --transition-slow: 500ms;\n            --ease-out: cubic-bezier(0, 0, 0.2, 1);\n            --ease-bounce: cubic-bezier(0.68, -0.6, 0.32, 1.6);\n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n            --paper: #f4edde;\n            --ink: #40362d;\n            --sage: #66816c;\n        }\n\n        *,\n        *::before,\n        *::after {\n            box-sizing: border-box;\n        }\n\n        html,\n        body {\n            width: 100%;\n            height: 100%;\n            margin: 0;\n            padding: 0;\n            overflow: hidden;\n            background: var(--paper);\n            touch-action: none;\n            -ms-touch-action: none;\n            overscroll-behavior: none;\n            -webkit-tap-highlight-color: transparent;\n        }\n\n        body {\n            font-family: \"Poppins\", \"Inter\", Arial, sans-serif;\n            color: var(--ink);\n            user-select: none;\n            -webkit-user-select: none;\n        }\n\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            background: var(--paper);\n        }\n\n        #game-canvas {\n            display: block;\n            width: 100%;\n            height: 100%;\n            border: 0;\n            outline: none;\n            touch-action: none;\n            -ms-touch-action: none;\n            user-select: none;\n            -webkit-user-select: none;\n            cursor: default;\n        }\n\n        #game-canvas:hover {\n            cursor: default;\n        }\n\n        #game-canvas:active {\n            cursor: grabbing;\n        }\n\n        #game-canvas:focus-visible {\n            outline: 4px solid #66816c;\n            outline-offset: -8px;\n        }\n\n        #game-canvas:disabled {\n            opacity: 0.65;\n            cursor: not-allowed;\n        }\n\n        @media (prefers-reduced-motion: reduce) {\n            *,\n            *::before,\n            *::after {\n                scroll-behavior: auto !important;\n                animation-duration: 0.01ms !important;\n                transition-duration: 0.01ms !important;\n            }\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas\n            id=\"game-canvas\"\n            width=\"720\"\n            height=\"1280\"\n            tabindex=\"0\"\n            aria-label=\"Desk Tidy Challenge. Organize books, group school supplies, and clear the center of the desk.\"\n        ></canvas>\n    </div>\n\n    <script>\n        (function () {\n            \"use strict\";\n\n            /*\n             * Tidy Sanctuary \u2014 current build step: Move things around.\n             * A quiet, fixed-size illustrated study scene with movable starter objects.\n             * Drag an object onto a shelf or the desktop to snap it into place.\n             * Dropping outside a surface returns it to its previous position.\n             *\n             * In Edit mode, dragging starter objects updates window.gameConfig.\n             * Play mode uses a separate runtime arrangement.\n             */\n\n            const SCENE_WIDTH = 720;\n            const SCENE_HEIGHT = 1280;\n\n            const DEFAULT_CONFIG = {\n                viewport: {\n                    width: 720,\n                    height: 1280\n                },\n                snap_step_px: 24,\n                surface_zones: [\n                    {\n                        id: \"upper_shelf\",\n                        kind: \"shelf\",\n                        x: 72,\n                        y: 184,\n                        width: 576,\n                        height: 146,\n                        baseline_y: 318\n                    },\n                    {\n                        id: \"middle_shelf\",\n                        kind: \"shelf\",\n                        x: 72,\n                        y: 336,\n                        width: 576,\n                        height: 139,\n                        baseline_y: 463\n                    },\n                    {\n                        id: \"lower_shelf\",\n                        kind: \"shelf\",\n                        x: 72,\n                        y: 481,\n                        width: 576,\n                        height: 139,\n                        baseline_y: 608\n                    },\n                    {\n                        id: \"desk_top\",\n                        kind: \"desk\",\n                        x: 38,\n                        y: 700,\n                        width: 644,\n                        height: 230\n                    }\n                ],\n                catalog_settings: {\n                    enabled_types: [\"hardcover_book\", \"closed_notebook\", \"pencil_cup\", \"desk_lamp\", \"small_potted_plant\", \"reading_glasses\", \"ceramic_mug\"],\n                    item_scale: 1,\n                    max_item_count: 24,\n                    stack_limit: 4,\n                    sound_enabled: true,\n                    sound_volume: 0.35,\n                    palette: [\"sage\", \"terracotta\", \"teal\", \"ochre\", \"plum\"],\n                    default_book_tint: \"sage\",\n                    halloween_items_migrated: true\n                },\n                starting_items: [\n                    {\n                        id: \"book_01\",\n                        type: \"hardcover_book\",\n                        surface: \"desk_top\",\n                        x: 230,\n                        y: 772,\n                        scale: 1,\n                        tint: \"sage\"\n                    },\n                    {\n                        id: \"plant_01\",\n                        type: \"small_potted_plant\",\n                        surface: \"upper_shelf\",\n                        x: 552,\n                        y: 269,\n                        scale: 0.9,\n                        tint: \"sage\"\n                    },\n                    {\n                        id: \"notebook_01\",\n                        type: \"closed_notebook\",\n                        surface: \"desk_top\",\n                        x: 360,\n                        y: 824,\n                        scale: 1,\n                        tint: \"ochre\"\n                    },\n                    {\n                        id: \"book_04\",\n                        type: \"hardcover_book\",\n                        surface: \"desk_top\",\n                        x: 470,\n                        y: 784,\n                        scale: 1,\n                        tint: \"plum\"\n                    },\n                    {\n                        id: \"book_02\",\n                        type: \"hardcover_book\",\n                        surface: \"lower_shelf\",\n                        x: 504,\n                        y: 564,\n                        scale: 1,\n                        tint: \"terracotta\"\n                    },\n                    {\n                        id: \"book_03\",\n                        type: \"hardcover_book\",\n                        surface: \"desk_top\",\n                        x: 230,\n                        y: 772,\n                        scale: 1,\n                        tint: \"teal\"\n                    },\n                    {\n                        id: \"glasses_01\",\n                        type: \"reading_glasses\",\n                        surface: \"desk_top\",\n                        x: 302,\n                        y: 796,\n                        scale: 1,\n                        tint: \"cream\"\n                    },\n                    {\n                        id: \"pencil_cup_01\",\n                        type: \"pencil_cup\",\n                        surface: \"desk_top\",\n                        x: 398,\n                        y: 796,\n                        scale: 1,\n                        tint: \"cream\"\n                    },\n                    {\n                        id: \"mug_01\",\n                        type: \"ceramic_mug\",\n                        surface: \"desk_top\",\n                        x: 494,\n                        y: 796,\n                        scale: 1,\n                        tint: \"cream\"\n                    },\n                    {\n                        id: \"lamp_01\",\n                        type: \"desk_lamp\",\n                        surface: \"desk_top\",\n                        x: 602,\n                        y: 772,\n                        scale: 1,\n                        tint: \"sage\"\n                    },\n                    {\n                        id: \"halloween_garland_01\",\n                        type: \"halloween_bat_garland\",\n                        surface: \"upper_shelf\",\n                        x: 360,\n                        y: 278,\n                        scale: 0.9,\n                        tint: \"plum\"\n                    },\n                    {\n                        id: \"halloween_ghost_01\",\n                        type: \"halloween_ghost\",\n                        surface: \"middle_shelf\",\n                        x: 552,\n                        y: 422,\n                        scale: 0.9,\n                        tint: \"cream\"\n                    },\n                    {\n                        id: \"halloween_pumpkin_01\",\n                        type: \"halloween_pumpkin\",\n                        surface: \"lower_shelf\",\n                        x: 312,\n                        y: 568,\n                        scale: 0.95,\n                        tint: \"terracotta\"\n                    }\n                ]\n            };\n\n            const ITEM_TYPES = {\n                hardcover_book: {\n                    width: 56,\n                    height: 88,\n                    label: \"Hardcover book\"\n                },\n                pencil_cup: {\n                    width: 80,\n                    height: 106,\n                    label: \"Pencil cup\"\n                },\n                desk_lamp: {\n                    width: 126,\n                    height: 120,\n                    label: \"Desk lamp\"\n                },\n                closed_notebook: {\n                    width: 88,\n                    height: 66,\n                    label: \"Notebook\"\n                },\n                small_potted_plant: {\n                    width: 78,\n                    height: 98,\n                    label: \"Plant\"\n                },\n                reading_glasses: {\n                    width: 92,\n                    height: 54,\n                    label: \"Glasses\"\n                },\n                ceramic_mug: {\n                    width: 72,\n                    height: 80,\n                    label: \"Mug\"\n                },\n                halloween_pumpkin: {\n                    width: 82,\n                    height: 82,\n                    label: \"Jack-o'-lantern\"\n                },\n                halloween_ghost: {\n                    width: 70,\n                    height: 82,\n                    label: \"Friendly ghost\"\n                },\n                halloween_bat_garland: {\n                    width: 220,\n                    height: 78,\n                    label: \"Bat garland\"\n                }\n            };\n\n            const CATALOG_CATEGORIES = {\n                Books: [\"hardcover_book\", \"closed_notebook\"],\n                Supplies: [\"pencil_cup\", \"desk_lamp\", \"reading_glasses\", \"ceramic_mug\"],\n                Extras: [\"small_potted_plant\"]\n            };\n\n            const CATALOG_LABELS = {\n                hardcover_book: \"Hardcover book\",\n                closed_notebook: \"Notebook\",\n                pencil_cup: \"Pencil cup\",\n                desk_lamp: \"Desk lamp\",\n                small_potted_plant: \"Plant\",\n                reading_glasses: \"Reading glasses\",\n                ceramic_mug: \"Mug\",\n                halloween_pumpkin: \"Jack-o'-lantern\",\n                halloween_ghost: \"Friendly ghost\",\n                halloween_bat_garland: \"Bat garland\"\n            };\n\n            const CATALOG_LABELS_ZH = {\n                hardcover_book: \"BOOK\",\n                closed_notebook: \"NOTEBOOK\",\n                pencil_cup: \"PENCILS\",\n                desk_lamp: \"LAMP\",\n                small_potted_plant: \"PLANT\",\n                reading_glasses: \"GLASSES\",\n                ceramic_mug: \"MUG\",\n                halloween_pumpkin: \"PUMPKIN\",\n                halloween_ghost: \"GHOST\",\n                halloween_bat_garland: \"GARLAND\"\n            };\n\n            const CATALOG_CATEGORY_LABELS = { Books: \"\u66f8\u7c4d\", Desk: \"\u684c\u9762\", Decor: \"\u88dd\u98fe\", Halloween: \"\u842c\u8056\u7bc0\" };\n            const BUTTON_LABELS_ZH = {\n                Rotate: \"\u65cb\u8f49\", Duplicate: \"\u8907\u88fd\", Remove: \"\u79fb\u9664\", Place: \"\u653e\u7f6e\",\n                Undo: \"\u5fa9\u539f\", Redo: \"\u91cd\u505a\", Reset: \"\u91cd\u8a2d\", \"Sound on\": \"\u958b\u555f\u97f3\u6548\", \"Sound off\": \"\u95dc\u9589\u97f3\u6548\"\n            };\n\n            const BOOK_COLORS = {\n                sage: \"#6f9b55\",\n                terracotta: \"#e5773f\",\n                teal: \"#258baa\",\n                ochre: \"#dca52f\",\n                plum: \"#9852a5\"\n            };\n\n            const SHELF_LIMITS = {\n                upper_shelf: { baselineMin: 314, baselineMax: 322 },\n                middle_shelf: { baselineMin: 459, baselineMax: 467 },\n                lower_shelf: { baselineMin: 604, baselineMax: 612 }\n            };\n\n            const ZONE_ORDER = [\"upper_shelf\", \"middle_shelf\", \"lower_shelf\", \"desk_top\"];\n\n            let activeApp = null;\n\n            function cloneJson(value) {\n                return JSON.parse(JSON.stringify(value));\n            }\n\n            function numericValue(value, fallback) {\n                const number = Number(value);\n                return Number.isFinite(number) ? number : fallback;\n            }\n\n            function clamp(value, minimum, maximum) {\n                return Math.max(minimum, Math.min(maximum, value));\n            }\n\n            function setIfDifferent(object, key, value) {\n                if (object[key] !== value) {\n                    object[key] = value;\n                }\n            }\n\n            function ensureGameConfig() {\n                if (!window.gameConfig || typeof window.gameConfig !== \"object\") {\n                    window.gameConfig = cloneJson(DEFAULT_CONFIG);\n                }\n\n                const config = window.gameConfig;\n\n                if (!config.viewport || typeof config.viewport !== \"object\") {\n                    config.viewport = { width: 720, height: 1280 };\n                } else {\n                    setIfDifferent(config.viewport, \"width\", 720);\n                    setIfDifferent(config.viewport, \"height\", 1280);\n                }\n\n                if (!Number.isFinite(Number(config.snap_step_px))) {\n                    config.snap_step_px = 24;\n                }\n\n                config.snap_step_px = Math.round(\n                    clamp(numericValue(config.snap_step_px, 24), 12, 48)\n                );\n\n                if (!Array.isArray(config.surface_zones)) {\n                    config.surface_zones = cloneJson(DEFAULT_CONFIG.surface_zones);\n                }\n\n                for (const defaultZone of DEFAULT_CONFIG.surface_zones) {\n                    const existing = config.surface_zones.find(function (zone) {\n                        return zone && zone.id === defaultZone.id;\n                    });\n\n                    if (!existing) {\n                        config.surface_zones.push(cloneJson(defaultZone));\n                    }\n                }\n\n                if (!Array.isArray(config.starting_items)) {\n                    config.starting_items = cloneJson(DEFAULT_CONFIG.starting_items);\n                }\n\n                if (!config.catalog_settings || typeof config.catalog_settings !== \"object\") {\n                    config.catalog_settings = cloneJson(DEFAULT_CONFIG.catalog_settings);\n                }\n                if (!Array.isArray(config.catalog_settings.enabled_types)) {\n                    config.catalog_settings.enabled_types = cloneJson(DEFAULT_CONFIG.catalog_settings.enabled_types);\n                }\n                config.catalog_settings.enabled_types = config.catalog_settings.enabled_types.filter(function (type, index, list) {\n                    return ITEM_TYPES[type] && type.indexOf(\"halloween_\") !== 0 && list.indexOf(type) === index;\n                });\n                config.catalog_settings.item_scale = clamp(numericValue(config.catalog_settings.item_scale, 1), 0.75, 1.25);\n                config.catalog_settings.max_item_count = Math.round(clamp(numericValue(config.catalog_settings.max_item_count, 24), 8, 40));\n                config.catalog_settings.stack_limit = Math.round(clamp(numericValue(config.catalog_settings.stack_limit, 4), 1, 4));\n                config.catalog_settings.sound_enabled = config.catalog_settings.sound_enabled !== false;\n                config.catalog_settings.sound_volume = clamp(numericValue(config.catalog_settings.sound_volume, 0.35), 0, 1);\n                if (!Array.isArray(config.catalog_settings.palette)) config.catalog_settings.palette = cloneJson(DEFAULT_CONFIG.catalog_settings.palette);\n                config.catalog_settings.palette = config.catalog_settings.palette.filter(function (tint) { return Object.prototype.hasOwnProperty.call(BOOK_COLORS, tint); });\n                if (!config.catalog_settings.palette.length) config.catalog_settings.palette = cloneJson(DEFAULT_CONFIG.catalog_settings.palette);\n                if (!BOOK_COLORS[config.catalog_settings.default_book_tint]) config.catalog_settings.default_book_tint = \"sage\";\n                config.starting_items = config.starting_items.filter(function(item){return item&&ITEM_TYPES[item.type]&&item.type.indexOf(\"halloween_\")!==0;}).slice(0,config.catalog_settings.max_item_count);\n\n                normalizeSurfaceZones(config);\n                return config;\n            }\n\n            function normalizeSurfaceZones(config) {\n                const zones = config.surface_zones;\n\n                for (let index = 0; index < 3; index += 1) {\n                    const defaultZone = DEFAULT_CONFIG.surface_zones[index];\n                    const zone = zones.find(function (entry) {\n                        return entry && entry.id === defaultZone.id;\n                    });\n\n                    if (!zone) {\n                        continue;\n                    }\n\n                    const limits = SHELF_LIMITS[defaultZone.id];\n                    const x = Math.round(\n                        clamp(numericValue(zone.x, defaultZone.x), 72, 120)\n                    );\n                    const maximumWidth = Math.min(576, 648 - x);\n                    const width = Math.round(\n                        clamp(numericValue(zone.width, defaultZone.width), 432, maximumWidth)\n                    );\n                    const baseline = Math.round(\n                        clamp(\n                            numericValue(zone.baseline_y, defaultZone.baseline_y),\n                            limits.baselineMin,\n                            limits.baselineMax\n                        )\n                    );\n\n                    const previousShelf = index > 0\n                        ? zones.find(function (entry) {\n                            return entry && entry.id === DEFAULT_CONFIG.surface_zones[index - 1].id;\n                        })\n                        : null;\n\n                    const minimumTop = index === 0\n                        ? 166\n                        : numericValue(previousShelf && previousShelf.baseline_y, defaultZone.baseline_y - 145) + 18;\n\n                    const maximumTop = baseline - 64;\n                    const top = Math.round(\n                        clamp(numericValue(zone.y, defaultZone.y), minimumTop, maximumTop)\n                    );\n\n                    setIfDifferent(zone, \"id\", defaultZone.id);\n                    setIfDifferent(zone, \"kind\", \"shelf\");\n                    setIfDifferent(zone, \"x\", x);\n                    setIfDifferent(zone, \"width\", width);\n                    setIfDifferent(zone, \"baseline_y\", baseline);\n                    setIfDifferent(zone, \"y\", top);\n                    setIfDifferent(zone, \"height\", baseline + 12 - top);\n                }\n\n                const deskDefault = DEFAULT_CONFIG.surface_zones[3];\n                const desk = zones.find(function (entry) {\n                    return entry && entry.id === \"desk_top\";\n                });\n\n                if (desk) {\n                    const x = Math.round(\n                        clamp(numericValue(desk.x, deskDefault.x), 38, 78)\n                    );\n                    const maximumWidth = 682 - x;\n                    const width = Math.round(\n                        clamp(numericValue(desk.width, deskDefault.width), 520, maximumWidth)\n                    );\n                    const top = Math.round(\n                        clamp(numericValue(desk.y, deskDefault.y), 700, 760)\n                    );\n                    const maximumHeight = 930 - top;\n                    const height = Math.round(\n                        clamp(numericValue(desk.height, deskDefault.height), 152, maximumHeight)\n                    );\n\n                    setIfDifferent(desk, \"id\", \"desk_top\");\n                    setIfDifferent(desk, \"kind\", \"desk\");\n                    setIfDifferent(desk, \"x\", x);\n                    setIfDifferent(desk, \"y\", top);\n                    setIfDifferent(desk, \"width\", width);\n                    setIfDifferent(desk, \"height\", height);\n                }\n            }\n\n            function roundRectPath(ctx, x, y, width, height, radius) {\n                const r = Math.max(0, Math.min(radius, width / 2, height / 2));\n\n                ctx.beginPath();\n                ctx.moveTo(x + r, y);\n                ctx.lineTo(x + width - r, y);\n                ctx.quadraticCurveTo(x + width, y, x + width, y + r);\n                ctx.lineTo(x + width, y + height - r);\n                ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);\n                ctx.lineTo(x + r, y + height);\n                ctx.quadraticCurveTo(x, y + height, x, y + height - r);\n                ctx.lineTo(x, y + r);\n                ctx.quadraticCurveTo(x, y, x + r, y);\n                ctx.closePath();\n            }\n\n            function fillRoundRect(ctx, x, y, width, height, radius, fillStyle) {\n                roundRectPath(ctx, x, y, width, height, radius);\n                ctx.fillStyle = fillStyle;\n                ctx.fill();\n            }\n\n            function strokeRoundRect(ctx, x, y, width, height, radius, strokeStyle, lineWidth) {\n                roundRectPath(ctx, x, y, width, height, radius);\n                ctx.strokeStyle = strokeStyle;\n                ctx.lineWidth = lineWidth || 1;\n                ctx.stroke();\n            }\n\n            function darkenHex(hex, amount) {\n                if (!/^#[0-9a-fA-F]{6}$/.test(hex)) {\n                    return \"#52665a\";\n                }\n\n                const red = parseInt(hex.slice(1, 3), 16);\n                const green = parseInt(hex.slice(3, 5), 16);\n                const blue = parseInt(hex.slice(5, 7), 16);\n                const factor = 1 - amount;\n\n                function channel(value) {\n                    return Math.max(0, Math.round(value * factor))\n                        .toString(16)\n                        .padStart(2, \"0\");\n                }\n\n                return \"#\" + channel(red) + channel(green) + channel(blue);\n            }\n\n            function bookColor(tint) {\n                if (BOOK_COLORS[tint]) {\n                    return BOOK_COLORS[tint];\n                }\n\n                if (typeof tint === \"string\" && /^#[0-9a-fA-F]{6}$/.test(tint)) {\n                    return tint;\n                }\n\n                return BOOK_COLORS.sage;\n            }\n\n            function drawMiniShelfIcon(ctx, centerX, centerY) {\n                ctx.save();\n                ctx.lineCap = \"round\";\n                ctx.lineJoin = \"round\";\n\n                ctx.strokeStyle = \"#5c755f\";\n                ctx.lineWidth = 3;\n\n                ctx.beginPath();\n                ctx.moveTo(centerX - 17, centerY - 13);\n                ctx.lineTo(centerX + 17, centerY - 13);\n                ctx.moveTo(centerX - 17, centerY);\n                ctx.lineTo(centerX + 17, centerY);\n                ctx.moveTo(centerX - 17, centerY + 13);\n                ctx.lineTo(centerX + 17, centerY + 13);\n                ctx.stroke();\n\n                fillRoundRect(ctx, centerX - 12, centerY - 25, 9, 10, 2, \"#c87d62\");\n                fillRoundRect(ctx, centerX + 2, centerY - 25, 9, 10, 2, \"#718d77\");\n                fillRoundRect(ctx, centerX - 7, centerY - 11, 13, 9, 2, \"#c2a062\");\n\n                ctx.restore();\n            }\n\n            function drawBackground(ctx) {\n                const wallGradient = ctx.createLinearGradient(0, 0, 0, SCENE_HEIGHT);\n                wallGradient.addColorStop(0, \"#f7f1e6\");\n                wallGradient.addColorStop(0.56, \"#f0e6d6\");\n                wallGradient.addColorStop(1, \"#ece1cf\");\n\n                ctx.fillStyle = wallGradient;\n                ctx.fillRect(0, 0, SCENE_WIDTH, SCENE_HEIGHT);\n\n                const sunlight = ctx.createRadialGradient(570, 190, 20, 570, 190, 520);\n                sunlight.addColorStop(0, \"rgba(255, 248, 228, 0.34)\");\n                sunlight.addColorStop(1, \"rgba(255, 248, 228, 0)\");\n                ctx.fillStyle = sunlight;\n                ctx.fillRect(0, 0, SCENE_WIDTH, 680);\n\n                ctx.fillStyle = \"#e8dac5\";\n                ctx.fillRect(0, 650, SCENE_WIDTH, 22);\n                ctx.fillStyle = \"#cdb99c\";\n                ctx.fillRect(0, 672, SCENE_WIDTH, 4);\n                ctx.fillStyle = \"rgba(255,255,255,0.22)\";\n                ctx.fillRect(0, 651, SCENE_WIDTH, 2);\n\n                ctx.save();\n                ctx.globalAlpha = 0.08;\n                ctx.fillStyle = \"#876c4f\";\n                for (let row = 0; row < 12; row += 1) {\n                    for (let column = 0; column < 17; column += 1) {\n                        const x = 18 + column * 43 + (row % 2) * 12;\n                        const y = 136 + row * 39;\n                        ctx.beginPath();\n                        ctx.arc(x, y, 1.1, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                }\n                ctx.restore();\n            }\n\n            function drawRoomDepth(ctx) {\n                // Subtle ambient occlusion and bevel highlights reinforce the cabinet and desk's depth.\n                ctx.save();\n                ctx.globalCompositeOperation = \"multiply\";\n                [318, 463, 608].forEach(function (y) {\n                    const shelfShadow = ctx.createLinearGradient(0, y, 0, y + 22);\n                    shelfShadow.addColorStop(0, \"rgba(47, 30, 20, 0.24)\");\n                    shelfShadow.addColorStop(0.34, \"rgba(47, 30, 20, 0.10)\");\n                    shelfShadow.addColorStop(1, \"rgba(47, 30, 20, 0)\");\n                    ctx.fillStyle = shelfShadow;\n                    ctx.fillRect(62, y + 1, 596, 22);\n                });\n                const cabinetSideShade = ctx.createLinearGradient(70, 0, 96, 0);\n                cabinetSideShade.addColorStop(0, \"rgba(58, 36, 22, 0.24)\");\n                cabinetSideShade.addColorStop(1, \"rgba(58, 36, 22, 0)\");\n                ctx.fillStyle = cabinetSideShade;\n                ctx.fillRect(70, 166, 26, 452);\n                const rightSideShade = ctx.createLinearGradient(624, 0, 650, 0);\n                rightSideShade.addColorStop(0, \"rgba(58, 36, 22, 0)\");\n                rightSideShade.addColorStop(1, \"rgba(58, 36, 22, 0.24)\");\n                ctx.fillStyle = rightSideShade;\n                ctx.fillRect(624, 166, 26, 452);\n\n                ctx.globalCompositeOperation = \"source-over\";\n                ctx.globalAlpha = 0.48;\n                const deskEdge = ctx.createLinearGradient(0, 878, 0, 910);\n                deskEdge.addColorStop(0, \"rgba(255, 235, 190, 0)\");\n                deskEdge.addColorStop(0.22, \"rgba(255, 235, 190, 0.25)\");\n                deskEdge.addColorStop(0.34, \"rgba(73, 45, 26, 0.18)\");\n                deskEdge.addColorStop(1, \"rgba(45, 28, 20, 0.08)\");\n                ctx.fillStyle = deskEdge;\n                ctx.fillRect(42, 878, 636, 32);\n                ctx.restore();\n            }\n\n            function drawPrestigeFrame(ctx, elapsed) {\n                // A restrained champagne-gold inlay frames the study without covering its furnishings.\n                ctx.save();\n                const gold = ctx.createLinearGradient(0, 0, 720, 0);\n                gold.addColorStop(0, \"rgba(177, 139, 70, 0.18)\");\n                gold.addColorStop(0.5, \"rgba(226, 197, 132, 0.78)\");\n                gold.addColorStop(1, \"rgba(177, 139, 70, 0.18)\");\n                ctx.strokeStyle = gold;\n                ctx.lineWidth = 1;\n                ctx.globalAlpha = 0.72;\n                [22, 698].forEach(function (x) {\n                    ctx.beginPath();\n                    ctx.moveTo(x, 178);\n                    ctx.lineTo(x, 902);\n                    ctx.stroke();\n                    ctx.fillStyle = \"#d4b66e\";\n                    [184, 650, 897].forEach(function (y) {\n                        ctx.beginPath();\n                        ctx.arc(x, y, 2.2, 0, Math.PI * 2);\n                        ctx.fill();\n                    });\n                });\n                ctx.globalAlpha = 0.58;\n                ctx.lineWidth = 1.6;\n                ctx.lineCap = \"round\";\n                ctx.beginPath();\n                ctx.moveTo(22, 210);\n                ctx.quadraticCurveTo(22, 184, 49, 184);\n                ctx.moveTo(698, 210);\n                ctx.quadraticCurveTo(698, 184, 671, 184);\n                ctx.moveTo(22, 870);\n                ctx.quadraticCurveTo(22, 897, 49, 897);\n                ctx.moveTo(698, 870);\n                ctx.quadraticCurveTo(698, 897, 671, 897);\n                ctx.stroke();\n                ctx.restore();\n            }\n\n            function drawHeader(ctx, isEditMode, level) {\n                const headerGradient = ctx.createLinearGradient(0, 0, 0, 124);\n                headerGradient.addColorStop(0, \"#19382f\");\n                headerGradient.addColorStop(1, \"#10271f\");\n                ctx.fillStyle = headerGradient;\n                ctx.fillRect(0, 0, SCENE_WIDTH, 124);\n                const headerGilt = ctx.createLinearGradient(0, 0, SCENE_WIDTH, 0);\n                headerGilt.addColorStop(0, \"rgba(190, 153, 83, 0.15)\");\n                headerGilt.addColorStop(0.5, \"rgba(232, 204, 143, 0.95)\");\n                headerGilt.addColorStop(1, \"rgba(190, 153, 83, 0.15)\");\n                ctx.fillStyle = headerGilt;\n                ctx.fillRect(0, 0, SCENE_WIDTH, 3);\n\n                ctx.fillStyle = \"#f0e3c6\";\n                fillRoundRect(ctx, 48, 31, 48, 58, 14, \"#f0e3c6\");\n                strokeRoundRect(ctx, 48, 31, 48, 58, 14, \"#c9a95f\", 2);\n                drawMiniShelfIcon(ctx, 72, 61);\n\n                ctx.textAlign = \"left\";\n                ctx.textBaseline = \"middle\";\n                ctx.fillStyle = \"#f3e8ce\";\n                ctx.font = '700 42px \"Playfair Display\", \"Source Serif\", Georgia, serif';\n                ctx.fillText(\"Desk Tidy Challenge\", 112, 52);\n\n                ctx.fillStyle = \"#d4c29b\";\n                ctx.font = '500 17px \"Poppins\", \"Inter\", Arial, sans-serif';\n                ctx.fillText(\"A Student Study Space \u00b7 Sort, Group, Make Room\", 114, 91);\n\n                const goldRule = ctx.createLinearGradient(114, 0, 430, 0);\n                goldRule.addColorStop(0, \"rgba(177, 139, 70, 0.9)\");\n                goldRule.addColorStop(1, \"rgba(177, 139, 70, 0)\");\n                ctx.fillStyle = goldRule;\n                ctx.fillRect(114, 111, 300, 2);\n\n                const badgeFill = isEditMode ? \"#294438\" : \"#314a37\";\n                const badgeStroke = isEditMode ? \"#c5a65f\" : \"#d5b56a\";\n                const badgeText = \"#f2dfad\";\n                fillRoundRect(ctx, 530, 39, 142, 46, 23, badgeFill);\n                strokeRoundRect(ctx, 530, 39, 142, 46, 23, badgeStroke, 1.5);\n\n                ctx.textAlign = \"center\";\n                ctx.fillStyle = badgeText;\n                ctx.font = '700 13px \"Poppins\", \"Inter\", Arial, sans-serif';\n                ctx.fillText(isEditMode ? \"EDIT MODE\" : \"LEVEL \" + level + \" / 3\", 601, 55);\n                ctx.font = '600 12px \"Poppins\", \"Inter\", Arial, sans-serif';\n                ctx.fillText(isEditMode ? \"LAYOUT\" : \"TAP TO CHANGE\", 601, 74);\n\n                ctx.strokeStyle = \"rgba(207, 177, 110, 0.72)\";\n                ctx.lineWidth = 1;\n                ctx.beginPath();\n                ctx.moveTo(0, 123.5);\n                ctx.lineTo(SCENE_WIDTH, 123.5);\n                ctx.stroke();\n\n                ctx.textAlign = \"left\";\n            }\n\n            function drawIllustratedBackdrop(ctx, assets) {\n                const image = assets && assets.cozy_study_backdrop;\n                if (image && image.complete && image.naturalWidth > 0) {\n                    ctx.drawImage(image, 0, 0, SCENE_WIDTH, SCENE_HEIGHT);\n                    const warmLight = ctx.createRadialGradient(566, 300, 24, 566, 300, 470);\n                    warmLight.addColorStop(0, \"rgba(255, 246, 214, 0.18)\");\n                    warmLight.addColorStop(1, \"rgba(255, 246, 214, 0)\");\n                    ctx.fillStyle = warmLight;\n                    ctx.fillRect(0, 124, SCENE_WIDTH, 760);\n                    const edgeShade = ctx.createLinearGradient(0, 0, SCENE_WIDTH, 0);\n                    edgeShade.addColorStop(0, \"rgba(73, 54, 35, 0.06)\");\n                    edgeShade.addColorStop(0.14, \"rgba(73, 54, 35, 0)\");\n                    edgeShade.addColorStop(0.86, \"rgba(73, 54, 35, 0)\");\n                    edgeShade.addColorStop(1, \"rgba(73, 54, 35, 0.06)\");\n                    ctx.fillStyle = edgeShade;\n                    ctx.fillRect(0, 124, SCENE_WIDTH, SCENE_HEIGHT - 124);\n                } else {\n                    // Keep the scene calm while the backdrop asset finishes loading.\n                    ctx.fillStyle = \"#f4edde\";\n                    ctx.fillRect(0, 0, SCENE_WIDTH, SCENE_HEIGHT);\n                }\n            }\n\n            function drawPrestigeInlays(ctx, elapsed) {\n                ctx.save();\n\n                // Fine champagne-brass detailing makes the existing walnut furniture feel bespoke,\n                // while keeping all decoration on the furniture edges and clear of movable items.\n                const gilt = ctx.createLinearGradient(42, 0, 678, 0);\n                gilt.addColorStop(0, \"rgba(151, 105, 43, 0.38)\");\n                gilt.addColorStop(0.18, \"rgba(239, 211, 151, 0.78)\");\n                gilt.addColorStop(0.52, \"rgba(255, 239, 194, 0.72)\");\n                gilt.addColorStop(0.84, \"rgba(201, 162, 91, 0.7)\");\n                gilt.addColorStop(1, \"rgba(151, 105, 43, 0.38)\");\n                ctx.strokeStyle = gilt;\n                ctx.lineWidth = 1.15;\n                ctx.shadowColor = \"rgba(226, 190, 112, 0.24)\";\n                ctx.shadowBlur = 5;\n\n                // Double, restrained inlay around the walnut bookcase silhouette.\n                roundRectPath(ctx, 47, 144, 626, 500, 14);\n                ctx.stroke();\n                ctx.globalAlpha = 0.48;\n                roundRectPath(ctx, 55, 152, 610, 484, 8);\n                ctx.stroke();\n                ctx.globalAlpha = 1;\n\n                // A small carved crown medallion, echoing a classic library cabinet crest.\n                ctx.fillStyle = \"rgba(33, 47, 37, 0.72)\";\n                ctx.beginPath();\n                ctx.ellipse(360, 154, 28, 9, 0, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.strokeStyle = \"rgba(228, 194, 122, 0.86)\";\n                ctx.lineWidth = 1.2;\n                ctx.beginPath();\n                ctx.ellipse(360, 154, 27, 8, 0, 0, Math.PI * 2);\n                ctx.stroke();\n                ctx.beginPath();\n                ctx.moveTo(360, 148);\n                ctx.lineTo(366, 154);\n                ctx.lineTo(360, 160);\n                ctx.lineTo(354, 154);\n                ctx.closePath();\n                ctx.fillStyle = \"#d9bb76\";\n                ctx.fill();\n                ctx.fillStyle = \"rgba(255, 239, 192, 0.82)\";\n                ctx.beginPath();\n                ctx.arc(360, 154, 1.6, 0, Math.PI * 2);\n                ctx.fill();\n\n                // Delicate gilt shelf lips and tiny brass end caps.\n                [318, 463, 608].forEach(function (y) {\n                    const lip = ctx.createLinearGradient(54, y, 666, y);\n                    lip.addColorStop(0, \"rgba(160, 112, 49, 0.32)\");\n                    lip.addColorStop(0.5, \"rgba(248, 222, 164, 0.76)\");\n                    lip.addColorStop(1, \"rgba(160, 112, 49, 0.32)\");\n                    ctx.strokeStyle = lip;\n                    ctx.lineWidth = 1.25;\n                    ctx.beginPath();\n                    ctx.moveTo(58, y + 2);\n                    ctx.lineTo(662, y + 2);\n                    ctx.stroke();\n                    [60, 660].forEach(function (x) {\n                        ctx.fillStyle = \"rgba(231, 197, 128, 0.88)\";\n                        ctx.beginPath();\n                        ctx.arc(x, y + 8, 2.1, 0, Math.PI * 2);\n                        ctx.fill();\n                        ctx.fillStyle = \"rgba(94, 62, 34, 0.36)\";\n                        ctx.beginPath();\n                        ctx.arc(x, y + 8, 0.8, 0, Math.PI * 2);\n                        ctx.fill();\n                    });\n                });\n\n                // An understated brass perimeter and a fine inset line on the desk surface.\n                ctx.strokeStyle = \"rgba(228, 193, 125, 0.58)\";\n                ctx.lineWidth = 1.2;\n                roundRectPath(ctx, 39, 700, 642, 230, 17);\n                ctx.stroke();\n                ctx.globalAlpha = 0.34;\n                ctx.lineWidth = 0.9;\n                roundRectPath(ctx, 49, 709, 622, 205, 12);\n                ctx.stroke();\n                ctx.globalAlpha = 1;\n\n                // Refined twin drawer pulls on the desk apron.\n                [302, 418].forEach(function (x) {\n                    ctx.fillStyle = \"rgba(89, 57, 31, 0.34)\";\n                    fillRoundRect(ctx, x - 18, 903, 36, 8, 4, ctx.fillStyle);\n                    const handle = ctx.createLinearGradient(x - 16, 0, x + 16, 0);\n                    handle.addColorStop(0, \"#9a7139\");\n                    handle.addColorStop(0.5, \"#f0d28e\");\n                    handle.addColorStop(1, \"#9a7139\");\n                    fillRoundRect(ctx, x - 15, 902, 30, 4, 2, handle);\n                    [x - 20, x + 20].forEach(function (studX) {\n                        ctx.fillStyle = \"#e5c47d\";\n                        ctx.beginPath();\n                        ctx.arc(studX, 904, 1.8, 0, Math.PI * 2);\n                        ctx.fill();\n                    });\n                });\n\n                // Fine Art-Deco side ornaments sit in the margins, away from the play area.\n                const glint = 0.58 + Math.sin(elapsed * 0.75) * 0.12;\n                ctx.globalAlpha = glint;\n                ctx.strokeStyle = \"rgba(222, 190, 122, 0.82)\";\n                ctx.fillStyle = \"rgba(239, 215, 165, 0.8)\";\n                ctx.lineWidth = 1;\n                [34, 686].forEach(function (x) {\n                    [248, 402, 556, 776].forEach(function (y) {\n                        ctx.beginPath();\n                        ctx.moveTo(x, y - 5);\n                        ctx.lineTo(x + (x < 360 ? 4 : -4), y);\n                        ctx.lineTo(x, y + 5);\n                        ctx.lineTo(x + (x < 360 ? -4 : 4), y);\n                        ctx.closePath();\n                        ctx.fill();\n                        ctx.stroke();\n                        ctx.beginPath();\n                        ctx.moveTo(x + (x < 360 ? 5 : -5), y);\n                        ctx.lineTo(x + (x < 360 ? 9 : -9), y);\n                        ctx.stroke();\n                    });\n                });\n\n                ctx.restore();\n            }\n\n            function drawHalloweenAccents(ctx, elapsed) {\n                ctx.save();\n                ctx.lineCap = \"round\";\n                ctx.lineJoin = \"round\";\n\n                // Candle-warm fairy lights trace the cabinet crown.\n                ctx.strokeStyle = \"rgba(74, 47, 59, 0.68)\";\n                ctx.lineWidth = 1.5;\n                ctx.beginPath();\n                ctx.moveTo(94, 170);\n                ctx.bezierCurveTo(220, 185, 500, 185, 626, 170);\n                ctx.stroke();\n                for (let i = 0; i < 7; i += 1) {\n                    const x = 126 + i * 78;\n                    const y = 174 + Math.sin((i / 6) * Math.PI) * 8;\n                    const color = i % 2 === 0 ? \"#f2a44e\" : \"#d8a5c4\";\n                    ctx.globalAlpha = 0.82 + Math.sin(elapsed * 1.8 + i * 1.4) * 0.12;\n                    ctx.shadowColor = color;\n                    ctx.shadowBlur = 9;\n                    ctx.fillStyle = color;\n                    ctx.beginPath();\n                    ctx.arc(x, y + 5, 3.2, 0, Math.PI * 2);\n                    ctx.fill();\n                }\n                ctx.shadowBlur = 0;\n                ctx.globalAlpha = 1;\n\n                // Airy cobwebs live in the otherwise empty upper cabinet corners.\n                function drawCobweb(originX, originY, direction) {\n                    ctx.strokeStyle = \"rgba(236, 221, 194, 0.76)\";\n                    ctx.lineWidth = 0.85;\n                    for (let ray = 0; ray <= 4; ray += 1) {\n                        const t = ray / 4;\n                        ctx.beginPath();\n                        ctx.moveTo(originX, originY);\n                        ctx.lineTo(originX + direction * 32 * t, originY + 27 * (1 - t));\n                        ctx.stroke();\n                    }\n                    for (let ring = 1; ring <= 4; ring += 1) {\n                        const t = ring / 4;\n                        ctx.beginPath();\n                        ctx.moveTo(originX + direction * 32 * t, originY);\n                        ctx.quadraticCurveTo(originX + direction * 32 * t, originY + 27 * t, originX, originY + 27 * t);\n                        ctx.stroke();\n                    }\n                    ctx.fillStyle = \"rgba(216, 165, 196, 0.92)\";\n                    ctx.beginPath();\n                    ctx.arc(originX + direction * 32, originY + 27, 1.8, 0, Math.PI * 2);\n                    ctx.fill();\n                }\n                drawCobweb(76, 188, 1);\n                drawCobweb(644, 188, -1);\n\n                // Small bats drift in the side margins, leaving the play area clear.\n                function drawBat(x, y, scale, phase) {\n                    ctx.save();\n                    ctx.translate(x, y + Math.sin(phase) * 4);\n                    ctx.scale(scale, scale);\n                    ctx.fillStyle = \"rgba(55, 43, 66, 0.92)\";\n                    ctx.beginPath();\n                    ctx.moveTo(0, 0);\n                    ctx.bezierCurveTo(-6, -8, -12, -10, -18, -7);\n                    ctx.lineTo(-13, 1);\n                    ctx.lineTo(-20, -2);\n                    ctx.quadraticCurveTo(-17, 8, -8, 5);\n                    ctx.quadraticCurveTo(-4, 10, 0, 5);\n                    ctx.quadraticCurveTo(4, 10, 8, 5);\n                    ctx.quadraticCurveTo(17, 8, 20, -2);\n                    ctx.lineTo(13, 1);\n                    ctx.lineTo(18, -7);\n                    ctx.bezierCurveTo(12, -10, 6, -8, 0, 0);\n                    ctx.closePath();\n                    ctx.fill();\n                    ctx.restore();\n                }\n                drawBat(28, 365, 0.62, elapsed * 1.1);\n                drawBat(692, 510, 0.62, elapsed * 1.1 + 2.4);\n\n                // Small glowing pumpkin accents finish the lower corners of the scene.\n                function drawMiniPumpkin(x, y) {\n                    ctx.save();\n                    ctx.translate(x, y);\n                    ctx.fillStyle = \"rgba(99, 54, 44, 0.9)\";\n                    ctx.beginPath();\n                    ctx.ellipse(0, 0, 10, 8, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    ctx.fillStyle = \"#d9803c\";\n                    ctx.beginPath();\n                    ctx.ellipse(0, 0, 8, 7, 0, 0, Math.PI * 2);\n                    ctx.fill();\n                    ctx.strokeStyle = \"rgba(255, 195, 104, 0.78)\";\n                    ctx.lineWidth = 0.8;\n                    ctx.beginPath();\n                    ctx.ellipse(0, 0, 3.2, 6.3, 0, 0, Math.PI * 2);\n                    ctx.stroke();\n                    ctx.strokeStyle = \"#f9d878\";\n                    ctx.lineWidth = 1;\n                    ctx.beginPath();\n                    ctx.moveTo(-4, -1); ctx.lineTo(-1, 0); ctx.lineTo(-3, 2);\n                    ctx.moveTo(4, -1); ctx.lineTo(1, 0); ctx.lineTo(3, 2);\n                    ctx.moveTo(-3, 4); ctx.quadraticCurveTo(0, 2, 3, 4);\n                    ctx.stroke();\n                    ctx.fillStyle = \"#728157\";\n                    fillRoundRect(ctx, -1.5, -9, 3, 4, 1, \"#728157\");\n                    ctx.restore();\n                }\n                drawMiniPumpkin(20, 874);\n                drawMiniPumpkin(700, 874);\n                ctx.restore();\n            }\n\n            function drawHalloweenAtmosphere(ctx, elapsed, sessionComplete) {\n                ctx.save();\n                const warmGlow = ctx.createRadialGradient(610, 734, 12, 610, 734, 330);\n                warmGlow.addColorStop(0, sessionComplete ? \"rgba(255, 160, 52, 0.15)\" : \"rgba(255, 156, 67, 0.09)\");\n                warmGlow.addColorStop(1, \"rgba(255, 156, 67, 0)\");\n                ctx.fillStyle = warmGlow;\n                ctx.fillRect(280, 500, 440, 430);\n                const dusk = ctx.createLinearGradient(0, 124, 0, 920);\n                dusk.addColorStop(0, \"rgba(85, 59, 95, 0.035)\");\n                dusk.addColorStop(0.55, \"rgba(85, 59, 95, 0.012)\");\n                dusk.addColorStop(1, \"rgba(85, 59, 95, 0)\");\n                ctx.fillStyle = dusk;\n                ctx.fillRect(0, 124, SCENE_WIDTH, 800);\n                for (let i = 0; i < 9; i += 1) {\n                    const phase = elapsed * 0.55 + i * 1.71;\n                    const x = 24 + ((i * 83 + Math.sin(phase) * 9 + 720) % 672);\n                    const y = 220 + ((i * 97 + elapsed * 7) % 650);\n                    ctx.globalAlpha = 0.18 + (Math.sin(phase) + 1) * 0.12;\n                    ctx.fillStyle = i % 2 ? \"#d88a4b\" : \"#f0c77d\";\n                    ctx.beginPath();\n                    ctx.ellipse(x, y, 2.3, 4.2, phase, 0, Math.PI * 2);\n                    ctx.fill();\n                }\n                ctx.restore();\n            }\n\n            function drawBookshelf(ctx, zones) {\n                ctx.save();\n                ctx.shadowColor = \"rgba(73, 49, 30, 0.20)\";\n                ctx.shadowBlur = 18;\n                ctx.shadowOffsetY = 8;\n\n                const outerGradient = ctx.createLinearGradient(48, 145, 672, 145);\n                outerGradient.addColorStop(0, \"#81583a\");\n                outerGradient.addColorStop(0.48, \"#a87449\");\n                outerGradient.addColorStop(1, \"#805537\");\n                fillRoundRect(ctx, 48, 145, 624, 495, 15, outerGradient);\n                ctx.restore();\n\n                fillRoundRect(ctx, 62, 158, 596, 470, 7, \"#7e5337\");\n\n                const backGradient = ctx.createLinearGradient(0, 166, 0, 620);\n                backGradient.addColorStop(0, \"#e8dbc4\");\n                backGradient.addColorStop(1, \"#d9c3a5\");\n                fillRoundRect(ctx, 72, 166, 576, 452, 3, backGradient);\n\n                const shelfZones = zones.filter(function (zone) {\n                    return zone.kind === \"shelf\";\n                });\n\n                for (let index = 0; index < shelfZones.length; index += 1) {\n                    const zone = shelfZones[index];\n                    const previous = index > 0 ? shelfZones[index - 1] : null;\n                    const openingTop = previous ? previous.baseline_y + 18 : 166;\n                    const openingHeight = Math.max(1, zone.baseline_y - openingTop);\n\n                    const openingGradient = ctx.createLinearGradient(72, openingTop, 72, zone.baseline_y);\n                    openingGradient.addColorStop(0, \"#eadfcf\");\n                    openingGradient.addColorStop(1, \"#e1d2bb\");\n                    ctx.fillStyle = openingGradient;\n                    ctx.fillRect(72, openingTop, 576, openingHeight);\n\n                    ctx.save();\n                    ctx.globalAlpha = 0.14;\n                    ctx.strokeStyle = \"#9d8262\";\n                    ctx.lineWidth = 1;\n                    for (let x = 126; x <= 600; x += 92) {\n                        ctx.beginPath();\n                        ctx.moveTo(x, openingTop + 8);\n                        ctx.lineTo(x + 3, zone.baseline_y - 10);\n                        ctx.stroke();\n                    }\n                    ctx.restore();\n\n                    const boardGradient = ctx.createLinearGradient(\n                        0,\n                        zone.baseline_y,\n                        0,\n                        zone.baseline_y + 21\n                    );\n                    boardGradient.addColorStop(0, \"#c28a55\");\n                    boardGradient.addColorStop(0.35, \"#a97044\");\n                    boardGradient.addColorStop(1, \"#8c5c3b\");\n\n                    ctx.fillStyle = \"rgba(65, 43, 27, 0.18)\";\n                    ctx.fillRect(55, zone.baseline_y + 17, 610, 5);\n\n                    ctx.fillStyle = boardGradient;\n                    ctx.fillRect(54, zone.baseline_y, 612, 18);\n\n                    ctx.fillStyle = \"#d29b61\";\n                    ctx.fillRect(54, zone.baseline_y, 612, 3);\n\n                    ctx.strokeStyle = \"rgba(92, 56, 32, 0.35)\";\n                    ctx.lineWidth = 1;\n                    ctx.beginPath();\n                    ctx.moveTo(56, zone.baseline_y + 17.5);\n                    ctx.lineTo(664, zone.baseline_y + 17.5);\n                    ctx.stroke();\n                }\n\n                const sideGradient = ctx.createLinearGradient(48, 0, 74, 0);\n                sideGradient.addColorStop(0, \"#784f34\");\n                sideGradient.addColorStop(0.5, \"#a87549\");\n                sideGradient.addColorStop(1, \"#8a5e3d\");\n                fillRoundRect(ctx, 48, 145, 28, 495, 9, sideGradient);\n\n                const rightSideGradient = ctx.createLinearGradient(646, 0, 672, 0);\n                rightSideGradient.addColorStop(0, \"#8a5e3d\");\n                rightSideGradient.addColorStop(0.5, \"#a87549\");\n                rightSideGradient.addColorStop(1, \"#784f34\");\n                fillRoundRect(ctx, 644, 145, 28, 495, 9, rightSideGradient);\n\n                const crownGradient = ctx.createLinearGradient(0, 140, 0, 169);\n                crownGradient.addColorStop(0, \"#c28a55\");\n                crownGradient.addColorStop(1, \"#99663f\");\n                fillRoundRect(ctx, 42, 140, 636, 29, 11, crownGradient);\n                ctx.fillStyle = \"rgba(255, 220, 164, 0.48)\";\n                ctx.fillRect(56, 145, 608, 2);\n\n                const baseGradient = ctx.createLinearGradient(0, 622, 0, 648);\n                baseGradient.addColorStop(0, \"#a16c43\");\n                baseGradient.addColorStop(1, \"#7d5237\");\n                fillRoundRect(ctx, 44, 622, 632, 25, 8, baseGradient);\n                ctx.fillStyle = \"rgba(229, 183, 125, 0.55)\";\n                ctx.fillRect(58, 624, 604, 2);\n            }\n\n            function drawDesk(ctx) {\n                ctx.save();\n\n                const legGradient = ctx.createLinearGradient(80, 0, 640, 0);\n                legGradient.addColorStop(0, \"#8b5e3d\");\n                legGradient.addColorStop(0.5, \"#b17a4b\");\n                legGradient.addColorStop(1, \"#805337\");\n\n                fillRoundRect(ctx, 79, 893, 42, 70, 7, legGradient);\n                fillRoundRect(ctx, 599, 893, 42, 70, 7, legGradient);\n                fillRoundRect(ctx, 95, 895, 530, 35, 6, \"#956541\");\n\n                ctx.shadowColor = \"rgba(68, 46, 29, 0.22)\";\n                ctx.shadowBlur = 17;\n                ctx.shadowOffsetY = 8;\n\n                const deskGradient = ctx.createLinearGradient(0, 700, 0, 930);\n                deskGradient.addColorStop(0, \"#d5aa75\");\n                deskGradient.addColorStop(0.45, \"#c8945c\");\n                deskGradient.addColorStop(1, \"#a86f45\");\n                fillRoundRect(ctx, 38, 700, 644, 230, 18, deskGradient);\n                ctx.restore();\n\n                ctx.save();\n                roundRectPath(ctx, 43, 705, 634, 218, 14);\n                ctx.clip();\n\n                const topSheen = ctx.createLinearGradient(0, 706, 0, 888);\n                topSheen.addColorStop(0, \"rgba(255, 238, 204, 0.34)\");\n                topSheen.addColorStop(1, \"rgba(255, 238, 204, 0.04)\");\n                ctx.fillStyle = topSheen;\n                ctx.fillRect(43, 705, 634, 183);\n\n                ctx.globalAlpha = 0.18;\n                ctx.strokeStyle = \"#845a37\";\n                ctx.lineWidth = 1.2;\n                for (let y = 724; y < 878; y += 31) {\n                    ctx.beginPath();\n                    ctx.moveTo(56, y);\n                    ctx.bezierCurveTo(205, y - 5, 474, y + 7, 666, y - 1);\n                    ctx.stroke();\n                }\n\n                ctx.globalAlpha = 0.11;\n                ctx.fillStyle = \"#704a30\";\n                ctx.beginPath();\n                ctx.ellipse(475, 813, 29, 8, -0.12, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.beginPath();\n                ctx.ellipse(475, 813, 18, 4, -0.12, 0, Math.PI * 2);\n                ctx.stroke();\n\n                const frontGradient = ctx.createLinearGradient(0, 884, 0, 930);\n                frontGradient.addColorStop(0, \"#b67b4b\");\n                frontGradient.addColorStop(1, \"#99633f\");\n                ctx.globalAlpha = 1;\n                ctx.fillStyle = frontGradient;\n                ctx.fillRect(42, 884, 636, 46);\n\n                ctx.fillStyle = \"rgba(237, 193, 135, 0.56)\";\n                ctx.fillRect(48, 884, 624, 3);\n                ctx.restore();\n\n                strokeRoundRect(ctx, 38, 700, 644, 230, 18, \"#9e6c45\", 2);\n                ctx.strokeStyle = \"rgba(248, 222, 178, 0.52)\";\n                ctx.lineWidth = 1.2;\n                ctx.beginPath();\n                ctx.moveTo(53, 710);\n                ctx.lineTo(667, 710);\n                ctx.stroke();\n            }\n\n            function drawOrganizationZones(ctx, desk) {\n                if (!desk) return;\n                const supplies = { x: desk.x + 18, y: desk.y + 25, width: desk.width * 0.36 - 18, height: desk.height - 50 };\n                const workspace = { x: desk.x + desk.width * 0.36, y: desk.y + 25, width: desk.width * 0.31, height: desk.height - 50 };\n                ctx.save();\n                ctx.fillStyle = \"rgba(94, 132, 95, 0.12)\";\n                fillRoundRect(ctx, supplies.x, supplies.y, supplies.width, supplies.height, 13, ctx.fillStyle);\n                ctx.setLineDash([6, 6]);\n                strokeRoundRect(ctx, supplies.x, supplies.y, supplies.width, supplies.height, 13, \"rgba(79, 119, 83, 0.72)\", 2);\n                ctx.fillStyle = \"rgba(225, 191, 111, 0.12)\";\n                fillRoundRect(ctx, workspace.x, workspace.y, workspace.width, workspace.height, 13, ctx.fillStyle);\n                strokeRoundRect(ctx, workspace.x, workspace.y, workspace.width, workspace.height, 13, \"rgba(170, 127, 55, 0.72)\", 2);\n                ctx.setLineDash([]);\n                ctx.textAlign = \"center\";\n                ctx.textBaseline = \"top\";\n                ctx.font = '700 11px \"Poppins\", \"Inter\", Arial, sans-serif';\n                ctx.fillStyle = \"rgba(51, 84, 58, 0.78)\";\n                ctx.fillText(\"SUPPLIES\", supplies.x + supplies.width / 2, supplies.y + 8);\n                ctx.fillStyle = \"rgba(116, 81, 31, 0.72)\";\n                ctx.fillText(\"CLEAR WORK AREA\", workspace.x + workspace.width / 2, workspace.y + 8);\n                ctx.restore();\n            }\n\n            function drawZoneGuides(ctx, zones, isEditMode, targetZone) {\n                for (const zone of zones) {\n                    const isTarget = targetZone && targetZone.id === zone.id;\n                    if (!isEditMode && !isTarget) {\n                        continue;\n                    }\n\n                    ctx.save();\n                    ctx.fillStyle = isTarget\n                        ? \"rgba(93, 139, 110, 0.15)\"\n                        : \"rgba(83, 126, 98, 0.045)\";\n                    fillRoundRect(ctx, zone.x, zone.y, zone.width, zone.height, 8, ctx.fillStyle);\n\n                    ctx.setLineDash(isTarget ? [9, 6] : [7, 7]);\n                    ctx.lineDashOffset = 0;\n                    ctx.lineWidth = isTarget ? 3 : 2;\n                    ctx.strokeStyle = isTarget\n                        ? \"rgba(64, 112, 79, 0.92)\"\n                        : \"rgba(71, 113, 84, 0.56)\";\n                    strokeRoundRect(\n                        ctx,\n                        zone.x,\n                        zone.y,\n                        zone.width,\n                        zone.height,\n                        8,\n                        ctx.strokeStyle,\n                        ctx.lineWidth\n                    );\n                    ctx.restore();\n                }\n            }\n\n            function drawPencil(ctx, x, pointY, bottomY, color, width) {\n                const half = width / 2;\n                ctx.beginPath();\n                ctx.moveTo(x, pointY);\n                ctx.lineTo(x + half, pointY + 8);\n                ctx.lineTo(x + half, bottomY);\n                ctx.lineTo(x - half, bottomY);\n                ctx.lineTo(x - half, pointY + 8);\n                ctx.closePath();\n                ctx.fillStyle = color;\n                ctx.fill();\n\n                ctx.beginPath();\n                ctx.moveTo(x - half + 1, pointY + 7);\n                ctx.lineTo(x + half - 1, pointY + 7);\n                ctx.lineTo(x + half - 1, bottomY);\n                ctx.lineTo(x - half + 1, bottomY);\n                ctx.closePath();\n                ctx.fillStyle = \"rgba(255,255,255,0.22)\";\n                ctx.fill();\n\n                ctx.beginPath();\n                ctx.moveTo(x, pointY);\n                ctx.lineTo(x - 2, pointY + 7);\n                ctx.lineTo(x + 2, pointY + 7);\n                ctx.closePath();\n                ctx.fillStyle = \"#d7b98b\";\n                ctx.fill();\n            }\n\n            function drawHardcover(ctx, tint) {\n                const width = 46;\n                const height = 76;\n                const cover = bookColor(tint);\n                const spine = darkenHex(cover, 0.26);\n\n                ctx.fillStyle = \"rgba(58, 41, 27, 0.18)\";\n                ctx.beginPath();\n                ctx.ellipse(1, 37, 22, 4.5, 0, 0, Math.PI * 2);\n                ctx.fill();\n\n                const coverGradient = ctx.createLinearGradient(-23, 0, 23, 0);\n                coverGradient.addColorStop(0, spine);\n                coverGradient.addColorStop(0.24, cover);\n                coverGradient.addColorStop(1, darkenHex(cover, 0.08));\n                fillRoundRect(ctx, -width / 2, -height / 2, width, height, 6, coverGradient);\n                strokeRoundRect(ctx, -width / 2, -height / 2, width, height, 6, \"rgba(53, 43, 34, 0.40)\", 1.4);\n\n                fillRoundRect(ctx, -16, -33, 33, 66, 4, \"rgba(255, 242, 214, 0.08)\");\n                ctx.strokeStyle = \"rgba(245, 224, 181, 0.68)\";\n                ctx.lineWidth = 1.2;\n                roundRectPath(ctx, -15, -32, 30, 64, 3);\n                ctx.stroke();\n\n                ctx.fillStyle = spine;\n                fillRoundRect(ctx, -21, -35, 7, 70, 3, spine);\n\n                ctx.strokeStyle = \"rgba(248, 224, 174, 0.78)\";\n                ctx.lineWidth = 1.5;\n                ctx.beginPath();\n                ctx.moveTo(-19, -24);\n                ctx.lineTo(-16, -24);\n                ctx.moveTo(-19, 23);\n                ctx.lineTo(-16, 23);\n                ctx.stroke();\n\n                ctx.fillStyle = \"rgba(248, 225, 179, 0.78)\";\n                ctx.beginPath();\n                ctx.arc(0, -1, 5, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.fillStyle = \"rgba(95, 73, 48, 0.46)\";\n                ctx.beginPath();\n                ctx.arc(0, -1, 2.2, 0, Math.PI * 2);\n                ctx.fill();\n\n                ctx.strokeStyle = \"rgba(255, 242, 218, 0.48)\";\n                ctx.lineWidth = 1;\n                ctx.beginPath();\n                ctx.moveTo(-11, -27);\n                ctx.lineTo(10, -27);\n                ctx.moveTo(-11, 28);\n                ctx.lineTo(10, 28);\n                ctx.stroke();\n            }\n\n            function drawPencilCup(ctx) {\n                ctx.fillStyle = \"rgba(58, 41, 27, 0.18)\";\n                ctx.beginPath();\n                ctx.ellipse(1, 45, 34, 5, 0, 0, Math.PI * 2);\n                ctx.fill();\n\n                drawPencil(ctx, -21, -48, -12, \"#c77b5e\", 8);\n                drawPencil(ctx, -9, -55, -12, \"#657f72\", 8);\n                drawPencil(ctx, 4, -45, -12, \"#d1a34f\", 8);\n                drawPencil(ctx, 17, -52, -12, \"#758a9a\", 8);\n\n                const cupGradient = ctx.createLinearGradient(-31, 0, 31, 0);\n                cupGradient.addColorStop(0, \"#9c7857\");\n                cupGradient.addColorStop(0.4, \"#c5a37c\");\n                cupGradient.addColorStop(1, \"#a47e5b\");\n                fillRoundRect(ctx, -31, -15, 62, 59, 11, cupGradient);\n\n                ctx.beginPath();\n                ctx.ellipse(0, -14, 31, 9, 0, 0, Math.PI * 2);\n                ctx.fillStyle = \"#d4b68e\";\n                ctx.fill();\n                ctx.strokeStyle = \"#96724f\";\n                ctx.lineWidth = 1.5;\n                ctx.stroke();\n\n                ctx.beginPath();\n                ctx.ellipse(0, -15, 24, 5.4, 0, 0, Math.PI * 2);\n                ctx.fillStyle = \"#70563f\";\n                ctx.fill();\n\n                ctx.strokeStyle = \"rgba(255, 243, 220, 0.55)\";\n                ctx.lineWidth = 2;\n                ctx.beginPath();\n                ctx.moveTo(-23, -4);\n                ctx.lineTo(-21, 29);\n                ctx.stroke();\n\n                ctx.fillStyle = \"rgba(255, 239, 211, 0.42)\";\n                fillRoundRect(ctx, 9, -3, 8, 34, 4, \"rgba(255, 239, 211, 0.42)\");\n            }\n\n            function drawDeskLamp(ctx) {\n                ctx.fillStyle = \"rgba(58, 41, 27, 0.17)\";\n                ctx.beginPath();\n                ctx.ellipse(0, 55, 42, 7, 0, 0, Math.PI * 2);\n                ctx.fill();\n\n                const metal = \"#65786f\";\n                const metalDark = \"#455b53\";\n\n                const footGradient = ctx.createLinearGradient(0, 42, 0, 60);\n                footGradient.addColorStop(0, \"#91a196\");\n                footGradient.addColorStop(1, metalDark);\n                fillRoundRect(ctx, -35, 43, 70, 13, 7, footGradient);\n                ctx.beginPath();\n                ctx.ellipse(0, 43, 30, 7, 0, Math.PI, Math.PI * 2);\n                ctx.fillStyle = \"#83958a\";\n                ctx.fill();\n\n                ctx.lineCap = \"round\";\n                ctx.lineJoin = \"round\";\n                ctx.strokeStyle = metalDark;\n                ctx.lineWidth = 9;\n                ctx.beginPath();\n                ctx.moveTo(-7, 43);\n                ctx.lineTo(7, -11);\n                ctx.stroke();\n\n                ctx.strokeStyle = \"#8b9a8e\";\n                ctx.lineWidth = 4;\n                ctx.beginPath();\n                ctx.moveTo(-4, 40);\n                ctx.lineTo(10, -8);\n                ctx.stroke();\n\n                ctx.fillStyle = \"#c18a50\";\n                ctx.beginPath();\n                ctx.arc(8, -12, 7, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.fillStyle = \"#e0b273\";\n                ctx.beginPath();\n                ctx.arc(8, -12, 3, 0, Math.PI * 2);\n                ctx.fill();\n\n                const shadeGradient = ctx.createLinearGradient(-24, -42, 25, -12);\n                shadeGradient.addColorStop(0, \"#d6a85f\");\n                shadeGradient.addColorStop(0.55, \"#c88d4b\");\n                shadeGradient.addColorStop(1, \"#a96f3e\");\n\n                ctx.beginPath();\n                ctx.moveTo(-25, -43);\n                ctx.lineTo(25, -43);\n                ctx.lineTo(16, -12);\n                ctx.lineTo(-17, -12);\n                ctx.closePath();\n                ctx.fillStyle = shadeGradient;\n                ctx.fill();\n                ctx.strokeStyle = \"#8f6039\";\n                ctx.lineWidth = 1.6;\n                ctx.stroke();\n\n                ctx.beginPath();\n                ctx.moveTo(-25, -43);\n                ctx.quadraticCurveTo(0, -56, 25, -43);\n                ctx.lineTo(19, -36);\n                ctx.quadraticCurveTo(0, -45, -19, -36);\n                ctx.closePath();\n                ctx.fillStyle = \"#e0b66e\";\n                ctx.fill();\n\n                ctx.beginPath();\n                ctx.ellipse(0, -12, 17, 4, 0, 0, Math.PI);\n                ctx.fillStyle = \"#f2d39a\";\n                ctx.fill();\n\n                ctx.strokeStyle = \"rgba(255, 241, 204, 0.7)\";\n                ctx.lineWidth = 2;\n                ctx.beginPath();\n                ctx.moveTo(-16, -39);\n                ctx.lineTo(13, -39);\n                ctx.stroke();\n            }\n\n            function itemDimensions(item) {\n                const definition = ITEM_TYPES[item.type];\n                const scale = clamp(numericValue(item.scale, 1), 0.75, 1.25);\n\n                return {\n                    width: definition.width * scale,\n                    height: definition.height * scale,\n                    scale: scale\n                };\n            }\n\n            function drawSelection(ctx, item, hovered) {\n                const dimensions = itemDimensions(item);\n                const padding = hovered ? 8 : 10;\n                const width = dimensions.width + padding * 2;\n                const height = dimensions.height + padding * 2;\n\n                ctx.save();\n                ctx.fillStyle = hovered\n                    ? \"rgba(92, 131, 102, 0.045)\"\n                    : \"rgba(78, 124, 91, 0.075)\";\n                fillRoundRect(\n                    ctx,\n                    item.x - width / 2,\n                    item.y - height / 2,\n                    width,\n                    height,\n                    13,\n                    ctx.fillStyle\n                );\n\n                ctx.shadowColor = hovered\n                    ? \"rgba(82, 125, 92, 0.14)\"\n                    : \"rgba(69, 119, 82, 0.24)\";\n                ctx.shadowBlur = hovered ? 8 : 13;\n                ctx.strokeStyle = hovered ? \"#819a7f\" : \"#527b5d\";\n                ctx.lineWidth = hovered ? 2 : 3;\n                strokeRoundRect(\n                    ctx,\n                    item.x - width / 2,\n                    item.y - height / 2,\n                    width,\n                    height,\n                    13,\n                    ctx.strokeStyle,\n                    ctx.lineWidth\n                );\n                ctx.restore();\n            }\n\n            function drawItem(ctx, item, options) {\n                const settings = options || {};\n                const definition = ITEM_TYPES[item.type];\n                if (!definition) {\n                    return;\n                }\n\n                const dimensions = itemDimensions(item);\n\n                // Soft, offset contact shadows visually anchor each object to its surface.\n                if (!settings.ghost) {\n                    ctx.save();\n                    ctx.fillStyle = \"rgba(47, 31, 20, 0.18)\";\n                    ctx.shadowColor = \"rgba(48, 31, 20, 0.25)\";\n                    ctx.shadowBlur = settings.dragging ? 12 : 6;\n                    ctx.shadowOffsetX = 5;\n                    ctx.shadowOffsetY = settings.dragging ? 12 : 5;\n                    ctx.beginPath();\n                    ctx.ellipse(\n                        item.x,\n                        item.y + dimensions.height * 0.40,\n                        Math.max(12, dimensions.width * 0.39),\n                        Math.max(4, dimensions.height * 0.075),\n                        0,\n                        0,\n                        Math.PI * 2\n                    );\n                    ctx.fill();\n                    ctx.restore();\n                }\n\n                if (!settings.ghost && settings.selected) {\n                    drawSelection(ctx, item, false);\n                } else if (!settings.ghost && settings.hovered) {\n                    drawSelection(ctx, item, true);\n                }\n\n                ctx.save();\n                ctx.translate(item.x, item.y);\n                ctx.rotate(numericValue(item.rotation, 0) * Math.PI / 180);\n                ctx.scale(dimensions.scale, dimensions.scale);\n\n                if (settings.ghost) {\n                    ctx.globalAlpha = 0.25;\n                } else {\n                    ctx.shadowColor = settings.dragging\n                        ? \"rgba(48, 35, 25, 0.28)\"\n                        : \"rgba(48, 35, 25, 0.18)\";\n                    // Smaller blur radii reduce repeated GPU/compositing work for every sprite.\n                    ctx.shadowBlur = settings.dragging ? 15 : 7;\n                    ctx.shadowOffsetX = settings.dragging ? 7 : 3;\n                    ctx.shadowOffsetY = settings.dragging ? 10 : 5;\n                }\n\n                const image = settings.assets && settings.assets[item.type];\n                if (image && image.complete && image.naturalWidth > 0) {\n                    ctx.drawImage(\n                        image,\n                        -definition.width / 2,\n                        -definition.height / 2,\n                        definition.width,\n                        definition.height\n                    );\n\n                    // Strongly colorize hardcover art so the four task colors remain easy to tell apart.\n                    if (item.type === \"hardcover_book\" && item.tint) {\n                        ctx.save();\n                        ctx.globalCompositeOperation = \"source-atop\";\n                        ctx.globalAlpha *= 0.76;\n                        ctx.fillStyle = bookColor(item.tint);\n                        ctx.fillRect(\n                            -definition.width / 2,\n                            -definition.height / 2,\n                            definition.width,\n                            definition.height\n                        );\n                        ctx.restore();\n\n                        // A clear colored spine marker makes each book identifiable at a glance.\n                        ctx.save();\n                        ctx.globalCompositeOperation = \"source-over\";\n                        ctx.globalAlpha = 0.96;\n                        ctx.fillStyle = bookColor(item.tint);\n                        fillRoundRect(ctx, -definition.width / 2 + 3, -definition.height / 2 + 9, 8, definition.height - 18, 3, ctx.fillStyle);\n                        ctx.globalAlpha = 0.72;\n                        strokeRoundRect(ctx, -definition.width / 2 + 3, -definition.height / 2 + 9, 8, definition.height - 18, 3, \"rgba(255,255,255,0.82)\", 1);\n                        ctx.restore();\n                    }\n                }\n\n                ctx.restore();\n\n                if (settings.ghost) {\n                    ctx.save();\n                    ctx.globalAlpha = 0.75;\n                    ctx.setLineDash([7, 6]);\n                    ctx.lineWidth = 2;\n                    ctx.strokeStyle = \"#557d61\";\n                    strokeRoundRect(\n                        ctx,\n                        item.x - dimensions.width / 2 - 5,\n                        item.y - dimensions.height / 2 - 5,\n                        dimensions.width + 10,\n                        dimensions.height + 10,\n                        9,\n                        ctx.strokeStyle,\n                        ctx.lineWidth\n                    );\n                    ctx.restore();\n                }\n            }\n\n            function drawFeedback(ctx, feedback) {\n                if (!feedback) {\n                    return;\n                }\n\n                const progress = 1 - feedback.life / feedback.duration;\n                const alpha = Math.max(0, 1 - progress);\n                const radius = 18 + progress * 20;\n\n                ctx.save();\n                ctx.globalAlpha = alpha;\n                ctx.strokeStyle = feedback.color;\n                ctx.lineWidth = 3 - progress * 1.5;\n                ctx.beginPath();\n                ctx.arc(feedback.x, feedback.y, radius, 0, Math.PI * 2);\n                ctx.stroke();\n\n                ctx.fillStyle = feedback.color;\n                ctx.globalAlpha = alpha * 0.25;\n                ctx.beginPath();\n                ctx.arc(feedback.x, feedback.y, 7, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.restore();\n            }\n\n            const TRAY = { tabs: { x: 40, y: 944, width: 205, height: 64, gap: 12 }, cards: { x: 40, y: 1016, width: 152, height: 94, gap: 8 }, controls: { x: 28, y: 1134, width: 154, height: 46, gap: 10 } };\n            const PRACTICE_LEVELS = [\n                {\n                    title: \"Reading Nook Setup\",\n                    instruction: \"Shelve the hardcovers, leave the notebook on the desk, and place the plant on the top shelf.\",\n                    complete: \"Lovely reading nook! Tap the Level badge for a different challenge.\",\n                    tasks: [\"Books shelved\", \"Notebook on desk\", \"Plant on top shelf\"]\n                },\n                {\n                    title: \"Desk Zone Practice\",\n                    instruction: \"Move the cup and glasses left, the lamp and mug right, and leave the middle of the desk clear.\",\n                    complete: \"A perfectly zoned desk! Tap the Level badge for a different challenge.\",\n                    tasks: [\"Cup & glasses left\", \"Lamp & mug right\", \"Center desk clear\"]\n                },\n                {\n                    title: \"Color-Coordinated Shelves\",\n                    instruction: \"Group sage and teal books on the upper shelf, terracotta and plum books on the lower shelf, and the plant in the middle.\",\n                    complete: \"Beautifully color-coordinated! Tap the Level badge to practise again.\",\n                    tasks: [\"Sage + teal \u2192 upper\", \"Terracotta + plum \u2192 lower\", \"Plant \u2192 middle shelf\"]\n                }\n            ];\n\n            async function run(mode) {\n                const isEditMode = mode === \"edit\";\n                const gameWorld = document.getElementById(\"game-world\");\n                const canvas = document.getElementById(\"game-canvas\");\n\n                if (!gameWorld || !canvas) {\n                    return;\n                }\n\n                if (activeApp) {\n                    activeApp.cleanup();\n                }\n\n                const ctx = canvas.getContext(\"2d\");\n                if (!ctx) {\n                    return;\n                }\n\n                // Keep the large, mostly static room artwork in a 1x offscreen layer.\n                // Reusing it each frame avoids rebuilding gradients, paths, and shadows on mobile.\n                const staticLayerCanvas = document.createElement(\"canvas\");\n                staticLayerCanvas.width = SCENE_WIDTH;\n                staticLayerCanvas.height = SCENE_HEIGHT;\n                const staticLayerContext = staticLayerCanvas.getContext(\"2d\");\n                let staticLayerKey = \"\";\n\n                ensureGameConfig();\n\n                const assetImages = {};\n                const host = window.lib;\n                [\"cozy_study_backdrop\", \"hardcover_book\", \"pencil_cup\", \"desk_lamp\", \"closed_notebook\", \"small_potted_plant\", \"reading_glasses\", \"ceramic_mug\", \"halloween_pumpkin\", \"halloween_ghost\", \"halloween_bat_garland\"].forEach(function (id) {\n                    const asset = host && typeof host.getAsset === \"function\"\n                        ? host.getAsset(id)\n                        : null;\n                    if (!asset || !asset.url) {\n                        if (host && typeof host.log === \"function\") {\n                            host.log(\"Illustration asset unavailable: \" + id);\n                        }\n                        return;\n                    }\n                    const image = new Image();\n                    image.src = asset.url;\n                    assetImages[id] = image;\n                });\n\n                const app = {\n                    rafId: 0,\n                    listeners: [],\n                    cleanup: function () {\n                        if (this.rafId) {\n                            cancelAnimationFrame(this.rafId);\n                        }\n\n                        for (const listener of this.listeners) {\n                            listener.target.removeEventListener(\n                                listener.type,\n                                listener.handler,\n                                listener.options\n                            );\n                        }\n\n                        this.listeners.length = 0;\n\n                        if (activeApp === this) {\n                            activeApp = null;\n                        }\n                    }\n                };\n\n                activeApp = app;\n\n                let objects = [];\n                let selectedId = null;\n                let hoverId = null;\n                let activePointerId = null;\n                let dragState = null;\n                let feedback = null;\n                let previousTimestamp = null;\n                let activeCategory = \"Books\";\n                let currentLevel = 1;\n                let selectedCatalogType = null;\n                let trayPointer = null;\n                let undoStack = [];\n                let redoStack = [];\n                let persistenceReady = isEditMode;\n                let saveLoadPending = !isEditMode;\n                let audioContext = null;\n                const soundBuffers = {};\n                let muted = false;\n\n                let sessionElapsed = 0;\n                const MIN_SESSION_SECONDS = 60;\n                let completionCelebrated = false;\n                let previousTaskChecks = [false, false, false];\n                let taskNotice = \"\";\n                let taskNoticeLife = 0;\n                function captureArrangement() { return cloneJson(objects); }\n                function sameArrangement(a, b) { return JSON.stringify(a) === JSON.stringify(b); }\n                function saveArrangement() {\n                    if (isEditMode || !persistenceReady || !host || typeof host.saveUserGameState !== \"function\") return;\n                    Promise.resolve(host.saveUserGameState({ version: 3, level: currentLevel, items: captureArrangement() })).catch(function (error) { if (host.log) host.log(\"Arrangement save unavailable: \" + (error && error.message || error)); });\n                }\n                function recordChange(before) {\n                    if (!before || sameArrangement(before, objects)) return;\n                    undoStack.push(before);\n                    if (undoStack.length > 60) undoStack.shift();\n                    redoStack.length = 0;\n                    saveArrangement();\n                }\n                function applyArrangement(snapshot) {\n                    objects = cloneJson(snapshot || []).filter(function (item) { return item && ITEM_TYPES[item.type]; });\n                    if (isEditMode) window.gameConfig.starting_items = cloneJson(objects);\n                    selectedId = null;\n                }\n                function persistEditArrangement() { if (isEditMode) window.gameConfig.starting_items = cloneJson(objects); }\n                function undoChange() { if (!undoStack.length) return; redoStack.push(captureArrangement()); applyArrangement(undoStack.pop()); saveArrangement(); setFeedback(360, 908, \"#78917b\"); }\n                function redoChange() { if (!redoStack.length) return; undoStack.push(captureArrangement()); applyArrangement(redoStack.pop()); saveArrangement(); setFeedback(360, 908, \"#78917b\"); }\n                function resetArrangement() {\n                    const before = captureArrangement();\n                    const starter = buildLevelItems(currentLevel);\n                    if (sameArrangement(before, starter)) return;\n                    applyArrangement(starter);\n                    previousTaskChecks = tidyChecklist();\n                    taskNotice = \"\";\n                    taskNoticeLife = 0;\n                    completionCelebrated = false;\n                    recordChange(before);\n                }\n                function changePracticeLevel() {\n                    if (isEditMode) return;\n                    currentLevel = currentLevel % PRACTICE_LEVELS.length + 1;\n                    objects = buildLevelItems(currentLevel);\n                    selectedId = null;\n                    selectedCatalogType = null;\n                    undoStack.length = 0;\n                    redoStack.length = 0;\n                    completionCelebrated = false;\n                    previousTaskChecks = tidyChecklist();\n                    taskNotice = \"\";\n                    taskNoticeLife = 0;\n                    feedback = null;\n                    saveArrangement();\n                }\n                function loadArrangement() {\n                    if (isEditMode || !host || typeof host.getUserGameState !== \"function\") { persistenceReady = true; saveLoadPending = false; return; }\n                    let hasSavedArrangement = false;\n                    Promise.resolve(host.getUserGameState()).then(function (result) {\n                        const state = result && result.state;\n                        if (state && Number.isFinite(Number(state.level))) currentLevel = Math.round(clamp(Number(state.level), 1, PRACTICE_LEVELS.length));\n                        if (state && Array.isArray(state.items)) {\n                            hasSavedArrangement = true;\n                            objects = state.items.slice(0, 40).filter(function (item) { return item && ITEM_TYPES[item.type] && typeof item.id === \"string\"; }).map(function (item) {\n                                return { id: item.id, type: item.type, surface: String(item.surface || \"desk_top\"), x: numericValue(item.x, 360), y: numericValue(item.y, 760), scale: clamp(numericValue(item.scale, 1), 0.75, 1.25), tint: item.tint || \"sage\", rotation: numericValue(item.rotation, 0) };\n                            });\n                            if (numericValue(state.version, 1) < 2) {\n                                DEFAULT_CONFIG.starting_items.filter(function (starter) { return starter.type.indexOf(\"halloween_\") === 0; }).forEach(function (starter) {\n                                    if (objects.length < 40 && !objects.some(function (item) { return item.id === starter.id; })) objects.push(cloneJson(starter));\n                                });\n                            }\n                            objects.forEach(function (item) { const zone = getZoneById(item.surface) || getZoneById(\"desk_top\"); if (zone) { item.surface = zone.id; const pos = snapToZone(item, zone, item.x, item.y); item.x = pos.x; item.y = pos.y; } });\n                        }\n                        if (!hasSavedArrangement) objects = buildLevelItems(currentLevel);\n                    }).catch(function (error) { if (host && host.log) host.log(\"Arrangement load unavailable: \" + (error && error.message || error)); if (!hasSavedArrangement) objects = buildLevelItems(currentLevel); }).finally(function () {\n                        // Treat the restored/starter arrangement as the baseline. Only announce\n                        // a task when the player completes it after entering the scene.\n                        previousTaskChecks = tidyChecklist();\n                        taskNotice = \"\";\n                        taskNoticeLife = 0;\n                        persistenceReady = true;\n                        saveLoadPending = false;\n                    });\n                }\n                function loadSounds() {\n                    if (!host || typeof host.getAsset !== \"function\") return;\n                    [\"paper_slide_sfx\", \"gentle_place_sfx\", \"completion_applause\"].forEach(function (id) {\n                        const asset = host.getAsset(id);\n                        if (!asset || !asset.url) return;\n                        fetch(asset.url).then(function (response) { return response.arrayBuffer(); }).then(function (data) {\n                            const AudioCtor = window.AudioContext || window.webkitAudioContext;\n                            if (!AudioCtor) return;\n                            if (!audioContext) audioContext = new AudioCtor();\n                            return audioContext.decodeAudioData(data).then(function (buffer) {\n                                soundBuffers[id] = buffer;\n                                // If the minute finished before this asset decoded, celebrate now.\n                                if (id === \"completion_applause\" && completionCelebrated) playSound(id);\n                            });\n                        }).catch(function () {});\n                    });\n                }\n                function playSound(id) {\n                    if (muted || !window.gameConfig.catalog_settings.sound_enabled || !soundBuffers[id]) return;\n                    const AudioCtor = window.AudioContext || window.webkitAudioContext;\n                    if (!AudioCtor) return;\n                    try {\n                        if (!audioContext) audioContext = new AudioCtor();\n                        if (audioContext.state === \"suspended\") audioContext.resume();\n                        const source = audioContext.createBufferSource(); const gain = audioContext.createGain();\n                        source.buffer = soundBuffers[id]; gain.gain.value = clamp(numericValue(window.gameConfig.catalog_settings.sound_volume, 0.35), 0, 1);\n                        source.connect(gain); gain.connect(audioContext.destination); source.start(0);\n                    } catch (error) { /* Sound is optional; interaction remains available. */ }\n                }\n                function enabledCatalogTypes() { return window.gameConfig.catalog_settings.enabled_types; }\n                function visibleCatalogTypes() { return (CATALOG_CATEGORIES[activeCategory] || []).filter(function (type) { return enabledCatalogTypes().indexOf(type) !== -1; }); }\n                function selectedItem() { return objects.find(function (item) { return item.id === selectedId; }) || null; }\n                function pointInRect(point, rect) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; }\n                function trayRect(section, index) {\n                    const base = section === \"tabs\" ? {x:40,y:944,width:150,height:58,gap:8} : {x:40,y:1016,width:152,height:94,gap:8};\n                    return {x:base.x + index * (base.width + base.gap), y:base.y, width:base.width, height:base.height};\n                }\n                function stackCountAt(item, x, y, ignoredId) {\n                    if (item.type !== \"hardcover_book\" || item.surface !== \"desk_top\") return 0;\n                    return objects.filter(function (other) { return other.id !== ignoredId && other.type === \"hardcover_book\" && other.surface === \"desk_top\" && Math.abs(other.x - x) <= 24 && Math.abs(other.y - y) <= 12; }).length;\n                }\n                function canStack(item, x, y, ignoredId) { return stackCountAt(item, x, y, ignoredId) < Math.round(clamp(numericValue(window.gameConfig.catalog_settings.stack_limit, 4), 1, 4)); }\n                function stackLayer(item) {\n                    if (item.type !== \"hardcover_book\" || item.surface !== \"desk_top\") return 0;\n                    const members = objects.filter(function (other) { return other.type === item.type && other.surface === item.surface && Math.abs(other.x - item.x) <= 24 && Math.abs(other.y - item.y) <= 12; }).sort(function (a,b) { return objects.indexOf(a)-objects.indexOf(b); });\n                    return Math.max(0, members.indexOf(item));\n                }\n                function visualItem(item) { const shown=Object.assign({},item); if(item.type===\"hardcover_book\"&&item.surface===\"desk_top\")shown.y-=stackLayer(item)*13; return shown; }\n                function isBookType(type) { return type === \"hardcover_book\" || type === \"closed_notebook\"; }\n                function tidyChecklist() {\n                    const desk = getZoneById(\"desk_top\");\n                    const hardcovers = objects.filter(function (item) { return item.type === \"hardcover_book\"; });\n                    const notebooks = objects.filter(function (item) { return item.type === \"closed_notebook\"; });\n                    const plants = objects.filter(function (item) { return item.type === \"small_potted_plant\"; });\n                    const booksShelved = hardcovers.length > 0 && hardcovers.every(function (item) { return item.surface !== \"desk_top\"; });\n                    const notebookOnDesk = notebooks.length > 0 && notebooks.every(function (item) { return item.surface === \"desk_top\"; });\n                    const plantOnUpperShelf = plants.length > 0 && plants.every(function (item) { return item.surface === \"upper_shelf\"; });\n\n                    const leftTypes = [\"pencil_cup\", \"reading_glasses\"];\n                    const rightTypes = [\"desk_lamp\", \"ceramic_mug\"];\n                    const leftBoundary = desk ? desk.x + desk.width * 0.34 : 257;\n                    const rightBoundary = desk ? desk.x + desk.width * 0.66 : 463;\n                    const leftItems = objects.filter(function (item) { return leftTypes.indexOf(item.type) !== -1; });\n                    const rightItems = objects.filter(function (item) { return rightTypes.indexOf(item.type) !== -1; });\n                    const leftZoneReady = !!desk && leftTypes.every(function (type) {\n                        return leftItems.some(function (item) { return item.type === type; });\n                    }) && leftItems.every(function (item) {\n                        return item.surface === \"desk_top\" && item.x >= desk.x && item.x <= leftBoundary;\n                    });\n                    const rightZoneReady = !!desk && rightTypes.every(function (type) {\n                        return rightItems.some(function (item) { return item.type === type; });\n                    }) && rightItems.every(function (item) {\n                        return item.surface === \"desk_top\" && item.x >= rightBoundary && item.x <= desk.x + desk.width;\n                    });\n                    const workArea = desk ? { x: desk.x + desk.width * 0.36, y: desk.y + 28, width: desk.width * 0.31, height: desk.height - 56 } : null;\n                    const workspaceClear = !!workArea && objects.filter(function (item) { return item.surface === \"desk_top\"; }).every(function (item) {\n                        const dimensions = itemDimensions(item);\n                        return item.x + dimensions.width / 2 <= workArea.x || item.x - dimensions.width / 2 >= workArea.x + workArea.width || item.y + dimensions.height / 2 <= workArea.y || item.y - dimensions.height / 2 >= workArea.y + workArea.height;\n                    });\n\n                    if (currentLevel === 2) {\n                        return [leftZoneReady, rightZoneReady, workspaceClear];\n                    }\n\n                    if (currentLevel === 3) {\n                        const upperColorBooks = hardcovers.filter(function (item) { return item.tint === \"sage\" || item.tint === \"teal\"; });\n                        const lowerColorBooks = hardcovers.filter(function (item) { return item.tint === \"terracotta\" || item.tint === \"plum\"; });\n                        const warmPlant = plants.length > 0 && plants.every(function (item) { return item.surface === \"middle_shelf\"; });\n                        const coolBooksUpper = upperColorBooks.length >= 2 && upperColorBooks.every(function (item) { return item.surface === \"upper_shelf\"; });\n                        const warmBooksLower = lowerColorBooks.length >= 2 && lowerColorBooks.every(function (item) { return item.surface === \"lower_shelf\"; });\n                        return [coolBooksUpper, warmBooksLower, warmPlant];\n                    }\n\n                    return [booksShelved, notebookOnDesk, plantOnUpperShelf];\n                }\n                function drawTaskChecklistStrip(ctx) {\n                    if (isEditMode || saveLoadPending) return;\n                    const level = PRACTICE_LEVELS[currentLevel - 1];\n                    const checks = tidyChecklist();\n                    const cardY = 634;\n                    const cardWidth = 220;\n                    const cardHeight = 62;\n                    const gap = 8;\n                    const startX = 22;\n\n                    ctx.save();\n                    for (let index = 0; index < level.tasks.length; index += 1) {\n                        const x = startX + index * (cardWidth + gap);\n                        const complete = Boolean(checks[index]);\n                        ctx.shadowColor = \"rgba(49, 41, 29, 0.18)\";\n                        ctx.shadowBlur = 8;\n                        ctx.shadowOffsetY = 2;\n                        fillRoundRect(ctx, x, cardY, cardWidth, cardHeight, 12, complete ? \"#edf4e8\" : \"#fffaf0\");\n                        ctx.shadowColor = \"transparent\";\n                        ctx.shadowBlur = 0;\n                        ctx.shadowOffsetY = 0;\n                        strokeRoundRect(ctx, x, cardY, cardWidth, cardHeight, 12, complete ? \"#638365\" : \"#b9a983\", 1.5);\n\n                        const checkX = x + 21;\n                        const checkY = cardY + cardHeight / 2;\n                        ctx.beginPath();\n                        ctx.arc(checkX, checkY, 11, 0, Math.PI * 2);\n                        ctx.fillStyle = complete ? \"#5b7e5b\" : \"#fffdf7\";\n                        ctx.fill();\n                        ctx.lineWidth = 2;\n                        ctx.strokeStyle = complete ? \"#4d704f\" : \"#9a927f\";\n                        ctx.stroke();\n                        if (complete) {\n                            ctx.beginPath();\n                            ctx.moveTo(checkX - 5, checkY);\n                            ctx.lineTo(checkX - 1, checkY + 4);\n                            ctx.lineTo(checkX + 6, checkY - 5);\n                            ctx.strokeStyle = \"#ffffff\";\n                            ctx.lineWidth = 2.6;\n                            ctx.lineCap = \"round\";\n                            ctx.lineJoin = \"round\";\n                            ctx.stroke();\n                        }\n\n                        ctx.textAlign = \"left\";\n                        ctx.textBaseline = \"middle\";\n                        ctx.fillStyle = complete ? \"#3f6948\" : \"#514536\";\n                        ctx.font = '700 16px \"Poppins\", \"Inter\", Arial, sans-serif';\n                        const words = level.tasks[index].split(\" \");\n                        const lines = [];\n                        let line = \"\";\n                        const textWidth = cardWidth - 50;\n                        words.forEach(function (word) {\n                            const candidate = line ? line + \" \" + word : word;\n                            if (line && ctx.measureText(candidate).width > textWidth) {\n                                lines.push(line);\n                                line = word;\n                            } else {\n                                line = candidate;\n                            }\n                        });\n                        if (line) lines.push(line);\n                        const lineHeight = 19;\n                        const firstLineY = checkY - ((lines.length - 1) * lineHeight) / 2;\n                        lines.forEach(function (textLine, lineIndex) {\n                            ctx.fillText(textLine, x + 40, firstLineY + lineIndex * lineHeight, textWidth);\n                        });\n                    }\n                    ctx.restore();\n                }\n                function createCatalogItem(type, zone, x, y) {\n                    if (!type || !zone || objects.length >= window.gameConfig.catalog_settings.max_item_count) return null;\n                    const before = captureArrangement();\n                    const item = { id: \"item_\" + Date.now() + \"_\" + Math.floor(Math.random() * 10000), type: type, surface: zone.id, x: x, y: y, scale: clamp(numericValue(window.gameConfig.catalog_settings.item_scale, 1), 0.75, 1.25), tint: type === \"hardcover_book\" ? window.gameConfig.catalog_settings.default_book_tint : \"cream\", rotation: 0 };\n                    const snapped = snapToZone(item, zone, x, y); item.x = snapped.x; item.y = snapped.y;\n                    if (!canStack(item, item.x, item.y, null)) { setFeedback(item.x, item.y, \"#b88763\"); return null; }\n                    objects.push(item); selectedId = item.id; selectedCatalogType = null; syncItemToConfig(item); persistEditArrangement(); setFeedback(item.x, item.y, \"#658b6c\"); playSound(\"gentle_place_sfx\"); recordChange(before); return item;\n                }\n                function duplicateSelected() {\n                    const source = selectedItem(); if (!source || objects.length >= window.gameConfig.catalog_settings.max_item_count) return;\n                    const before = captureArrangement(); const copy = Object.assign({}, source, { id: \"item_\" + Date.now() + \"_\" + Math.floor(Math.random() * 10000) });\n                    const zone = getZoneById(copy.surface); const pos = snapToZone(copy, zone, copy.x + 48, copy.y); copy.x = pos.x; copy.y = pos.y;\n                    if (!canStack(copy, copy.x, copy.y, null)) { setFeedback(copy.x, copy.y, \"#b88763\"); return; }\n                    objects.push(copy); selectedId = copy.id; syncItemToConfig(copy); persistEditArrangement(); setFeedback(copy.x, copy.y, \"#658b6c\"); playSound(\"gentle_place_sfx\"); recordChange(before);\n                }\n                function removeSelected() {\n                    const item = selectedItem(); if (!item) return; const before = captureArrangement(); objects = objects.filter(function (entry) { return entry.id !== item.id; });\n                    if (isEditMode) window.gameConfig.starting_items = window.gameConfig.starting_items.filter(function (entry) { return entry.id !== item.id; });\n                    selectedId = null; persistEditArrangement(); recordChange(before);\n                }\n                function activateTrayControl(label) {\n                    const item = selectedItem();\n                    if (label === \"Rotate\" && item) { const before = captureArrangement(); item.rotation = (numericValue(item.rotation, 0) + 15) % 360; syncItemToConfig(item); recordChange(before); }\n                    else if (label === \"Duplicate\") duplicateSelected(); else if (label === \"Remove\") removeSelected();\n                    else if (label === \"Place\" && selectedCatalogType) { const zone = getZoneById(\"desk_top\"); createCatalogItem(selectedCatalogType, zone, zone.x + zone.width / 2, zone.y + zone.height / 2); }\n                    else if (label === \"Undo\") undoChange(); else if (label === \"Redo\") redoChange(); else if (label === \"Reset\") resetArrangement(); else if (label === \"Sound\") muted = !muted;\n                }\n                function drawCatalogTray(ctx) {\n                    ctx.save();\n                    ctx.shadowColor = \"rgba(70, 49, 30, 0.2)\";\n                    ctx.shadowBlur = 24;\n                    ctx.shadowOffsetY = -6;\n                    const trayGradient = ctx.createLinearGradient(0, 920, 0, 1270);\n                    trayGradient.addColorStop(0, \"#fffdf6\");\n                    trayGradient.addColorStop(0.55, \"#f5eddd\");\n                    trayGradient.addColorStop(1, \"#e8dcc4\");\n                    fillRoundRect(ctx, 20, 920, 680, 352, 26, trayGradient);\n                    ctx.restore();\n                    strokeRoundRect(ctx,20,920,680,352,26,\"#b39350\",2);\n                    strokeRoundRect(ctx,27,927,666,338,21,\"rgba(225, 204, 157, 0.8)\",1);\n                    ctx.textAlign=\"left\";ctx.textBaseline=\"middle\";ctx.fillStyle=\"#765f43\";ctx.font='700 14px \"Poppins\", \"Inter\", Arial, sans-serif';ctx.fillText(isEditMode ? \"STUDENT STUDY SPACE \u00b7 EDIT LAYOUT\" : \"LEVEL \" + currentLevel + \" \u00b7 \" + PRACTICE_LEVELS[currentLevel - 1].title.toUpperCase(),42,935,475);\n                    if (!isEditMode) {\n                        const checks = tidyChecklist();\n                        const readyCount = checks.filter(Boolean).length;\n                        ctx.textAlign = \"right\";\n                        ctx.fillStyle = readyCount === checks.length ? \"#4f7655\" : \"#765f43\";\n                        ctx.font = '700 14px \"Poppins\", \"Inter\", Arial, sans-serif';\n                        ctx.fillText(readyCount + \" / \" + checks.length + \" TASKS COMPLETE\", 678, 935);\n                    }\n                    Object.keys(CATALOG_CATEGORIES).forEach(function(category,index){const r=trayRect(\"tabs\",index),on=category===activeCategory;fillRoundRect(ctx,r.x,r.y,r.width,r.height,16,on?\"#eeeadb\":\"#f6f0e5\");strokeRoundRect(ctx,r.x,r.y,r.width,r.height,16,on?\"#a77d32\":\"#d8c9aa\",on?2.5:1.5);ctx.textAlign=\"center\";ctx.textBaseline=\"middle\";ctx.fillStyle=on?\"#5b492a\":\"#675846\";ctx.font='700 15px \"Poppins\", \"Inter\", Arial, sans-serif';ctx.fillText(category,r.x+r.width/2,r.y+r.height/2-9);ctx.font='600 15px \"Poppins\", \"Inter\", Arial, sans-serif';ctx.fillText(CATALOG_CATEGORY_LABELS[category]||'',r.x+r.width/2,r.y+r.height/2+11);});\n                    const types=visibleCatalogTypes().slice(0,4); types.forEach(function(type,index){const r=trayRect(\"cards\",index),on=type===selectedCatalogType,def=ITEM_TYPES[type];ctx.save();ctx.shadowColor=on?\"rgba(78,116,83,0.2)\":\"rgba(75,57,38,0.1)\";ctx.shadowBlur=on?12:6;ctx.shadowOffsetY=3;const cardGradient=ctx.createLinearGradient(0,r.y,0,r.y+r.height);cardGradient.addColorStop(0,on?\"#f5faf1\":\"#fffefa\");cardGradient.addColorStop(1,on?\"#e5eee1\":\"#f8f3ea\");fillRoundRect(ctx,r.x,r.y,r.width,r.height,15,cardGradient);ctx.restore();strokeRoundRect(ctx,r.x,r.y,r.width,r.height,15,on?\"#a77d32\":\"#ded1b8\",on?2.5:1.5);const scale=Math.min(.42,42/Math.max(def.width,def.height));drawItem(ctx,{id:\"preview_\"+type,type:type,x:r.x+r.width/2,y:r.y+31,scale:scale,tint:type===\"hardcover_book\"?window.gameConfig.catalog_settings.default_book_tint:\"sage\",rotation:0},{assets:assetImages});ctx.textAlign=\"center\";ctx.textBaseline=\"middle\";ctx.fillStyle=\"#514536\";ctx.font='600 12px \"Poppins\", \"Inter\", Arial, sans-serif';ctx.fillText(CATALOG_LABELS[type],r.x+r.width/2,r.y+65,r.width-8);ctx.font='600 12px \"Poppins\", \"Inter\", Arial, sans-serif';ctx.fillStyle=\"#85765f\";ctx.fillText(CATALOG_LABELS_ZH[type],r.x+r.width/2,r.y+82,r.width-8);});\n                    ctx.textAlign=\"left\";ctx.textBaseline=\"middle\";ctx.fillStyle=\"#766856\";ctx.font='500 14px \"Poppins\", \"Inter\", Arial, sans-serif';ctx.textAlign=\"center\";\n                    if (!isEditMode) {\n                        const checks = tidyChecklist();\n                        const complete = checks.every(Boolean);\n                        const level = PRACTICE_LEVELS[currentLevel - 1];\n                        ctx.font='600 13px \"Poppins\", \"Inter\", Arial, sans-serif';\n                        ctx.fillStyle = complete ? \"#4f7655\" : \"#514536\";\n                        ctx.fillText(complete ? level.complete : level.instruction,360,1112,650);\n                        ctx.fillStyle=\"#675b48\";\n                        ctx.fillText(\"Each task above gets a green checkmark when complete.\",360,1128,660);\n                    } else {\n                        ctx.font='500 13px \"Poppins\", \"Inter\", Arial, sans-serif';\n                        ctx.fillText(\"Select an item, then tap a surface or drag it into place.\",360,1112,650);\n                        ctx.fillText(\"Drag the objects to preview the study layout.\",360,1128,660);\n                    }\n                    [\"Rotate\",\"Duplicate\",\"Remove\",\"Place\",\"Undo\",\"Redo\",\"Reset\",muted?\"Sound off\":\"Sound on\"].forEach(function(label,index){const r={x:32+(index%4)*164,y:1140+Math.floor(index/4)*62,width:154,height:56};const disabled=((label===\"Rotate\"||label===\"Duplicate\"||label===\"Remove\")&&!selectedItem())||(label===\"Place\"&&!selectedCatalogType)||(label===\"Undo\"&&!undoStack.length)||(label===\"Redo\"&&!redoStack.length);fillRoundRect(ctx,r.x,r.y,r.width,r.height,14,disabled?\"#eee9df\":label===\"Place\"||label===\"Undo\"||label===\"Redo\"?\"#e7dfc8\":\"#f7f1e6\");strokeRoundRect(ctx,r.x,r.y,r.width,r.height,14,\"#cbb98f\",1.5);ctx.textAlign=\"center\";ctx.textBaseline=\"middle\";ctx.fillStyle=disabled?\"#a99f90\":\"#594b3b\";ctx.font='700 16px \"Poppins\", \"Inter\", Arial, sans-serif';ctx.font='700 13px \"Poppins\", \"Inter\", Arial, sans-serif';ctx.fillText(label,r.x+r.width/2,r.y+r.height/2-7);ctx.font='600 13px \"Poppins\", \"Inter\", Arial, sans-serif';ctx.fillText(BUTTON_LABELS_ZH[label]||\"\",r.x+r.width/2,r.y+r.height/2+10);});\n                }\n                function drawFooter(ctx) { drawCatalogTray(ctx); }\n\n                function drawTaskNotice(ctx) {\n                    if (isEditMode || taskNoticeLife <= 0 || tidyChecklist().every(Boolean)) return;\n                    const fade = Math.min(1, taskNoticeLife / 0.35);\n                    ctx.save();\n                    ctx.globalAlpha = fade;\n                    ctx.shadowColor = \"rgba(49, 67, 42, 0.28)\";\n                    ctx.shadowBlur = 18;\n                    ctx.shadowOffsetY = 5;\n                    const card = ctx.createLinearGradient(0, 628, 0, 716);\n                    card.addColorStop(0, \"#fffdf4\");\n                    card.addColorStop(1, \"#eee3c8\");\n                    fillRoundRect(ctx, 150, 632, 420, 82, 18, card);\n                    ctx.shadowColor = \"transparent\";\n                    strokeRoundRect(ctx, 150, 632, 420, 82, 18, \"#c6a45a\", 2.5);\n                    ctx.textAlign = \"center\";\n                    ctx.textBaseline = \"middle\";\n                    ctx.fillStyle = \"#3f6948\";\n                    ctx.font = '700 21px \"Poppins\", \"Inter\", Arial, sans-serif';\n                    ctx.fillText(taskNotice, 360, 660);\n                    ctx.fillStyle = \"#6b5a3f\";\n                    ctx.font = '500 14px \"Poppins\", \"Inter\", Arial, sans-serif';\n                    ctx.fillText(\"Great work! Check the task list below for what comes next.\", 360, 689, 395);\n                    ctx.restore();\n                }\n\n                function drawCompletionCelebration(ctx, elapsed) {\n                    if (isEditMode || !tidyChecklist().every(Boolean)) return;\n\n                    // Persistent on-canvas celebration: prominent, but not a blocking dialog.\n                    const pulse = 1 + Math.sin(elapsed * 3.2) * 0.012;\n                    ctx.save();\n                    ctx.translate(360, 674);\n                    ctx.scale(pulse, pulse);\n                    ctx.shadowColor = \"rgba(49, 67, 42, 0.34)\";\n                    ctx.shadowBlur = 24;\n                    ctx.shadowOffsetY = 8;\n                    const card = ctx.createLinearGradient(0, -49, 0, 49);\n                    card.addColorStop(0, \"#fffdf4\");\n                    card.addColorStop(1, \"#eee3c8\");\n                    fillRoundRect(ctx, -293, -49, 586, 98, 22, card);\n                    ctx.shadowColor = \"transparent\";\n                    strokeRoundRect(ctx, -293, -49, 586, 98, 22, \"#c6a45a\", 3);\n                    strokeRoundRect(ctx, -285, -41, 570, 82, 17, \"rgba(255,255,255,0.8)\", 1.5);\n\n                    ctx.textAlign = \"center\";\n                    ctx.textBaseline = \"middle\";\n                    ctx.fillStyle = \"#3f6948\";\n                    ctx.font = '700 25px \"Poppins\", \"Inter\", Arial, sans-serif';\n                    ctx.fillText(\"LEVEL \" + currentLevel + \" COMPLETE!\", 0, -12);\n                    ctx.fillStyle = \"#6b5a3f\";\n                    ctx.font = '500 15px \"Poppins\", \"Inter\", Arial, sans-serif';\n                    ctx.fillText(currentLevel < PRACTICE_LEVELS.length ? \"Tap the Level badge to start the next practice.\" : \"Wonderful! Tap the Level badge to practise again.\", 0, 20);\n\n                    // Small warm starbursts make completion feel celebratory without hiding play.\n                    const sparkle = 0.72 + Math.sin(elapsed * 4.5) * 0.22;\n                    ctx.globalAlpha = sparkle;\n                    ctx.fillStyle = \"#c89a42\";\n                    [[-264, -22], [-244, 24], [264, -22], [244, 24]].forEach(function (point) {\n                        ctx.beginPath();\n                        ctx.arc(point[0], point[1], 3.2, 0, Math.PI * 2);\n                        ctx.fill();\n                        ctx.beginPath();\n                        ctx.moveTo(point[0] - 6, point[1]);\n                        ctx.lineTo(point[0] + 6, point[1]);\n                        ctx.moveTo(point[0], point[1] - 6);\n                        ctx.lineTo(point[0], point[1] + 6);\n                        ctx.lineWidth = 1.5;\n                        ctx.strokeStyle = \"#c89a42\";\n                        ctx.stroke();\n                    });\n                    ctx.restore();\n                }\n\n                const reducedMotion = Boolean(\n                    window.matchMedia &&\n                    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n                );\n\n                function addListener(target, type, handler, options) {\n                    target.addEventListener(type, handler, options);\n                    app.listeners.push({\n                        target: target,\n                        type: type,\n                        handler: handler,\n                        options: options\n                    });\n                }\n\n                function resizeCanvas() {\n                    // A modest backing resolution is noticeably cheaper on high-DPI phones,\n                    // while retaining crisp text and touch targets at the fixed 720px layout.\n                    const pixelRatio = Math.min(\n                        1.25,\n                        Math.max(1, Number(window.devicePixelRatio) || 1)\n                    );\n\n                    canvas.width = Math.round(SCENE_WIDTH * pixelRatio);\n                    canvas.height = Math.round(SCENE_HEIGHT * pixelRatio);\n                    ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);\n                    ctx.imageSmoothingEnabled = true;\n                }\n\n                function readZones() {\n                    const config = window.gameConfig;\n                    return ZONE_ORDER.map(function (id) {\n                        const zone = config.surface_zones.find(function (entry) {\n                            return entry && entry.id === id;\n                        });\n\n                        if (!zone) {\n                            return null;\n                        }\n\n                        return {\n                            id: zone.id,\n                            kind: zone.kind,\n                            x: numericValue(zone.x, 0),\n                            y: numericValue(zone.y, 0),\n                            width: numericValue(zone.width, 0),\n                            height: numericValue(zone.height, 0),\n                            baseline_y: numericValue(zone.baseline_y, 0)\n                        };\n                    }).filter(Boolean);\n                }\n\n                function getZoneById(id) {\n                    return readZones().find(function (zone) {\n                        return zone.id === id;\n                    }) || null;\n                }\n\n                function currentSnapStep() {\n                    return Math.round(\n                        clamp(numericValue(window.gameConfig.snap_step_px, 24), 12, 48)\n                    );\n                }\n\n                function snapAxis(value, origin, minimum, maximum, step) {\n                    if (maximum < minimum) {\n                        return (minimum + maximum) / 2;\n                    }\n\n                    const firstIndex = Math.ceil((minimum - origin) / step - 1e-8);\n                    const lastIndex = Math.floor((maximum - origin) / step + 1e-8);\n\n                    if (firstIndex <= lastIndex) {\n                        const desiredIndex = Math.round((value - origin) / step);\n                        const index = clamp(desiredIndex, firstIndex, lastIndex);\n                        return Math.round(origin + index * step);\n                    }\n\n                    return Math.round(clamp(value, minimum, maximum));\n                }\n\n                function snapToZone(item, zone, desiredX, desiredY) {\n                    const dimensions = itemDimensions(item);\n                    const halfWidth = dimensions.width / 2;\n                    const halfHeight = dimensions.height / 2;\n                    const step = currentSnapStep();\n\n                    const minimumX = zone.x + halfWidth;\n                    const maximumX = zone.x + zone.width - halfWidth;\n                    const x = snapAxis(\n                        desiredX,\n                        zone.x,\n                        minimumX,\n                        maximumX,\n                        step\n                    );\n\n                    if (zone.kind === \"shelf\") {\n                        return {\n                            x: x,\n                            y: Math.round(zone.baseline_y - halfHeight)\n                        };\n                    }\n\n                    const minimumY = zone.y + halfHeight;\n                    const maximumY = zone.y + zone.height - halfHeight;\n                    const y = snapAxis(\n                        desiredY,\n                        zone.y,\n                        minimumY,\n                        maximumY,\n                        step\n                    );\n\n                    return { x: x, y: y };\n                }\n\n                function findZoneAtPoint(x, y, zones) {\n                    const candidates = zones || readZones();\n                    return candidates.find(function (zone) {\n                        return (\n                            x >= zone.x &&\n                            x <= zone.x + zone.width &&\n                            y >= zone.y &&\n                            y <= zone.y + zone.height\n                        );\n                    }) || null;\n                }\n\n                function syncItemToConfig(item) {\n                    if (!isEditMode) {\n                        return;\n                    }\n\n                    const list = window.gameConfig.starting_items;\n                    let entry = list.find(function (candidate) {\n                        return candidate && candidate.id === item.id;\n                    });\n\n                    if (!entry) {\n                        entry = {\n                            id: item.id,\n                            type: item.type,\n                            surface: item.surface,\n                            x: Math.round(item.x),\n                            y: Math.round(item.y),\n                            scale: item.scale,\n                            tint: item.tint,\n                            rotation: numericValue(item.rotation, 0)\n                        };\n                        list.push(entry);\n                        return;\n                    }\n\n                    entry.surface = item.surface;\n                    entry.x = Math.round(item.x);\n                    entry.y = Math.round(item.y);\n                    entry.scale = clamp(numericValue(item.scale, 1), 0.75, 1.25);\n                    entry.rotation = numericValue(item.rotation, 0);\n                }\n\n                function reprojectItems() {\n                    for (const item of objects) {\n                        const zone = getZoneById(item.surface);\n                        if (!zone) {\n                            continue;\n                        }\n\n                        const position = snapToZone(item, zone, item.x, item.y);\n                        item.x = position.x;\n                        item.y = position.y;\n                        syncItemToConfig(item);\n                    }\n                }\n\n                function setSnapSpacing(value) {\n                    const step = Math.round(clamp(numericValue(value, 24), 12, 48));\n                    window.gameConfig.snap_step_px = step;\n                    reprojectItems();\n                }\n\n                function setZoneParameter(zoneId, field, value) {\n                    const zone = window.gameConfig.surface_zones.find(function (entry) {\n                        return entry && entry.id === zoneId;\n                    });\n\n                    if (!zone) {\n                        return;\n                    }\n\n                    const nextValue = numericValue(value, zone[field]);\n\n                    if (field === \"baseline_y\" && SHELF_LIMITS[zoneId]) {\n                        const limits = SHELF_LIMITS[zoneId];\n                        zone.baseline_y = Math.round(\n                            clamp(nextValue, limits.baselineMin, limits.baselineMax)\n                        );\n                    } else if (field === \"x\" && zoneId === \"desk_top\") {\n                        zone.x = Math.round(clamp(nextValue, 38, 78));\n                    } else if (field === \"y\" && zoneId === \"desk_top\") {\n                        zone.y = Math.round(clamp(nextValue, 700, 760));\n                    } else if (field === \"width\" && zoneId === \"desk_top\") {\n                        zone.width = Math.round(clamp(nextValue, 520, 644));\n                    } else if (field === \"height\" && zoneId === \"desk_top\") {\n                        zone.height = Math.round(clamp(nextValue, 152, 230));\n                    } else if (field === \"x\" && SHELF_LIMITS[zoneId]) {\n                        zone.x = Math.round(clamp(nextValue, 72, 120));\n                    } else if (field === \"y\" && SHELF_LIMITS[zoneId]) {\n                        zone.y = Math.round(nextValue);\n                    } else if (field === \"width\" && SHELF_LIMITS[zoneId]) {\n                        zone.width = Math.round(clamp(nextValue, 432, 576));\n                    }\n\n                    normalizeSurfaceZones(window.gameConfig);\n                    reprojectItems();\n                    feedback = null;\n                }\n\n                function addSlider(params, label, key, minimum, maximum, step, onChange) {\n                    params[label] = {\n                        key: key,\n                        type: \"slider\",\n                        min: minimum,\n                        max: maximum,\n                        step: step,\n                        onChange: onChange\n                    };\n                }\n\n                function showParameters() {\n                    const host = window.lib;\n                    if (!host || typeof host.showGameParameters !== \"function\") return;\n\n                    const params = {};\n\n                    addSlider(\n                        params,\n                        \"Snap spacing \u683c\u7dda\u9593\u8ddd (px)\",\n                        \"snap_step_px\",\n                        12,\n                        48,\n                        1,\n                        setSnapSpacing\n                    );\n                    addSlider(params, \"Catalog item scale \u76ee\u9304\u7269\u4ef6\u5927\u5c0f\", \"catalog_settings.item_scale\", 0.75, 1.25, 0.05, function(value) {\n                        window.gameConfig.catalog_settings.item_scale = clamp(numericValue(value, 1), 0.75, 1.25);\n                    });\n                    addSlider(params, \"Maximum item count \u7269\u4ef6\u6578\u4e0a\u9650\", \"catalog_settings.max_item_count\", 8, 40, 1, function(value) { window.gameConfig.catalog_settings.max_item_count = Math.round(clamp(numericValue(value, 24), 8, 40)); });\n                    addSlider(params, \"Desk book stack limit \u684c\u9762\u758a\u66f8\u4e0a\u9650\", \"catalog_settings.stack_limit\", 1, 4, 1, function(value) { window.gameConfig.catalog_settings.stack_limit = Math.round(clamp(numericValue(value, 4), 1, 4)); });\n                    params[\"Placement sounds \u653e\u7f6e\u97f3\u6548\"] = { key: \"catalog_settings.sound_enabled\", type: \"checkbox\", onChange: function(value) { window.gameConfig.catalog_settings.sound_enabled = Boolean(value); } };\n                    addSlider(params, \"Sound volume \u97f3\u91cf\", \"catalog_settings.sound_volume\", 0, 1, 0.05, function(value) { window.gameConfig.catalog_settings.sound_volume = clamp(numericValue(value, 0.35), 0, 1); });\n                    params[\"Starter book color \u521d\u59cb\u66f8\u672c\u984f\u8272\"] = { key: \"catalog_settings.default_book_tint\", type: \"dropdown\", options: window.gameConfig.catalog_settings.palette.map(function(tint) { return { label: tint.charAt(0).toUpperCase() + tint.slice(1), value: tint }; }), onChange: function(value) { window.gameConfig.catalog_settings.default_book_tint = value; } };\n                    window.gameConfig.catalog_settings.palette.forEach(function(tint,index) { params[\"Palette color \" + (index + 1)] = { key: \"catalog_settings.palette.\" + index, type: \"dropdown\", options: Object.keys(BOOK_COLORS).map(function(color) { return {label:color.charAt(0).toUpperCase()+color.slice(1),value:color}; }) }; });\n                    for (const type of Object.keys(ITEM_TYPES)) {\n                        params[\"Enable \" + (CATALOG_LABELS[type] || type)] = {\n                            key: \"catalog_settings.enabled_types\",\n                            type: \"checkbox\",\n                            onChange: function(enabled) {\n                                const list = window.gameConfig.catalog_settings.enabled_types;\n                                if (enabled && list.indexOf(type) === -1) list.push(type);\n                                if (!enabled) window.gameConfig.catalog_settings.enabled_types = list.filter(function(entry) { return entry !== type; });\n                                if (selectedCatalogType === type && !enabled) selectedCatalogType = null;\n                            }\n                        };\n                    };\n\n                    const shelfControls = [\n                        {\n                            id: \"upper_shelf\",\n                            label: \"Upper shelf \u4e0a\u5c64\u66f8\u67b6\",\n                            topMin: 166,\n                            topMax: 254,\n                            baselineMin: 314,\n                            baselineMax: 322\n                        },\n                        {\n                            id: \"middle_shelf\",\n                            label: \"Middle shelf \u4e2d\u5c64\u66f8\u67b6\",\n                            topMin: 336,\n                            topMax: 399,\n                            baselineMin: 459,\n                            baselineMax: 467\n                        },\n                        {\n                            id: \"lower_shelf\",\n                            label: \"Lower shelf \u4e0b\u5c64\u66f8\u67b6\",\n                            topMin: 481,\n                            topMax: 544,\n                            baselineMin: 604,\n                            baselineMax: 612\n                        }\n                    ];\n\n                    for (const control of shelfControls) {\n                        const index = window.gameConfig.surface_zones.findIndex(function (zone) {\n                            return zone && zone.id === control.id;\n                        });\n\n                        addSlider(\n                            params,\n                            control.label + \" left bound \u5de6\u754c\",\n                            \"surface_zones.\" + index + \".x\",\n                            72,\n                            120,\n                            4,\n                            function (value) {\n                                setZoneParameter(control.id, \"x\", value);\n                            }\n                        );\n\n                        addSlider(\n                            params,\n                            control.label + \" drop-area top \u653e\u7f6e\u5340\u9802\u90e8\",\n                            \"surface_zones.\" + index + \".y\",\n                            control.topMin,\n                            control.topMax,\n                            4,\n                            function (value) {\n                                setZoneParameter(control.id, \"y\", value);\n                            }\n                        );\n\n                        addSlider(\n                            params,\n                            control.label + \" width \u5bec\u5ea6\",\n                            \"surface_zones.\" + index + \".width\",\n                            432,\n                            576,\n                            4,\n                            function (value) {\n                                setZoneParameter(control.id, \"width\", value);\n                            }\n                        );\n\n                        addSlider(\n                            params,\n                            control.label + \" baseline \u5e95\u7dda\",\n                            \"surface_zones.\" + index + \".baseline_y\",\n                            control.baselineMin,\n                            control.baselineMax,\n                            2,\n                            function (value) {\n                                setZoneParameter(control.id, \"baseline_y\", value);\n                            }\n                        );\n                    }\n\n                    const deskIndex = window.gameConfig.surface_zones.findIndex(function (zone) {\n                        return zone && zone.id === \"desk_top\";\n                    });\n\n                    addSlider(\n                        params,\n                        \"Desktop left bound \u66f8\u684c\u5de6\u754c\",\n                        \"surface_zones.\" + deskIndex + \".x\",\n                        38,\n                        78,\n                        4,\n                        function (value) {\n                            setZoneParameter(\"desk_top\", \"x\", value);\n                        }\n                    );\n\n                    addSlider(\n                        params,\n                        \"Desktop top bound \u66f8\u684c\u4e0a\u754c\",\n                        \"surface_zones.\" + deskIndex + \".y\",\n                        700,\n                        760,\n                        4,\n                        function (value) {\n                            setZoneParameter(\"desk_top\", \"y\", value);\n                        }\n                    );\n\n                    addSlider(\n                        params,\n                        \"Desktop width \u66f8\u684c\u5bec\u5ea6\",\n                        \"surface_zones.\" + deskIndex + \".width\",\n                        520,\n                        644,\n                        4,\n                        function (value) {\n                            setZoneParameter(\"desk_top\", \"width\", value);\n                        }\n                    );\n\n                    addSlider(\n                        params,\n                        \"Desktop height \u66f8\u684c\u9ad8\u5ea6\",\n                        \"surface_zones.\" + deskIndex + \".height\",\n                        152,\n                        230,\n                        4,\n                        function (value) {\n                            setZoneParameter(\"desk_top\", \"height\", value);\n                        }\n                    );\n\n                    try {\n                        host.showGameParameters({\n                            name: \"Desk Tidy Challenge\",\n                            params: params\n                        });\n                    } catch (error) {\n                        if (typeof host.log === \"function\") {\n                            host.log(\"Unable to show scene parameters: \" + error.message);\n                        }\n                    }\n                }\n\n                function buildRuntimeItems() {\n                    const list = window.gameConfig.starting_items;\n                    const result = [];\n\n                    list.forEach(function (entry, index) {\n                        if (!entry || !ITEM_TYPES[entry.type]) {\n                            return;\n                        }\n\n                        const item = {\n                            id: String(entry.id || (\"starter_\" + (index + 1))),\n                            type: entry.type,\n                            surface: String(entry.surface || \"desk_top\"),\n                            x: numericValue(entry.x, 360),\n                            y: numericValue(entry.y, 760),\n                            scale: clamp(numericValue(entry.scale, 1), 0.75, 1.25),\n                            tint: entry.tint || \"sage\",\n                            rotation: numericValue(entry.rotation, 0)\n                        };\n\n                        const zone = getZoneById(item.surface) || getZoneById(\"desk_top\");\n                        if (!zone) {\n                            return;\n                        }\n\n                        item.surface = zone.id;\n                        const position = snapToZone(item, zone, item.x, item.y);\n                        item.x = position.x;\n                        item.y = position.y;\n\n                        result.push(item);\n                    });\n\n                    return result;\n                }\n\n                function buildLevelItems(level) {\n                    const result = cloneJson(buildRuntimeItems());\n                    const desk = getZoneById(\"desk_top\");\n                    if (!desk) return result;\n                    function place(id, surface, x, y) {\n                        const item = result.find(function (entry) { return entry.id === id; });\n                        const zone = getZoneById(surface);\n                        if (!item || !zone) return;\n                        const position = snapToZone(item, zone, x, y);\n                        item.surface = zone.id;\n                        item.x = position.x;\n                        item.y = position.y;\n                    }\n                    if (level === 1) {\n                        place(\"book_01\", \"desk_top\", 174, 772);\n                        place(\"book_02\", \"desk_top\", 282, 796);\n                        place(\"book_03\", \"desk_top\", 438, 772);\n                        place(\"book_04\", \"desk_top\", 546, 796);\n                        // Start the notebook on a shelf so this desk-placement task is not already done.\n                        place(\"notebook_01\", \"lower_shelf\", 552, 560);\n                        place(\"plant_01\", \"middle_shelf\", 552, 422);\n                    } else if (level === 2) {\n                        place(\"book_01\", \"upper_shelf\", 190, 269);\n                        place(\"book_02\", \"upper_shelf\", 270, 269);\n                        place(\"book_03\", \"middle_shelf\", 350, 414);\n                        place(\"book_04\", \"middle_shelf\", 430, 414);\n                        place(\"notebook_01\", \"lower_shelf\", 552, 560);\n                        place(\"plant_01\", \"upper_shelf\", 552, 269);\n                        place(\"glasses_01\", \"desk_top\", 302, 796);\n                        place(\"pencil_cup_01\", \"desk_top\", 398, 796);\n                        // Keep the mug out of the right-hand zone until the player sorts it.\n                        place(\"mug_01\", \"desk_top\", 422, 796);\n                        place(\"lamp_01\", \"desk_top\", 602, 772);\n                    } else {\n                        place(\"book_01\", \"desk_top\", 210, 772);\n                        place(\"book_02\", \"desk_top\", 500, 772);\n                        place(\"book_03\", \"desk_top\", 270, 840);\n                        place(\"book_04\", \"desk_top\", 450, 840);\n                        place(\"notebook_01\", \"desk_top\", 360, 824);\n                        place(\"plant_01\", \"upper_shelf\", 552, 269);\n                    }\n                    return result;\n                }\n\n                // Start play mode from the selected challenge's deliberately unsolved layout.\n                // Edit mode continues to use the creator-authored arrangement.\n                objects = isEditMode ? buildRuntimeItems() : buildLevelItems(currentLevel);\n                previousTaskChecks = isEditMode ? [false, false, false] : tidyChecklist();\n\n                if (isEditMode) {\n                    for (const item of objects) {\n                        syncItemToConfig(item);\n                    }\n                }\n\n                function pointerToScene(event) {\n                    const rect = canvas.getBoundingClientRect();\n                    const width = rect.width || SCENE_WIDTH;\n                    const height = rect.height || SCENE_HEIGHT;\n\n                    return {\n                        x: (event.clientX - rect.left) * (SCENE_WIDTH / width),\n                        y: (event.clientY - rect.top) * (SCENE_HEIGHT / height)\n                    };\n                }\n\n                function itemAtPoint(x, y) {\n                    for (let index = objects.length - 1; index >= 0; index -= 1) {\n                        const item = objects[index];\n                        const dimensions = itemDimensions(item);\n                        const hitWidth = Math.max(96, dimensions.width);\n                        const hitHeight = Math.max(96, dimensions.height);\n\n                        if (\n                            Math.abs(x - item.x) <= hitWidth / 2 &&\n                            Math.abs(y - item.y) <= hitHeight / 2\n                        ) {\n                            return item;\n                        }\n                    }\n\n                    return null;\n                }\n\n                function focusCanvas() {\n                    try {\n                        canvas.focus({ preventScroll: true });\n                    } catch (error) {\n                        canvas.focus();\n                    }\n                }\n\n                function setFeedback(x, y, color) {\n                    if (reducedMotion) {\n                        feedback = null;\n                        return;\n                    }\n\n                    feedback = {\n                        x: x,\n                        y: y,\n                        life: 0.42,\n                        duration: 0.42,\n                        color: color\n                    };\n                }\n\n                function cancelActiveDrag() {\n                    if (!dragState) {\n                        activePointerId = null;\n                        return;\n                    }\n\n                    const item = objects.find(function (entry) {\n                        return entry.id === dragState.itemId;\n                    });\n\n                    if (item) {\n                        item.x = dragState.original.x;\n                        item.y = dragState.original.y;\n                        item.surface = dragState.original.surface;\n                    }\n\n                    const pointerId = activePointerId;\n                    activePointerId = null;\n                    dragState = null;\n\n                    if (pointerId !== null) {\n                        try {\n                            if (\n                                typeof canvas.hasPointerCapture === \"function\" &&\n                                canvas.hasPointerCapture(pointerId)\n                            ) {\n                                canvas.releasePointerCapture(pointerId);\n                            }\n                        } catch (error) {\n                            /* Pointer capture may already have been released. */\n                        }\n                    }\n\n                    canvas.style.cursor = \"default\";\n                }\n\n                function finishDrag(point) {\n                    if (!dragState) return;\n                    const currentDrag = dragState;\n                    const item = objects.find(function (entry) { return entry.id === currentDrag.itemId; });\n                    if (item) {\n                        item.x = point.x - currentDrag.offsetX; item.y = point.y - currentDrag.offsetY;\n                        const target = findZoneAtPoint(point.x, point.y);\n                        if (target) {\n                            const position = snapToZone(item, target, item.x, item.y);\n                            item.surface = target.id; item.x = position.x; item.y = position.y;\n                            if (!canStack(item, item.x, item.y, item.id)) {\n                                item.x=currentDrag.original.x; item.y=currentDrag.original.y; item.surface=currentDrag.original.surface;\n                                setFeedback(point.x,point.y,\"#b88763\");\n                            } else {\n                                syncItemToConfig(item); setFeedback(item.x,item.y,\"#658b6c\"); playSound(\"gentle_place_sfx\");\n                                if (currentDrag.moved) recordChange(currentDrag.before);\n                            }\n                            if (isEditMode) persistEditArrangement();\n                        } else {\n                            item.x=currentDrag.original.x; item.y=currentDrag.original.y; item.surface=currentDrag.original.surface; setFeedback(item.x,item.y,\"#b88763\");\n                        }\n                    }\n                    const pointerId=activePointerId; activePointerId=null; dragState=null;\n                    hoverId=itemAtPoint(point.x,point.y)?.id||null; canvas.style.cursor=hoverId?\"grab\":\"default\";\n                    if(pointerId!==null){try{if(typeof canvas.hasPointerCapture===\"function\"&&canvas.hasPointerCapture(pointerId))canvas.releasePointerCapture(pointerId);}catch(error){}}\n                }\n\n                function handlePointerDown(event) {\n                    if (event.button !== undefined && event.button !== 0) return;\n                    if (activePointerId !== null) { event.preventDefault(); return; }\n                    event.preventDefault();\n                    const point = pointerToScene(event);\n                    focusCanvas();\n                    if (!isEditMode && point.x >= 530 && point.x <= 672 && point.y >= 32 && point.y <= 86) {\n                        changePracticeLevel();\n                        return;\n                    }\n                    if (point.y >= 928 && point.y <= 1260) {\n                        for (let index = 0; index < 4; index += 1) {\n                            if (pointInRect(point, trayRect(\"tabs\", index))) {\n                                activeCategory = Object.keys(CATALOG_CATEGORIES)[index]; selectedCatalogType = null; selectedId = null; return;\n                            }\n                        }\n                        const types = visibleCatalogTypes().slice(0, 4);\n                        for (let index = 0; index < types.length; index += 1) {\n                            if (pointInRect(point, trayRect(\"cards\", index))) {\n                                selectedCatalogType = types[index]; selectedId = null;\n                                trayPointer = { pointerId: event.pointerId, type: types[index], moved: false, startX: point.x, startY: point.y, x: point.x, y: point.y };\n                                activePointerId = event.pointerId;\n                                try { canvas.setPointerCapture(event.pointerId); } catch (error) {}\n                                return;\n                            }\n                        }\n                        [\"Rotate\", \"Duplicate\", \"Remove\", \"Place\", \"Undo\", \"Redo\", \"Reset\", muted ? \"Sound off\" : \"Sound on\"].forEach(function (label, index) {\n                            const rect = { x: 32 + (index % 4) * 164, y: 1140 + Math.floor(index / 4) * 62, width: 154, height: 56 };\n                            if (pointInRect(point, rect)) activateTrayControl(label === \"Sound on\" || label === \"Sound off\" ? \"Sound\" : label);\n                        });\n                        return;\n                    }\n                    if (selectedCatalogType) {\n                        const placeZone = findZoneAtPoint(point.x, point.y);\n                        if (placeZone) { createCatalogItem(selectedCatalogType, placeZone, point.x, point.y); return; }\n                    }\n                    const item = itemAtPoint(point.x, point.y);\n                    if (!item) { selectedId = null; hoverId = null; canvas.style.cursor = \"default\"; return; }\n                    selectedId = item.id; hoverId = item.id; activePointerId = event.pointerId; playSound(\"paper_slide_sfx\");\n                    dragState = { pointerId: event.pointerId, itemId: item.id, before: captureArrangement(), original: { x: item.x, y: item.y, surface: item.surface }, offsetX: point.x - item.x, offsetY: point.y - item.y, startPointerX: point.x, startPointerY: point.y, pointerX: point.x, pointerY: point.y, moved: false };\n                    canvas.style.cursor = \"grabbing\";\n                    try { if (typeof canvas.setPointerCapture === \"function\") canvas.setPointerCapture(event.pointerId); } catch (error) {}\n                }\n\n                function handlePointerMove(event) {\n                    if (trayPointer && event.pointerId === trayPointer.pointerId) {\n                        event.preventDefault();\n                        const point = pointerToScene(event);\n                        trayPointer.x = point.x; trayPointer.y = point.y;\n                        if (Math.hypot(point.x - trayPointer.startX, point.y - trayPointer.startY) > 8) trayPointer.moved = true;\n                        return;\n                    }\n                    if (activePointerId !== null) {\n                        if (event.pointerId !== activePointerId || !dragState) {\n                            event.preventDefault();\n                            return;\n                        }\n\n                        event.preventDefault();\n                        const point = pointerToScene(event);\n                        const item = objects.find(function (entry) {\n                            return entry.id === dragState.itemId;\n                        });\n\n                        if (!item) {\n                            return;\n                        }\n\n                        dragState.pointerX = point.x;\n                        dragState.pointerY = point.y;\n\n                        if (\n                            Math.hypot(\n                                point.x - dragState.startPointerX,\n                                point.y - dragState.startPointerY\n                            ) > 3\n                        ) {\n                            dragState.moved = true;\n                        }\n\n                        item.x = point.x - dragState.offsetX;\n                        item.y = point.y - dragState.offsetY;\n                        return;\n                    }\n\n                    const point = pointerToScene(event);\n                    const item = itemAtPoint(point.x, point.y);\n                    hoverId = item ? item.id : null;\n                    canvas.style.cursor = hoverId ? \"grab\" : \"default\";\n                }\n\n                function handlePointerUp(event) {\n                    if (trayPointer && event.pointerId === trayPointer.pointerId) {\n                        event.preventDefault();\n                        const point = pointerToScene(event);\n                        if (trayPointer.moved) {\n                            const target = findZoneAtPoint(point.x, point.y);\n                            if (target) createCatalogItem(trayPointer.type, target, point.x, point.y);\n                        }\n                        trayPointer = null; activePointerId = null;\n                        try { if (canvas.hasPointerCapture(event.pointerId)) canvas.releasePointerCapture(event.pointerId); } catch (error) {}\n                        return;\n                    }\n                    if (event.pointerId !== activePointerId) {\n                        return;\n                    }\n\n                    event.preventDefault();\n                    finishDrag(pointerToScene(event));\n                }\n\n                function handlePointerCancel(event) {\n                    if (event.pointerId === activePointerId) {\n                        cancelActiveDrag();\n                    }\n                }\n\n                function handleLostPointerCapture(event) {\n                    if (event.pointerId === activePointerId) {\n                        cancelActiveDrag();\n                    }\n                }\n\n                function handlePointerLeave() {\n                    if (activePointerId === null) {\n                        hoverId = null;\n                        canvas.style.cursor = \"default\";\n                    }\n                }\n\n                function handleKeyDown(event) {\n                    if (document.activeElement !== canvas || activePointerId !== null) {\n                        return;\n                    }\n\n                    const key = String(event.key || \"\").toLowerCase();\n                    if ((event.ctrlKey || event.metaKey) && key === \"z\") { event.preventDefault(); undoChange(); return; }\n                    if ((event.ctrlKey || event.metaKey) && key === \"y\") { event.preventDefault(); redoChange(); return; }\n                    const step = currentSnapStep();\n                    let dx = 0;\n                    let dy = 0;\n\n                    if (key === \"arrowleft\") {\n                        dx = -step;\n                    } else if (key === \"arrowright\") {\n                        dx = step;\n                    } else if (key === \"arrowup\") {\n                        dy = -step;\n                    } else if (key === \"arrowdown\") {\n                        dy = step;\n                    } else {\n                        return;\n                    }\n\n                    const item = objects.find(function (entry) {\n                        return entry.id === selectedId;\n                    });\n\n                    if (!item) {\n                        return;\n                    }\n\n                    event.preventDefault();\n                    const zone = getZoneById(item.surface);\n                    if (!zone) {\n                        return;\n                    }\n\n                    const position = snapToZone(\n                        item,\n                        zone,\n                        item.x + dx,\n                        item.y + dy\n                    );\n\n                    if (position.x === item.x && position.y === item.y) {\n                        return;\n                    }\n\n                    const before = captureArrangement();\n                    item.x = position.x;\n                    item.y = position.y;\n                    syncItemToConfig(item);\n                    setFeedback(item.x, item.y, \"#658b6c\");\n                    recordChange(before);\n                }\n\n                function handleBlur() {\n                    cancelActiveDrag();\n                }\n\n                function drawScene() {\n                    const zones = readZones();\n                    const targetZone = dragState\n                        ? findZoneAtPoint(dragState.pointerX, dragState.pointerY, zones)\n                        : null;\n\n                    const staticKey = zones.map(function (zone) {\n                        return [zone.id, zone.x, zone.y, zone.width, zone.height, zone.baseline_y].join(\",\");\n                    }).join(\"|\");\n                    if (staticLayerContext && staticLayerKey !== staticKey) {\n                        staticLayerContext.clearRect(0, 0, SCENE_WIDTH, SCENE_HEIGHT);\n                        drawBackground(staticLayerContext);\n                        drawBookshelf(staticLayerContext, zones);\n                        drawDesk(staticLayerContext);\n                        drawRoomDepth(staticLayerContext);\n                        drawPrestigeFrame(staticLayerContext, 0);\n                        // The tiny decorative glint is static in the cache; avoiding a full redraw\n                        // each frame is more valuable than animating this subtle detail.\n                        drawPrestigeInlays(staticLayerContext, 0);\n                        staticLayerKey = staticKey;\n                    }\n\n                    ctx.clearRect(0, 0, SCENE_WIDTH, SCENE_HEIGHT);\n                    if (staticLayerContext) {\n                        ctx.drawImage(staticLayerCanvas, 0, 0, SCENE_WIDTH, SCENE_HEIGHT);\n                    } else {\n                        // Graceful fallback for browsers unable to create an offscreen 2D context.\n                        drawBackground(ctx);\n                        drawBookshelf(ctx, zones);\n                        drawDesk(ctx);\n                        drawRoomDepth(ctx);\n                        drawPrestigeFrame(ctx, sessionElapsed);\n                        drawPrestigeInlays(ctx, sessionElapsed);\n                    }\n                    drawHeader(ctx, isEditMode, currentLevel);\n                    drawOrganizationZones(ctx, zones.find(function (zone) { return zone.id === \"desk_top\"; }) || null);\n                    drawZoneGuides(ctx, zones, isEditMode, targetZone);\n                    drawTaskChecklistStrip(ctx);\n\n                    const draggedItem = dragState\n                        ? objects.find(function (item) {\n                            return item.id === dragState.itemId;\n                        })\n                        : null;\n\n                    // Paint items back-to-front so objects nearer the viewer naturally overlap those behind.\n                    const depthOrderedItems = objects.slice().sort(function (a, b) {\n                        return a.y - b.y;\n                    });\n                    for (const item of depthOrderedItems) {\n                        if (draggedItem && item.id === draggedItem.id) continue;\n                        drawItem(ctx, visualItem(item), { selected: item.id === selectedId, hovered: item.id === hoverId, assets: assetImages });\n                    }\n\n                    if (draggedItem && targetZone) {\n                        const snappedPosition = snapToZone(\n                            draggedItem,\n                            targetZone,\n                            draggedItem.x,\n                            draggedItem.y\n                        );\n\n                        const preview = Object.assign({}, draggedItem, {\n                            x: snappedPosition.x,\n                            y: snappedPosition.y\n                        });\n\n                        drawItem(ctx, preview, { ghost: true, assets: assetImages });\n                    }\n\n                    if (draggedItem) {\n                        drawItem(ctx, draggedItem, {\n                            selected: true,\n                            dragging: true,\n                            assets: assetImages\n                        });\n                    }\n\n                    if (trayPointer && trayPointer.moved) {\n                        const type = trayPointer.type;\n                        const zone = findZoneAtPoint(trayPointer.x, trayPointer.y, zones);\n                        if (zone) {\n                            const previewItem = { id: \"tray_ghost\", type: type, x: trayPointer.x, y: trayPointer.y, scale: clamp(numericValue(window.gameConfig.catalog_settings.item_scale, 1), 0.75, 1.25), rotation: 0, tint: \"sage\" };\n                            const snapped = snapToZone(previewItem, zone, trayPointer.x, trayPointer.y);\n                            previewItem.x = snapped.x; previewItem.y = snapped.y;\n                            drawItem(ctx, previewItem, { ghost: true, assets: assetImages });\n                        }\n                    }\n                    drawFeedback(ctx, feedback);\n                    drawFooter(ctx, isEditMode, dragState, targetZone);\n                    drawCompletionCelebration(ctx, sessionElapsed);\n                    drawTaskNotice(ctx);\n                    if (saveLoadPending) { ctx.fillStyle=\"rgba(247,242,231,0.72)\"; ctx.fillRect(0,0,SCENE_WIDTH,SCENE_HEIGHT); ctx.textAlign=\"center\"; ctx.textBaseline=\"middle\"; ctx.fillStyle=\"#625746\"; ctx.font='600 24px \"Poppins\", Arial, sans-serif'; ctx.fillText(\"Restoring your cozy arrangement\u2026\",360,638);ctx.font='600 22px \"Poppins\", Arial, sans-serif';ctx.fillText(\"\u6b63\u5728\u9084\u539f\u6eab\u99a8\u4f48\u7f6e\u2026\",360,672); }\n                }\n\n                resizeCanvas();\n                loadSounds();\n                loadArrangement();\n\n                addListener(canvas, \"pointerdown\", handlePointerDown);\n                addListener(canvas, \"pointermove\", handlePointerMove);\n                addListener(canvas, \"pointerup\", handlePointerUp);\n                addListener(canvas, \"pointercancel\", handlePointerCancel);\n                addListener(canvas, \"lostpointercapture\", handleLostPointerCapture);\n                addListener(canvas, \"pointerleave\", handlePointerLeave);\n                addListener(canvas, \"contextmenu\", function (event) {\n                    event.preventDefault();\n                });\n                addListener(window, \"keydown\", handleKeyDown);\n                addListener(window, \"blur\", handleBlur);\n                addListener(window, \"resize\", resizeCanvas);\n\n                showParameters();\n\n                function frame(timestamp) {\n                    if (activeApp !== app) {\n                        return;\n                    }\n\n                    const deltaTime = previousTimestamp === null\n                        ? 0\n                        : Math.min(0.11, Math.max(0, (timestamp - previousTimestamp) / 1000));\n                    previousTimestamp = timestamp;\n                    if (!isEditMode && !saveLoadPending) sessionElapsed += deltaTime;\n                    if (!isEditMode && !saveLoadPending) {\n                        const currentChecks = tidyChecklist();\n                        for (let index = 0; index < currentChecks.length; index += 1) {\n                            if (currentChecks[index] && !previousTaskChecks[index]) {\n                                taskNotice = \"TASK \" + (index + 1) + \" COMPLETE!\";\n                                taskNoticeLife = 4.5;\n                            }\n                        }\n                        previousTaskChecks = currentChecks.slice();\n                        taskNoticeLife = Math.max(0, taskNoticeLife - deltaTime);\n                        if (!completionCelebrated && currentChecks.every(Boolean)) {\n                            completionCelebrated = true;\n                            playSound(\"completion_applause\");\n                            setFeedback(360, 820, \"#d6b35f\");\n                        }\n                    }\n\n                    if (feedback) {\n                        feedback.life = Math.max(0, feedback.life - deltaTime);\n                        if (feedback.life <= 0) {\n                            feedback = null;\n                        }\n                    }\n\n                    drawScene();\n                    app.rafId = requestAnimationFrame(frame);\n                }\n\n                app.rafId = requestAnimationFrame(frame);\n            }\n\n            window.run = run;\n        })();\n    </script>\n</body>\n</html>", "game_config": "{\n  \"viewport\": {\n    \"width\": 720,\n    \"height\": 1280\n  },\n  \"snap_step_px\": 24,\n  \"surface_zones\": [\n    {\n      \"id\": \"upper_shelf\",\n      \"kind\": \"shelf\",\n      \"x\": 72,\n      \"y\": 184,\n      \"width\": 576,\n      \"height\": 146,\n      \"baseline_y\": 318\n    },\n    {\n      \"id\": \"middle_shelf\",\n      \"kind\": \"shelf\",\n      \"x\": 72,\n      \"y\": 336,\n      \"width\": 576,\n      \"height\": 139,\n      \"baseline_y\": 463\n    },\n    {\n      \"id\": \"lower_shelf\",\n      \"kind\": \"shelf\",\n      \"x\": 72,\n      \"y\": 481,\n      \"width\": 576,\n      \"height\": 139,\n      \"baseline_y\": 608\n    },\n    {\n      \"id\": \"desk_top\",\n      \"kind\": \"desk\",\n      \"x\": 38,\n      \"y\": 700,\n      \"width\": 644,\n      \"height\": 230\n    }\n  ],\n  \"starting_items\": [\n    {\n      \"id\": \"book_01\",\n      \"type\": \"hardcover_book\",\n      \"surface\": \"desk_top\",\n      \"x\": 230,\n      \"y\": 772,\n      \"scale\": 1,\n      \"tint\": \"sage\"\n    },\n    {\n      \"id\": \"plant_01\",\n      \"type\": \"small_potted_plant\",\n      \"surface\": \"upper_shelf\",\n      \"x\": 552,\n      \"y\": 269,\n      \"scale\": 0.9,\n      \"tint\": \"sage\"\n    },\n    {\n      \"id\": \"notebook_01\",\n      \"type\": \"closed_notebook\",\n      \"surface\": \"desk_top\",\n      \"x\": 360,\n      \"y\": 824,\n      \"scale\": 1,\n      \"tint\": \"ochre\"\n    },\n    {\n      \"id\": \"book_04\",\n      \"type\": \"hardcover_book\",\n      \"surface\": \"desk_top\",\n      \"x\": 470,\n      \"y\": 784,\n      \"scale\": 1,\n      \"tint\": \"plum\"\n    },\n    {\n      \"id\": \"book_02\",\n      \"type\": \"hardcover_book\",\n      \"surface\": \"lower_shelf\",\n      \"x\": 504,\n      \"y\": 564,\n      \"scale\": 1,\n      \"tint\": \"terracotta\"\n    },\n    {\n      \"id\": \"book_03\",\n      \"type\": \"hardcover_book\",\n      \"surface\": \"desk_top\",\n      \"x\": 256,\n      \"y\": 840,\n      \"scale\": 1,\n      \"tint\": \"teal\"\n    },\n    {\n      \"id\": \"glasses_01\",\n      \"type\": \"reading_glasses\",\n      \"surface\": \"desk_top\",\n      \"x\": 302,\n      \"y\": 796,\n      \"scale\": 1,\n      \"tint\": \"cream\"\n    },\n    {\n      \"id\": \"pencil_cup_01\",\n      \"type\": \"pencil_cup\",\n      \"surface\": \"desk_top\",\n      \"x\": 398,\n      \"y\": 796,\n      \"scale\": 1,\n      \"tint\": \"cream\"\n    },\n    {\n      \"id\": \"mug_01\",\n      \"type\": \"ceramic_mug\",\n      \"surface\": \"desk_top\",\n      \"x\": 494,\n      \"y\": 796,\n      \"scale\": 1,\n      \"tint\": \"cream\"\n    },\n    {\n      \"id\": \"lamp_01\",\n      \"type\": \"desk_lamp\",\n      \"surface\": \"desk_top\",\n      \"x\": 602,\n      \"y\": 772,\n      \"scale\": 1,\n      \"tint\": \"sage\"\n    }\n  ],\n  \"catalog_settings\": {\n    \"enabled_types\": [\n      \"hardcover_book\",\n      \"closed_notebook\",\n      \"pencil_cup\",\n      \"desk_lamp\",\n      \"small_potted_plant\",\n      \"reading_glasses\",\n      \"ceramic_mug\"\n    ],\n    \"item_scale\": 1,\n    \"max_item_count\": 24,\n    \"stack_limit\": 4,\n    \"sound_enabled\": true,\n    \"sound_volume\": 0.35,\n    \"palette\": [\n      \"sage\",\n      \"terracotta\",\n      \"teal\",\n      \"ochre\",\n      \"plum\"\n    ],\n    \"default_book_tint\": \"sage\",\n    \"halloween_items_migrated\": true\n  }\n}", "game_design_document": "## Experience and core loop\n\nAn open-ended organizing toy about making a small study feel just right. Players can move books and everyday desk objects between an open bookshelf and a desk, then keep adjusting the arrangement for as long as they like.\n\nThe loop is simple: choose an item, drag it to a shelf or desk, and release to see it snap into place. Browse the item tray to add more things, rotate or duplicate an object, and keep rearranging. A starter scene offers something to interact with, but it is not graded as messy or tidy. Every arrangement is valid.\n\nThis remains a toy: **no score, timer, levels, lives, objectives, win/lose states, or cleanliness rating.** Reset returns to the starter arrangement; it is never a penalty.\n\n## Perspective and scene layout\n\n- **View:** Fixed-screen, 2D room diorama with a gently elevated, mostly front-facing view so players can see both shelf ledges and the desktop.\n- **Viewport:** Portrait, 720 \u00d7 1280 CSS pixels.\n- **Layout:**\n  - Header and game title: y = 0\u2013120.\n  - Open bookcase: roughly x = 48\u2013672, y = 145\u2013635, with three shelf levels.\n  - Desk surface: roughly x = 38\u2013682, y = 700\u2013930.\n  - Item tray and touch controls: y = 940\u20131280.\n- The room does not scroll. The bookcase and desk stay in place while objects move above them.\n\n## Objects and placement\n\nThe starter scene has several books, a pencil cup, and a desk lamp in a gently scattered arrangement. The expanded catalog adds notebooks, a small plant, reading glasses, and a mug.\n\n- Drag an object onto a shelf or desk to place it. During a drag, show a subtle preview of its snapped position.\n- Shelved items snap horizontally to a grid and align with the shelf ledge. Desktop items snap to a two-axis grid.\n- Dropping outside a valid surface returns an existing object to its previous position. An unplaced catalog item cancels instead.\n- Overlap is allowed so players can make their own arrangements. The final version lets books form small, visible desk stacks.\n- Use a soft highlight to show which object is selected. Keep a minimum 96 \u00d7 96 px touch target even when the artwork is smaller.\n\n## Play mode and Edit mode\n\n**Play mode** is for arranging a personal scene. A bottom tray has English-language **Books**, **Desk**, and **Decor** categories. Tapping a catalog card enters placement mode; the player can then tap a valid spot or drag the item there. Selecting a placed item reveals controls to rotate, duplicate, or remove it. Undo, redo, and reset controls are also available.\n\n**Edit mode** is for creators shaping the game\u2019s defaults. Creators can arrange the starter objects, choose the room palette, enable or disable catalog items, and adjust placement settings. It changes the game\u2019s default setup; it does not impose a goal on Play mode.\n\nExpose these editable settings with safe limits:\n\n- Snap spacing: 12\u201348 px; default 24 px.\n- Maximum placed items: 8\u201340; default 32.\n- Item scale: 0.75\u20131.25\u00d7.\n- Room palette: a small set of coordinated presets.\n- Enabled catalog items and starter object positions.\n- Shelf and desk placement bounds, constrained to the visible surfaces.\n\nIllustrative editable configuration:\n\n```json\n{\n  \"viewport\": { \"width\": 720, \"height\": 1280 },\n  \"palette\": \"warm_oak\",\n  \"snap_step_px\": 24,\n  \"max_items\": 32,\n  \"surface_zones\": [\n    { \"id\": \"upper_shelf\", \"kind\": \"shelf\", \"x\": 72, \"y\": 205, \"width\": 576, \"height\": 125, \"baseline_y\": 318 },\n    { \"id\": \"middle_shelf\", \"kind\": \"shelf\", \"x\": 72, \"y\": 350, \"width\": 576, \"height\": 125, \"baseline_y\": 463 },\n    { \"id\": \"lower_shelf\", \"kind\": \"shelf\", \"x\": 72, \"y\": 495, \"width\": 576, \"height\": 125, \"baseline_y\": 608 },\n    { \"id\": \"desk_top\", \"kind\": \"desk\", \"x\": 38, \"y\": 700, \"width\": 644, \"height\": 230 }\n  ],\n  \"enabled_items\": [\n    \"hardcover_book\",\n    \"pencil_cup\",\n    \"desk_lamp\",\n    \"closed_notebook\",\n    \"small_potted_plant\",\n    \"reading_glasses\",\n    \"ceramic_mug\"\n  ],\n  \"starting_items\": [\n    { \"id\": \"book_01\", \"type\": \"hardcover_book\", \"surface\": \"upper_shelf\", \"x\": 160, \"y\": 267, \"rotation_deg\": 0, \"scale\": 1, \"tint\": \"sage\" },\n    { \"id\": \"book_02\", \"type\": \"hardcover_book\", \"surface\": \"upper_shelf\", \"x\": 216, \"y\": 267, \"rotation_deg\": 0, \"scale\": 1, \"tint\": \"terracotta\" },\n    { \"id\": \"plant_01\", \"type\": \"small_potted_plant\", \"surface\": \"lower_shelf\", \"x\": 120, \"y\": 558, \"rotation_deg\": 0, \"scale\": 1 },\n    { \"id\": \"lamp_01\", \"type\": \"desk_lamp\", \"surface\": \"desk_top\", \"x\": 566, \"y\": 812, \"rotation_deg\": 0, \"scale\": 1 }\n  ]\n}\n```\n\n## Controls\n\n**Touch-first**\n\n- Drag a placed object to move it.\n- Tap a catalog item, then tap a valid surface to place it; dragging it from the tray also works.\n- Use large on-screen buttons for rotate, duplicate, remove, undo, redo, and reset.\n- Track the active pointer ID so a second finger cannot interfere with a drag. One object is manipulated at a time.\n\n**Desktop**\n\n- Use the mouse to select and drag.\n- Arrow keys nudge the selected object by one grid step; **R** rotates it; **Delete** removes it; **Ctrl/Cmd+Z** undoes.\n- Prevent browser defaults for mapped keys, store key names in lowercase, and clear key state on blur.\n\n## Art, sound, and persistence\n\nUse a warm, cozy illustration style: soft gouache-like shading, clean silhouettes, light paper texture, cream walls, warm oak, and muted sage, teal, and terracotta accents. Keep the English UI crisp and readable; do not bake labels or text into artwork. Render contact shadows, selection highlights, and snap feedback in code.\n\nThe room background and movable objects are separate images so objects remain draggable. Book-cover color variations can reuse the same book artwork with code-applied tints. Keep sound optional and quiet: a light paper shuffle on pickup and a gentle wooden tap on placement.\n\nSave and restore each player\u2019s arrangement through `lib.saveUserGameState` and `lib.getUserGameState` only; do not use browser storage. If no saved arrangement exists, load the starter scene. The toy has no leaderboard.", "persistent_storage_config": {"leaderboard": {"sort_ascending": true}}, "game_metadata": {"game_id": "01M61E6P3B24J06XDJEK6Z51B0", "title": "Desk-tidying challenge", "description": "Arrange, stack, and style books and desk treasures in a cozy study\u2014no timers, scores, or pressure.", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_4ad2c9ac-15a1-49df-87ae-3a2928661a1d.webp", "created_at": 1793197828, "updated_at": 1793580273443.9768, "art_style": null}, "task_plan": null}