{"asset_map": {"character_base": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/character_base_0abe4b33-c2de-40a1-9301-f95f2105b216.webp", "type": "image", "prompt": "Regenerate the chibi-style character base from image 1, maintaining the exact same pose, proportions, and art style. Remove any and all drop shadows or ground shadows beneath the character's feet. The character must be clean, isolated, and have a fully transparent background with no shadow elements. Side-view, front-facing chibi illustration, clean lines, polished game-ready asset.", "description": "A chibi-style character base sprite in MapleStory's signature art style with no shadows for layering.", "subtype": "character", "aspect_ratio": [681, 1227], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "background_henesys": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/background_henesys_852e945b-d837-4bb3-911a-a706633bb841.webp", "type": "image", "prompt": "Generate a 9:16 tall portrait-orientation background for a character dress-up game. The scene is a luxurious Barbie-inspired pink dreamhouse fashion closet and runway. Featuring a fashionable glossy pink marble floor, sparkly elements, pink and magenta curtains with ornate gold accents, and gold-framed mirrors. Floating glitter particles throughout. Through the windows, a sunset sky in hot pink, coral, and gold is visible. Ultra-feminine, glamorous, and sassy aesthetic. Stylized cartoon art style to match a chibi-style dress-up game. Color palette: hot pink, magenta, blush pink, gold, white, and soft lavender.", "description": "A glamorous Barbie-inspired pink dreamhouse fashion closet and runway background.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "character_head": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781644611551_01KV94PKYZ2WFEH7NE1HHETRQS.base_head_resized.webp", "type": "image", "prompt": null, "description": "Head-only portion of the chibi character, used as a foundation for hair and face customization.", "subtype": null, "aspect_ratio": [205, 205], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "character_upper_body": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781644610993_01KV94PKDHDSE1N2B21GRKZQ76.base_upperhalf_resized.webp", "type": "image", "prompt": null, "description": "Upper body base (torso + arms) for the chibi character \u2014 used as a layering sprite for tops and clothing.", "subtype": null, "aspect_ratio": [205, 118], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "character_lower_body": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781644721511_01KV94SZB7GW1MF9XKF0AQN90P.base_lowerhalf_resized.webp", "type": "image", "prompt": null, "description": "Lower body base of the chibi character \u2014 legs and feet for the dress-up game", "subtype": null, "aspect_ratio": [128, 128], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "character_full_body": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781644611308_01KV94PKQB7QBCQA0582YHNME8.base_body_resized.webp", "type": "image", "prompt": null, "description": "Full body base of the chibi character (head to toe) \u2014 used as the single-layer base for dresses and full outfit overlays.", "subtype": null, "aspect_ratio": [205, 205], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "spritesheet_upper": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781954608423_01KVJCAZ97BM9ADNVV95DW4DPE.barbie_upperhalf.webp", "type": "image", "prompt": null, "description": "A 5x5 spritesheet of 25 upper body clothing items (shirts, tops, jackets) for the MapleStory dress-up game", "subtype": null, "aspect_ratio": [1025, 590], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "spritesheet_lower": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781953770398_01KVJBHCWYPGHSXZFCJPK8YQK6.barbie_lowerhalf.webp", "type": "image", "prompt": null, "description": "A 5x5 spritesheet of 25 lower body clothing items (skirts, pants, shorts, etc.) for the MapleStory dress-up game.", "subtype": null, "aspect_ratio": [638, 638], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "spritesheet_heads": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781650914100_01KV9APYSMFDM15NHJ8VRMR3MK.heads_remastered.webp", "type": "image", "prompt": null, "description": "5x5 spritesheet of 25 remastered head/hairstyle items for the MapleStory dress-up game. Chibi anime-style faces with diverse hair colors and styles.", "subtype": null, "aspect_ratio": [1025, 1025], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "spritesheet_fullbody": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781951934959_01KVJ9SCFFQ1XETT2H364R2ATZ.barbie_full.webp", "type": "image", "prompt": null, "description": "A 5x5 spritesheet of 25 Barbie-themed full-body outfits on a white background.", "subtype": null, "aspect_ratio": [1025, 1025], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "spritesheet_weapons": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781956913090_01KVJEH9Y2GWCKA35503CW43PR.barbie_handbags.png", "type": "image", "prompt": null, "description": "A 5x5 spritesheet of 25 Barbie-themed handbags and accessories for the MapleStory dress-up game.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "spritesheet_capes": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781955993807_01KVJDN86F22P533QFMF917NNQ.back_auras.webp", "type": "image", "prompt": null, "description": "A 5x5 spritesheet of 25 unique Barbie-themed magical glowing auras and effects for character customization.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "spritesheet_pets": {"url": "https://d16qq0o35va7kz.cloudfront.net/images/Google_115974500078511979658/1781955443497_01KVJD4ES9R2GWQT0FXMG8VRZF.barbie_pets.webp", "type": "image", "prompt": null, "description": "A 5x5 spritesheet of 25 Barbie-themed chibi pet companions for character customization", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <title>BARBIE Styling Studio | Free Online Dress-Up Game</title>\n\n    <!-- Primary SEO Meta Tags -->\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <meta name=\"title\" content=\"BARBIE Styling Studio | Free Online Dress-Up Game\">\n    <meta name=\"description\" content=\"Play BARBIE Styling Studio, a free online dress-up game. Mix outfits, hairstyles, handbags and pets, save your looks, and try fashion challenges.\">\n    <meta name=\"keywords\" content=\"BARBIE Styling Studio, Barbie dress up game, Barbie fashion game, free online dress up game, fashion styling game, avatar creator, character creator, outfit maker, chibi dress up, anime dress up, kawaii fashion, Barbiecore, pink aesthetic, virtual wardrobe, mix and match outfits, hairstyle game, handbag accessories, cute pets, fashion challenge, outfit challenge, save outfits, casual browser game, mobile dress up game\">\n    <meta name=\"author\" content=\"Astrocade\">\n    <meta name=\"robots\" content=\"index, follow, max-image-preview:large\">\n    <meta name=\"application-name\" content=\"BARBIE Styling Studio\">\n    <meta name=\"theme-color\" content=\"#FF69B4\">\n\n    <!-- Open Graph / Facebook -->\n    <meta property=\"og:type\" content=\"website\">\n    <meta property=\"og:title\" content=\"BARBIE Styling Studio | Free Online Dress-Up Game\">\n    <meta property=\"og:description\" content=\"Create your dream chibi look with outfits, hairstyles, handbags, auras and cute pets. Save your favorites and take on fashion challenges.\">\n    <meta property=\"og:locale\" content=\"en_US\">\n    <!-- Add og:url, a canonical URL, and social image tags only when verified public page and thumbnail URLs are available. -->\n    <meta property=\"og:site_name\" content=\"BARBIE Styling Studio\">\n\n    <!-- Twitter Card -->\n    <meta name=\"twitter:card\" content=\"summary_large_image\">\n    <meta name=\"twitter:title\" content=\"BARBIE Styling Studio | Free Online Dress-Up Game\">\n    <meta name=\"twitter:description\" content=\"Create your dream chibi look with outfits, hairstyles, handbags, auras and cute pets. Save your favorites and take on fashion challenges.\">\n\n\n    <!-- Structured Data / JSON-LD -->\n    <script type=\"application/ld+json\">\n    {\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"VideoGame\",\n      \"name\": \"BARBIE Styling Studio\",\n      \"description\": \"A free online Barbie-themed chibi dress-up game. Mix outfits, hairstyles, handbags, auras and pets, save favorite looks, and complete outfit-matching fashion challenges.\",\n      \"genre\": [\"Dress-up Game\", \"Fashion Game\", \"Character Creator\", \"Casual Game\"],\n      \"gamePlatform\": \"Web Browser\",\n      \"applicationCategory\": \"GameApplication\",\n      \"inLanguage\": \"en\",\n      \"isAccessibleForFree\": true,\n      \"playMode\": \"SinglePlayer\",\n      \"featureList\": [\"Mix-and-match wardrobe\", \"Chibi avatar customization\", \"Saved outfit gallery\", \"Random outfit generator\", \"Outfit-matching challenges\", \"Pet companions\"],\n      \"operatingSystem\": \"Any\",\n      \"keywords\": \"BARBIE Styling Studio, Barbie dress up game, free online dress up game, fashion styling, avatar creator, outfit maker, chibi, kawaii fashion, Barbiecore, pink aesthetic, cute pets, outfit challenge, mobile browser game\",\n      \"publisher\": { \"@type\": \"Organization\", \"name\": \"Astrocade\" }\n    }\n    </script>\n\n    <link href=\"https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n    <style>\n        :root {\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --ease-out: cubic-bezier(0.0, 0, 0.2, 1);\n            --ease-bounce: cubic-bezier(0.68, -0.6, 0.32, 1.6);\n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n            --primary: #FF69B4;\n            --primary-dark: #E91E8A;\n            --secondary: #FFD700;\n            --bg-panel: rgba(255, 240, 248, 0.96);\n            --bg-dark: rgba(80, 10, 50, 0.92);\n            --text-dark: #4A0E3C;\n            --text-light: #FFFFFF;\n            --highlight: #FF1493;\n            --shadow: 0 4px 16px rgba(255, 105, 180, 0.2);\n        }\n\n        body, html {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n            overflow: hidden;\n            touch-action: none;\n            -ms-touch-action: none;\n            font-family: 'Inter', sans-serif;\n        }\n\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n            background: linear-gradient(180deg, #FFB6C1 0%, #FF69B4 40%, #FF1493 70%, #C71585 100%);\n        }\n\n        #game-canvas {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n        }\n\n        .ui-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            pointer-events: none;\n        }\n\n        .ui-overlay > * {\n            pointer-events: auto;\n        }\n\n        /* Category Navigation */\n        .category-nav {\n            position: absolute;\n            left: 16px;\n            top: 80px;\n            bottom: 16px;\n            display: flex;\n            flex-direction: column;\n            gap: 8px;\n            overflow-y: auto;\n            overflow-x: hidden;\n            padding-right: 4px;\n            -webkit-overflow-scrolling: touch;\n        }\n\n        .category-nav::-webkit-scrollbar {\n            width: 4px;\n        }\n        .category-nav::-webkit-scrollbar-thumb {\n            background: linear-gradient(180deg, #FF69B4, #FF1493);\n            border-radius: 2px;\n        }\n\n        .category-btn {\n            min-width: 80px;\n            width: 80px;\n            height: 80px;\n            border-radius: 16px;\n            border: 3px solid transparent;\n            background: var(--bg-panel);\n            box-shadow: var(--shadow);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-out),\n                        border-color var(--transition-fast) var(--ease-out),\n                        background var(--transition-fast) var(--ease-out);\n            font-family: 'Fredoka', sans-serif;\n            font-size: 14px;\n            font-weight: 600;\n            color: var(--text-dark);\n            flex-shrink: 0;\n        }\n\n        .category-btn:hover {\n            transform: scale(1.05);\n            border-color: var(--primary);\n        }\n\n        .category-btn:active {\n            transform: scale(0.95);\n        }\n\n        .category-btn.active {\n            border-color: var(--primary);\n            background: linear-gradient(135deg, #FFE4F0, #FFF0F7);\n        }\n\n        .category-icon {\n            font-size: 26px;\n            margin-bottom: 2px;\n        }\n\n        /* Item Panel */\n        .item-panel {\n            position: absolute;\n            bottom: 0;\n            left: 0;\n            right: 0;\n            height: 280px;\n            background: var(--bg-panel);\n            border-radius: 24px 24px 0 0;\n            box-shadow: 0 -4px 24px rgba(255,105,180,0.2);\n            padding: 16px;\n            transform: translateY(100%);\n            transition: transform var(--transition-normal) var(--ease-out);\n            display: flex;\n            flex-direction: column;\n            border-top: 2px solid rgba(255,105,180,0.3);\n        }\n\n        .item-panel.visible {\n            transform: translateY(0);\n        }\n\n        .item-panel-header {\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n            margin-bottom: 12px;\n            padding: 0 8px;\n        }\n\n        .item-panel-title {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 28px;\n            font-weight: 700;\n            color: var(--text-dark);\n        }\n\n        .item-panel-close {\n            width: 48px;\n            height: 48px;\n            border-radius: 50%;\n            border: none;\n            background: #F0F0F0;\n            font-size: 24px;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: background var(--transition-fast);\n        }\n\n        .item-panel-close:hover {\n            background: #E0E0E0;\n        }\n\n        .item-panel-close:active {\n            background: #D0D0D0;\n        }\n\n        .item-grid {\n            display: flex;\n            gap: 12px;\n            overflow-x: auto;\n            overflow-y: hidden;\n            padding: 8px;\n            flex: 1;\n            align-items: center;\n            -webkit-overflow-scrolling: touch;\n        }\n\n        .item-grid::-webkit-scrollbar {\n            height: 6px;\n        }\n\n        .item-grid::-webkit-scrollbar-track {\n            background: #F0F0F0;\n            border-radius: 3px;\n        }\n\n        .item-grid::-webkit-scrollbar-thumb {\n            background: linear-gradient(90deg, #FF69B4, #FF1493);\n            border-radius: 3px;\n        }\n\n        .item-card {\n            min-width: 150px;\n            height: 150px;\n            border-radius: 16px;\n            border: 3px solid #E8E8E8;\n            background: white;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-out),\n                        border-color var(--transition-fast);\n            padding: 8px;\n            position: relative;\n            overflow: hidden;\n        }\n\n        .item-card::after {\n            content: '';\n            position: absolute;\n            top: 0; left: -100%;\n            width: 60%; height: 100%;\n            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.55), transparent);\n            transform: skewX(-20deg);\n            transition: left 0.45s ease;\n            pointer-events: none;\n        }\n        .item-card:hover::after { left: 160%; }\n\n        .item-card:hover {\n            transform: scale(1.08);\n            border-color: var(--primary);\n            cursor: pointer;\n        }\n\n        .item-card:active {\n            transform: scale(0.95);\n        }\n\n        .item-card.equipped {\n            border-color: var(--highlight);\n            background: linear-gradient(135deg, #FFF0F7, #FFD1E8);\n            box-shadow: 0 0 12px rgba(255, 20, 147, 0.3);\n        }\n\n        .item-card-preview {\n            width: 120px;\n            height: 120px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .item-card-name {\n            display: none;\n        }\n\n        /* Edit mode styles */\n        .edit-controls {\n            position: absolute;\n            top: 16px;\n            right: 16px;\n            display: flex;\n            flex-direction: column;\n            gap: 8px;\n        }\n\n        .edit-btn {\n            padding: 12px 20px;\n            border-radius: 12px;\n            border: none;\n            background: var(--primary);\n            color: white;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 18px;\n            font-weight: 600;\n            cursor: pointer;\n            box-shadow: var(--shadow);\n            transition: transform var(--transition-fast) var(--ease-out),\n                        background var(--transition-fast);\n        }\n\n        .edit-btn:hover {\n            background: var(--primary-dark);\n            transform: scale(1.03);\n        }\n\n        .edit-btn:active {\n            transform: scale(0.97);\n        }\n\n        .edit-btn.danger {\n            background: #E74C3C;\n        }\n\n        .edit-btn.danger:hover {\n            background: #C0392B;\n        }\n\n        /* Equipped Bar */\n        .equipped-bar {\n            position: absolute;\n            bottom: 24px;\n            left: 50%;\n            transform: translateX(-50%);\n            display: flex;\n            flex-direction: row;\n            gap: 8px;\n            transition: all var(--transition-normal) var(--ease-out);\n            z-index: 10;\n        }\n\n        .equipped-bar.moved-top {\n            bottom: 330px;\n        }\n\n        .equipped-slot {\n            width: 80px;\n            height: 80px;\n            border-radius: 16px;\n            background: rgba(80, 10, 50, 0.7);\n            border: 2px solid rgba(255, 105, 180, 0.7);\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            overflow: hidden;\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-bounce);\n            box-shadow: 0 0 8px rgba(255, 105, 180, 0.3);\n        }\n\n        .equipped-slot:hover {\n            transform: scale(1.1);\n        }\n\n        @keyframes equippedSlotClickPop {\n            0%   { transform: scale(1); }\n            25%  { transform: scale(0.86); }\n            60%  { transform: scale(1.12); }\n            80%  { transform: scale(0.97); }\n            100% { transform: scale(1); }\n        }\n        .equipped-slot.btn-click { animation: equippedSlotClickPop 0.35s cubic-bezier(0.34,1.56,0.64,1); }\n\n        .equipped-slot canvas {\n            width: 66px;\n            height: 66px;\n            pointer-events: none;\n        }\n\n        .equipped-slot-empty {\n            font-size: 20px;\n            opacity: 0.5;\n        }\n\n        /* Gallery UI */\n        .gallery-btn-group {\n            position: absolute;\n            top: 16px;\n            left: 50%;\n            transform: translateX(-50%);\n            display: flex;\n            flex-direction: row;\n            gap: 10px;\n            z-index: 50;\n        }\n\n        .gallery-btn {\n            border-radius: 28px;\n            background: white;\n            box-shadow: var(--shadow);\n            font-family: 'Fredoka', sans-serif;\n            font-size: 16px;\n            font-weight: 700;\n            padding: 10px 18px;\n            border: none;\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-out);\n            color: var(--text-dark);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            line-height: 1.2;\n            text-align: center;\n            min-width: 84px;\n            min-height: 64px;\n        }\n\n        .gallery-btn .btn-icon {\n            font-size: 26px;\n            line-height: 1.2;\n        }\n\n        .gallery-btn .btn-label {\n            font-size: 15px;\n            line-height: 1.2;\n        }\n\n        .gallery-btn:hover {\n            transform: scale(1.03);\n        }\n\n        .gallery-btn:active {\n            transform: scale(0.97);\n        }\n\n        .gallery-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.75);\n            z-index: 100;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            padding: 20px;\n            box-sizing: border-box;\n        }\n\n        .gallery-panel {\n            background: linear-gradient(180deg, #FFF0F7, #FFFFFF);\n            border-radius: 24px;\n            width: 88%;\n            max-width: 600px;\n            max-height: 75%;\n            overflow-y: auto;\n            padding: 20px;\n            display: flex;\n            flex-direction: column;\n            gap: 12px;\n            box-shadow: 0 8px 32px rgba(255,105,180,0.3);\n            position: relative;\n            border: 2px solid rgba(255,105,180,0.2);\n        }\n\n        .gallery-header {\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n            margin-bottom: 8px;\n            position: sticky;\n            top: 0;\n            background: white;\n            z-index: 1;\n            padding-bottom: 12px;\n        }\n\n        .gallery-title {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 28px;\n            font-weight: 700;\n            color: var(--text-dark);\n        }\n\n        .gallery-close {\n            width: 56px;\n            height: 56px;\n            border-radius: 50%;\n            border: none;\n            background: #E0E0E0;\n            font-size: 26px;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: background var(--transition-fast);\n            font-weight: 700;\n            flex-shrink: 0;\n        }\n\n        .gallery-close:hover {\n            background: #E0E0E0;\n        }\n\n        .gallery-empty {\n            text-align: center;\n            padding: 40px 20px;\n            color: #666;\n            font-size: 16px;\n            line-height: 1.5;\n        }\n\n        .gallery-entry {\n            display: flex;\n            align-items: center;\n            gap: 12px;\n            padding: 12px;\n            border-radius: 16px;\n            border: 2px solid #E8E8E8;\n            background: white;\n            transition: border-color var(--transition-fast);\n        }\n\n        .gallery-entry:hover {\n            border-color: var(--primary);\n        }\n\n        .gallery-preview {\n            width: 64px;\n            height: 64px;\n            border-radius: 12px;\n            background: #F8F8F8;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            flex-shrink: 0;\n            overflow: hidden;\n        }\n\n        .gallery-preview canvas {\n            width: 64px;\n            height: 64px;\n        }\n\n        .gallery-info {\n            flex: 1;\n            min-width: 0;\n        }\n\n        .gallery-name {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 18px;\n            font-weight: 600;\n            color: var(--text-dark);\n            margin-bottom: 4px;\n        }\n\n        .gallery-date {\n            font-size: 14px;\n            color: #666;\n        }\n\n        .gallery-actions {\n            display: flex;\n            gap: 8px;\n            flex-shrink: 0;\n        }\n\n        .gallery-action-btn {\n            padding: 8px 16px;\n            border-radius: 12px;\n            border: none;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 14px;\n            font-weight: 600;\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-out);\n        }\n\n        .gallery-action-btn:hover {\n            transform: scale(1.05);\n        }\n\n        .gallery-action-btn:active {\n            transform: scale(0.95);\n        }\n\n        .gallery-action-btn.wear {\n            background: var(--primary);\n            color: white;\n        }\n\n        .gallery-action-btn.delete {\n            background: #E74C3C;\n            color: white;\n        }\n\n        /* Challenge UI */\n        .challenge-popup {\n            position: absolute;\n            top: 80px;\n            left: 104px;\n            right: 10px;\n            width: auto;\n            max-width: 520px;\n            background: linear-gradient(160deg, #FFF0F7, #FFFFFF);\n            border-radius: 20px;\n            box-shadow: 0 8px 32px rgba(255,105,180,0.3);\n            padding: 16px 16px 18px;\n            z-index: 80;\n            display: flex;\n            flex-direction: column;\n            gap: 12px;\n            border: 2px solid rgba(255,105,180,0.2);\n        }\n\n        .challenge-popup-title {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 20px;\n            font-weight: 700;\n            color: var(--text-dark);\n            text-align: center;\n            padding-right: 20px;\n        }\n\n        .challenge-items-grid {\n            display: flex;\n            flex-wrap: wrap;\n            gap: 6px;\n            justify-content: center;\n        }\n\n        .challenge-item-slot {\n            width: 104px;\n            height: 120px;\n            border-radius: 12px;\n            background: rgba(0,0,0,0.05);\n            border: 2px solid #E8E8E8;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            overflow: hidden;\n            gap: 2px;\n        }\n\n        .challenge-item-slot.matched {\n            border-color: #27AE60;\n            background: #EAFAF1;\n        }\n\n        .challenge-item-slot.missed {\n            border-color: #E74C3C;\n            background: #FEF0EF;\n        }\n\n        .challenge-item-label {\n            font-size: 9px;\n            color: #888;\n            font-family: 'Fredoka', sans-serif;\n            text-align: center;\n            line-height: 1;\n        }\n\n        .challenge-submit-btn {\n            background: var(--primary);\n            color: white;\n            border: none;\n            border-radius: 14px;\n            padding: 14px;\n            font-family: 'Fredoka', sans-serif;\n            font-size: 19px;\n            font-weight: 700;\n            cursor: pointer;\n            width: 100%;\n            transition: transform var(--transition-fast) var(--ease-out),\n                        background var(--transition-fast);\n        }\n\n        .challenge-submit-btn:hover { transform: scale(1.03); }\n        .challenge-submit-btn:active { transform: scale(0.97); }\n\n        .challenge-result {\n            text-align: center;\n        }\n\n        .challenge-stars {\n            font-size: 22px;\n            letter-spacing: 1px;\n        }\n\n        .challenge-score-text {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 14px;\n            color: var(--text-dark);\n            margin-top: 3px;\n        }\n\n        .challenge-close-btn {\n            position: absolute;\n            top: 10px;\n            right: 10px;\n            width: 48px;\n            height: 48px;\n            border-radius: 50%;\n            border: none;\n            background: #E0E0E0;\n            font-size: 22px;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: background var(--transition-fast);\n            font-weight: 700;\n        }\n\n        .challenge-close-btn:hover { background: #E0E0E0; }\n\n        /* \u2500\u2500 Loading Screen \u2500\u2500 */\n        #loading-screen {\n            position: fixed;\n            top: 0; left: 0; width: 100%; height: 100%;\n            z-index: 9999;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            background: linear-gradient(160deg, #2d0028 0%, #4a0040 50%, #7a0060 100%);\n            font-family: 'Fredoka', sans-serif;\n            overflow: hidden;\n            transition: opacity 0.6s ease, transform 0.6s ease;\n        }\n\n        #loading-screen.fade-out {\n            opacity: 0;\n            pointer-events: none;\n        }\n\n        .loading-stars-bg {\n            position: absolute;\n            top: 0; left: 0; width: 100%; height: 100%;\n            pointer-events: none;\n        }\n\n        .loading-star {\n            position: absolute;\n            border-radius: 50%;\n            background: white;\n            animation: starTwinkle var(--dur, 2s) ease-in-out infinite var(--delay, 0s);\n        }\n\n        @keyframes starTwinkle {\n            0%, 100% { opacity: 0.1; transform: scale(1); }\n            50% { opacity: 0.9; transform: scale(1.4); }\n        }\n\n        .loading-orb {\n            position: relative;\n            width: 120px;\n            height: 120px;\n            margin-bottom: 32px;\n        }\n\n        .loading-orb-ring {\n            position: absolute;\n            top: 50%; left: 50%;\n            border-radius: 50%;\n            border: 3px solid transparent;\n            transform: translate(-50%, -50%);\n        }\n\n        .loading-orb-ring:nth-child(1) {\n            width: 120px; height: 120px;\n            border-top-color: #FF69B4;\n            border-right-color: rgba(255,105,180,0.3);\n            animation: orbSpin 1.4s linear infinite;\n        }\n\n        .loading-orb-ring:nth-child(2) {\n            width: 90px; height: 90px;\n            border-top-color: #FF1493;\n            border-left-color: rgba(255,20,147,0.3);\n            animation: orbSpin 1.0s linear infinite reverse;\n        }\n\n        .loading-orb-ring:nth-child(3) {\n            width: 62px; height: 62px;\n            border-top-color: #FFD700;\n            border-bottom-color: rgba(255,215,0,0.3);\n            animation: orbSpin 0.7s linear infinite;\n        }\n\n        .loading-orb-core {\n            position: absolute;\n            top: 50%; left: 50%;\n            transform: translate(-50%,-50%);\n            width: 32px; height: 32px;\n            border-radius: 50%;\n            background: radial-gradient(circle, #ffffff 0%, #FFB6C1 50%, #FF69B4 100%);\n            box-shadow: 0 0 20px #FF69B4, 0 0 40px rgba(255,105,180,0.5);\n            animation: coreGlow 2s ease-in-out infinite;\n        }\n\n        @keyframes orbSpin {\n            from { transform: translate(-50%,-50%) rotate(0deg); }\n            to   { transform: translate(-50%,-50%) rotate(360deg); }\n        }\n\n        @keyframes coreGlow {\n            0%, 100% { box-shadow: 0 0 20px #FF69B4, 0 0 40px rgba(255,105,180,0.5); transform: translate(-50%,-50%) scale(1); }\n            50% { box-shadow: 0 0 30px #FFD700, 0 0 60px rgba(255,215,0,0.6); transform: translate(-50%,-50%) scale(1.15); }\n        }\n\n        .loading-title {\n            font-size: 38px;\n            font-weight: 700;\n            color: white;\n            letter-spacing: 1px;\n            text-shadow: 0 0 20px rgba(255,105,180,0.8), 0 2px 4px rgba(0,0,0,0.5);\n            margin-bottom: 6px;\n            animation: titlePulse 3s ease-in-out infinite;\n        }\n\n        @keyframes titlePulse {\n            0%, 100% { opacity: 1; }\n            50% { opacity: 0.85; }\n        }\n\n        .loading-subtitle {\n            font-size: 18px;\n            font-weight: 500;\n            color: rgba(255,255,255,0.55);\n            margin-bottom: 40px;\n            min-height: 24px;\n            transition: opacity 0.3s ease;\n        }\n\n        .loading-bar-wrap {\n            width: 220px;\n            height: 6px;\n            background: rgba(255,255,255,0.1);\n            border-radius: 3px;\n            overflow: hidden;\n            margin-bottom: 14px;\n        }\n\n        .loading-bar-fill {\n            height: 100%;\n            width: 0%;\n            border-radius: 3px;\n            background: linear-gradient(90deg, #FF69B4, #FF1493, #FFD700);\n            background-size: 200% 100%;\n            animation: barShimmer 1.5s linear infinite;\n            transition: width 0.4s ease;\n        }\n\n        @keyframes barShimmer {\n            0%   { background-position: 200% center; }\n            100% { background-position: -200% center; }\n        }\n\n        .loading-dots {\n            display: flex;\n            gap: 8px;\n            margin-top: 20px;\n        }\n\n        .loading-dot {\n            width: 8px; height: 8px;\n            border-radius: 50%;\n            background: rgba(255,255,255,0.4);\n            animation: dotBounce 1.2s ease-in-out infinite;\n        }\n        .loading-dot:nth-child(1) { animation-delay: 0s; }\n        .loading-dot:nth-child(2) { animation-delay: 0.2s; }\n        .loading-dot:nth-child(3) { animation-delay: 0.4s; }\n\n        @keyframes dotBounce {\n            0%, 100% { transform: translateY(0); background: rgba(255,255,255,0.35); }\n            50% { transform: translateY(-8px); background: #FF69B4; }\n        }\n\n        .loading-maple-leaf {\n            position: absolute;\n            font-size: 22px;\n            animation: leafFloat var(--dur, 6s) ease-in-out infinite var(--delay, 0s);\n            opacity: 0;\n        }\n\n        @keyframes leafFloat {\n            0%   { opacity: 0; transform: translateY(60px) rotate(0deg) scale(0.8); }\n            15%  { opacity: 0.7; }\n            85%  { opacity: 0.5; }\n            100% { opacity: 0; transform: translateY(-80px) rotate(180deg) scale(1.1); }\n        }\n\n        /* \u2500\u2500 Juice & Polish \u2500\u2500 */\n        @keyframes cardEquipFlash {\n            0%   { box-shadow: none; }\n            20%  { box-shadow: 0 0 0 3px #FF69B4, 0 0 22px 6px rgba(255,105,180,0.65); border-color: #FF1493; transform: scale(1.1); }\n            100% { box-shadow: none; transform: scale(1); }\n        }\n        .item-card.flash-equip { animation: cardEquipFlash 0.45s ease-out forwards; }\n\n        @keyframes slotNewEquip {\n            0%   { box-shadow: 0 0 0 3px rgba(255,105,180,0.95); border-color: #FF69B4; }\n            55%  { box-shadow: 0 0 18px 6px rgba(255,20,147,0.4); border-color: #FF1493; }\n            100% { box-shadow: 0 0 8px rgba(255,105,180,0.3); border-color: rgba(255,105,180,0.7); }\n        }\n        .equipped-slot.new-equip { animation: slotNewEquip 0.75s ease-out forwards; }\n\n        @keyframes starCountPop {\n            0%   { transform: scale(1) rotate(0deg); }\n            30%  { transform: scale(1.65) rotate(-12deg); }\n            60%  { transform: scale(0.85) rotate(6deg); }\n            80%  { transform: scale(1.15) rotate(-3deg); }\n            100% { transform: scale(1) rotate(0deg); }\n        }\n        .star-count-pop { animation: starCountPop 0.55s cubic-bezier(0.34,1.56,0.64,1); }\n\n        @keyframes btnClickPop {\n            0%   { transform: scale(1.05); }\n            30%  { transform: scale(0.87); }\n            65%  { transform: scale(1.13); }\n            85%  { transform: scale(0.97); }\n            100% { transform: scale(1.05); }\n        }\n        .category-btn.btn-click { animation: btnClickPop 0.3s cubic-bezier(0.34,1.56,0.64,1) forwards; }\n\n        @keyframes itemCardClickPop {\n            0%   { transform: scale(1.08); }\n            30%  { transform: scale(0.90); }\n            65%  { transform: scale(1.12); }\n            85%  { transform: scale(0.98); }\n            100% { transform: scale(1.05); }\n        }\n        .item-card.btn-click { animation: itemCardClickPop 0.3s cubic-bezier(0.34,1.56,0.64,1) forwards; }\n\n        @keyframes challengePulse {\n            0%   { transform: scale(1); }\n            40%  { transform: scale(1.1); }\n            70%  { transform: scale(0.96); }\n            100% { transform: scale(1); }\n        }\n        .gallery-btn.challenge-pulse { animation: challengePulse 1.8s ease-in-out infinite; }\n\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"game-canvas\"></canvas>\n        <div class=\"ui-overlay\">\n            \n            <div class=\"category-nav\" id=\"category-nav\"></div>\n            <div class=\"item-panel\" id=\"item-panel\">\n                <div class=\"item-panel-header\">\n                    <span class=\"item-panel-title\" id=\"panel-title\">Items</span>\n                    <button class=\"item-panel-close\" id=\"panel-close\">\u2715</button>\n                </div>\n                <div class=\"item-grid\" id=\"item-grid\"></div>\n            </div>\n            <div class=\"equipped-bar\" id=\"equipped-bar\"></div>\n            <div class=\"edit-controls\" id=\"edit-controls\" style=\"display:none;\"></div>\n        </div>\n    </div>\n\n    <script>\n        /*\n         * MapleStory Avatar Studio\n         * A character dress-up game with authentic MapleStory art assets.\n         */\n        async function run(mode) {\n            const gameWorld = document.getElementById('game-world');\n            const canvas = document.getElementById('game-canvas');\n\n            // \u2500\u2500 Loading Screen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            const loadingScreen = document.createElement('div');\n            loadingScreen.id = 'loading-screen';\n\n            // Starfield background\n            const starsBg = document.createElement('div');\n            starsBg.className = 'loading-stars-bg';\n            for (let i = 0; i < 55; i++) {\n                const star = document.createElement('div');\n                star.className = 'loading-star';\n                const sz = Math.random() * 2.5 + 0.8;\n                star.style.cssText = [\n                    `width:${sz}px;height:${sz}px`,\n                    `top:${Math.random()*100}%;left:${Math.random()*100}%`,\n                    `--dur:${(Math.random()*2.5+1.2).toFixed(2)}s`,\n                    `--delay:-${(Math.random()*3).toFixed(2)}s`\n                ].join(';');\n                starsBg.appendChild(star);\n            }\n            loadingScreen.appendChild(starsBg);\n\n            // Floating maple-leaf emojis\n            const leafEmojis = ['\ud83d\udc8e','\u2728','\ud83d\udc51','\ud83d\udc96','\ud83c\udf38','\ud83d\udc85','\u2b50','\ud83d\udc8b'];\n            for (let i = 0; i < 8; i++) {\n                const leaf = document.createElement('div');\n                leaf.className = 'loading-maple-leaf';\n                leaf.textContent = leafEmojis[i % leafEmojis.length];\n                leaf.style.cssText = [\n                    `left:${10 + Math.random()*80}%`,\n                    `bottom:${Math.random()*30}%`,\n                    `--dur:${(Math.random()*3+4).toFixed(2)}s`,\n                    `--delay:${(Math.random()*4).toFixed(2)}s`\n                ].join(';');\n                loadingScreen.appendChild(leaf);\n            }\n\n            // Orb\n            const orb = document.createElement('div');\n            orb.className = 'loading-orb';\n            orb.innerHTML = `\n                <div class=\"loading-orb-ring\"></div>\n                <div class=\"loading-orb-ring\"></div>\n                <div class=\"loading-orb-ring\"></div>\n                <div class=\"loading-orb-core\"></div>`;\n            loadingScreen.appendChild(orb);\n\n            // Title\n            const loadingTitle = document.createElement('div');\n            loadingTitle.className = 'loading-title';\n            loadingTitle.textContent = 'LOADING...';\n            loadingScreen.appendChild(loadingTitle);\n\n            // Subtitle (updated during load)\n            const loadingSubtitle = document.createElement('div');\n            loadingSubtitle.className = 'loading-subtitle';\n            loadingSubtitle.textContent = 'Initializing...';\n            loadingScreen.appendChild(loadingSubtitle);\n\n            // Progress bar\n            const barWrap = document.createElement('div');\n            barWrap.className = 'loading-bar-wrap';\n            const barFill = document.createElement('div');\n            barFill.className = 'loading-bar-fill';\n            barWrap.appendChild(barFill);\n            loadingScreen.appendChild(barWrap);\n\n            // Bouncing dots\n            const dotsRow = document.createElement('div');\n            dotsRow.className = 'loading-dots';\n            for (let i = 0; i < 3; i++) {\n                const d = document.createElement('div');\n                d.className = 'loading-dot';\n                dotsRow.appendChild(d);\n            }\n            loadingScreen.appendChild(dotsRow);\n\n            document.body.appendChild(loadingScreen);\n\n            function setLoadingStatus(text, progress) {\n                loadingSubtitle.style.opacity = '0';\n                setTimeout(() => {\n                    loadingSubtitle.textContent = text;\n                    loadingSubtitle.style.opacity = '1';\n                }, 150);\n                if (progress !== undefined) barFill.style.width = progress + '%';\n            }\n\n            function dismissLoadingScreen() {\n                loadingScreen.classList.add('fade-out');\n                setTimeout(() => loadingScreen.remove(), 700);\n            }\n            const ctx = canvas.getContext('2d');\n            // Keep sprite rendering crisp/consistent across mobile and desktop\n            ctx.imageSmoothingEnabled = true;\n            // Re-apply after any transform reset\n            function ensureSmoothing() {\n                ctx.imageSmoothingEnabled = true;\n            }\n            const config = window.gameConfig;\n\n            // Logical (CSS) dimensions used for all drawing coordinates\n            let logicalW = gameWorld.clientWidth;\n            let logicalH = gameWorld.clientHeight;\n\n            function resizeCanvas() {\n                // Cap DPR to 2 to save GPU fillrate on high-res mobile devices (fixes Android slow-downs)\n                const dpr = Math.min(window.devicePixelRatio || 1, 2);\n                logicalW = gameWorld.clientWidth;\n                logicalH = gameWorld.clientHeight;\n                canvas.width = logicalW * dpr;\n                canvas.height = logicalH * dpr;\n                canvas.style.width = logicalW + 'px';\n                canvas.style.height = logicalH + 'px';\n                ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n                ctx.imageSmoothingEnabled = true;\n            }\n            resizeCanvas();\n            window.addEventListener('resize', resizeCanvas);\n\n            // \u2500\u2500 Asset Loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            const IMG_IDS = [\n                'character_base', 'character_head', 'character_upper_body', 'character_lower_body', 'character_full_body',\n                'background_henesys',\n                'spritesheet_upper', 'spritesheet_lower', 'spritesheet_heads', 'spritesheet_fullbody', 'spritesheet_weapons', 'spritesheet_capes', 'spritesheet_pets'\n            ];\n            const assets = {};\n\n            const LOAD_STEPS = [\n                { ids: ['background_henesys'],                                      label: 'Loading world...',      pct: 10 },\n                { ids: ['character_base','character_head','character_full_body'],   label: 'Building character...', pct: 30 },\n                { ids: ['character_upper_body','character_lower_body'],             label: 'Dressing up...',        pct: 50 },\n                { ids: ['spritesheet_heads','spritesheet_upper','spritesheet_lower'],label: 'Filling wardrobe...',  pct: 68 },\n                { ids: ['spritesheet_fullbody','spritesheet_weapons'],              label: 'Arming heroes...',      pct: 82 },\n                { ids: ['spritesheet_capes','spritesheet_pets'],                    label: 'Summoning pets...',     pct: 96 }\n            ];\n\n            async function loadAssets() {\n                setLoadingStatus('Connecting to Maple World...', 5);\n                for (const step of LOAD_STEPS) {\n                    setLoadingStatus(step.label, step.pct);\n                    await Promise.all(step.ids.map(id => {\n                        const info = lib.getAsset(id);\n                        if (!info) return Promise.resolve();\n                        const img = new Image();\n                        img.crossOrigin = 'anonymous';\n                        img.src = info.url;\n                        assets[id] = img;\n                        return new Promise(resolve => { img.onload = resolve; img.onerror = resolve; });\n                    }));\n                }\n                setLoadingStatus('Ready!', 100);\n                await new Promise(r => setTimeout(r, 380));\n            }\n            await loadAssets();\n            dismissLoadingScreen();\n\n            // Equip punch animation state\n            let equipPunch = {}; // { [category]: { startTime, duration } }\n            const PUNCH_DURATION = 0.35; // seconds\n            const PUNCH_SCALE = 1.15; // max scale\n\n            // \u2500\u2500 Particle System \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            const equipParticles = [];\n            const heartParticles = [];\n\n            // Pre-allocated ambient sparkle pool \u2014 avoids per-frame heap allocs\n            const SPARKLE_POOL_SIZE = 28;\n            const ambientSparkles = Array.from({ length: SPARKLE_POOL_SIZE }, () => ({\n                active: false,\n                x: 0, y: 0, vx: 0, vy: 0,\n                maxAlpha: 0, size: 0,\n                life: 0, maxLife: 0,\n                color: '#FFD700'\n            }));\n            let charWiggle = null; // { startTime, duration }\n            let petWiggle  = null; // { startTime, duration }\n\n            function drawStarShape(c, x, y, r) {\n                const spikes = 4, ir = r * 0.42;\n                let rot = -Math.PI / 4;\n                const step = Math.PI / spikes;\n                c.beginPath();\n                c.moveTo(x + Math.cos(rot) * r, y + Math.sin(rot) * r);\n                for (let i = 0; i < spikes; i++) {\n                    rot += step; c.lineTo(x + Math.cos(rot) * ir, y + Math.sin(rot) * ir);\n                    rot += step; c.lineTo(x + Math.cos(rot) * r, y + Math.sin(rot) * r);\n                }\n                c.closePath(); c.fill();\n            }\n\n            function spawnEquipParticles(cx, cy) {\n                const colors = ['#FF69B4','#FF1493','#FFD700','#FF85C2','#FFB6C1','#FFFFFF','#E0B0FF','#FF6EC7','#FFC0CB'];\n                for (let i = 0; i < 18; i++) {\n                    if (equipParticles.length >= MAX_EQUIP_PARTICLES) break;\n                    const angle = (Math.PI * 2 * i / 18) + (Math.random() - 0.5) * 0.7;\n                    const speed = Math.random() * 6 + 2;\n                    equipParticles.push({\n                        x: cx + (Math.random() - 0.5) * 40,\n                        y: cy + (Math.random() - 0.5) * 40,\n                        vx: Math.cos(angle) * speed,\n                        vy: Math.sin(angle) * speed - 2,\n                        color: colors[Math.floor(Math.random() * colors.length)],\n                        size: Math.random() * 8 + 3,\n                        life: 1,\n                        decay: Math.random() * 0.022 + 0.016,\n                        isStar: Math.random() > 0.45\n                    });\n                }\n            }\n\n            function updateDrawEquipParticles() {\n                for (let i = equipParticles.length - 1; i >= 0; i--) {\n                    const p = equipParticles[i];\n                    p.x += p.vx; p.y += p.vy;\n                    p.vy += 0.14; p.vx *= 0.97;\n                    p.life -= p.decay;\n                    if (p.life <= 0) { equipParticles.splice(i, 1); continue; }\n                    ctx.save();\n                    ctx.globalAlpha = Math.max(0, p.life);\n                    ctx.fillStyle = p.color;\n                    if (p.isStar) drawStarShape(ctx, p.x, p.y, p.size);\n                    else { ctx.beginPath(); ctx.arc(p.x, p.y, p.size / 2, 0, Math.PI * 2); ctx.fill(); }\n                    ctx.restore();\n                }\n            }\n\n            function updateDrawAmbientSparkles() {\n                // Spawn: find a free slot in the pool instead of pushing a new object\n                if (Math.random() < 0.14) {\n                    for (let s = 0; s < SPARKLE_POOL_SIZE; s++) {\n                        if (!ambientSparkles[s].active) {\n                            const p = ambientSparkles[s];\n                            p.active   = true;\n                            p.x        = Math.random() * logicalW;\n                            p.y        = logicalH * 0.1 + Math.random() * logicalH * 0.75;\n                            p.vy       = -(Math.random() * 0.45 + 0.12);\n                            p.vx       = (Math.random() - 0.5) * 0.4;\n                            p.maxAlpha = Math.random() * 0.32 + 0.08;\n                            p.size     = Math.random() * 3.5 + 1.2;\n                            p.life     = 0;\n                            p.maxLife  = Math.random() * 150 + 80;\n                            p.color    = SPARKLE_COLORS[Math.floor(Math.random() * SPARKLE_COLORS.length)];\n                            break;\n                        }\n                    }\n                }\n                // Update & draw: deactivate instead of splice\n                for (let i = 0; i < SPARKLE_POOL_SIZE; i++) {\n                    const p = ambientSparkles[i];\n                    if (!p.active) continue;\n                    p.x += p.vx; p.y += p.vy; p.life++;\n                    if (p.life >= p.maxLife) { p.active = false; continue; }\n                    const half = p.maxLife / 2;\n                    const alpha = p.life < half\n                        ? (p.life / half) * p.maxAlpha\n                        : ((p.maxLife - p.life) / half) * p.maxAlpha;\n                    ctx.save();\n                    ctx.globalAlpha = alpha;\n                    ctx.fillStyle = p.color;\n                    drawStarShape(ctx, p.x, p.y, p.size);\n                    ctx.restore();\n                }\n            }\n\n            // \u2500\u2500 Heart Particle System \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            const HEART_EMOJIS = ['\ud83d\udc96','\ud83d\udc97','\ud83d\udc9d','\ud83e\ude77','\ud83d\udc95','\u2728','\ud83d\udc51','\ud83d\udc8e','\ud83c\udf38','\ud83d\udc85','\ud83d\udc8b','\ud83e\udda9','\ud83d\udcab','\u2b50'];\n\n            // Pre-cache emojis to avoid expensive canvas text rendering every frame (fixes Android lag)\n            const emojiCache = {};\n            function getEmojiCanvas(emoji) {\n                if (emojiCache[emoji]) return emojiCache[emoji];\n                const c = document.createElement('canvas');\n                c.width = 64; c.height = 64;\n                const cctx = c.getContext('2d');\n                cctx.font = '40px serif';\n                cctx.textAlign = 'center';\n                cctx.textBaseline = 'middle';\n                cctx.fillText(emoji, 32, 32);\n                emojiCache[emoji] = c;\n                return c;\n            }\n\n            // Hard caps so mobile GPU/CPU is never overwhelmed by spam\n            const MAX_HEART_PARTICLES  = 60;\n            const MAX_EQUIP_PARTICLES  = 80;\n\n            function spawnHearts(cx, cy, count) {\n                for (let i = 0; i < count; i++) {\n                    if (heartParticles.length >= MAX_HEART_PARTICLES) break;\n                    heartParticles.push({\n                        x: cx + (Math.random() - 0.5) * 55,\n                        y: cy + (Math.random() - 0.5) * 35,\n                        vx: (Math.random() - 0.5) * 2.8,\n                        vy: -(Math.random() * 3.2 + 1.8),\n                        emoji: HEART_EMOJIS[Math.floor(Math.random() * HEART_EMOJIS.length)],\n                        size: Math.random() * 22 + 14,\n                        life: 1,\n                        decay: Math.random() * 0.011 + 0.009,\n                        rot: (Math.random() - 0.5) * 0.5,\n                        rotV: (Math.random() - 0.5) * 0.09,\n                        scale: 0\n                    });\n                }\n            }\n\n            function updateDrawHeartParticles() {\n                for (let i = heartParticles.length - 1; i >= 0; i--) {\n                    const p = heartParticles[i];\n                    p.x   += p.vx;\n                    p.y   += p.vy;\n                    p.vy  += 0.035;\n                    p.vx  *= 0.985;\n                    p.rot += p.rotV;\n                    p.life -= p.decay;\n                    if (p.scale < 1) p.scale = Math.min(1, p.scale + 0.18);\n                    if (p.life <= 0) { heartParticles.splice(i, 1); continue; }\n                    const alpha = p.life < 0.35 ? p.life / 0.35 : 1;\n                    ctx.save();\n                    ctx.globalAlpha = alpha * alpha;\n                    ctx.translate(p.x, p.y);\n                    ctx.rotate(p.rot);\n                    const sz = p.size * p.scale;\n                    const scale = sz / 40;\n                    ctx.scale(scale, scale);\n                    const ec = getEmojiCanvas(p.emoji);\n                    ctx.drawImage(ec, -32, -32);\n                    ctx.restore();\n                }\n            }\n\n            // \u2500\u2500 Tap Hit Detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            // Throttle: on mobile, spam-tapping can flood the particle arrays.\n            // We allow at most one tap every 120 ms to keep the heap bounded.\n            const TAP_THROTTLE_MS = 120;\n            let _lastTapTime = 0;\n\n            function handleTap(lx, ly) {\n                const now = performance.now();\n                if (now - _lastTapTime < TAP_THROTTLE_MS) return;\n                _lastTapTime = now;\n                const charX = logicalW / 2;\n                const charY = logicalH * 0.46;\n                const rect  = getCharRect(charX, charY);\n\n                // Check pet hit first (smaller target)\n                if (equipped.pet) {\n                    const petItem = findItem('pet', equipped.pet);\n                    if (petItem) {\n                        const pScale  = Number(config.petScale || 0.35);\n                        const petBaseX= rect.x + rect.w * 0.5 + off('petXOffset');\n                        const petGroundY = rect.y + rect.h * 0.6 + off('petYOffset');\n                        let petW = rect.h * pScale, petH = rect.h * pScale;\n                        if (petItem.spritesheet && assets[petItem.spritesheet] && assets[petItem.spritesheet].naturalWidth > 0) {\n                            const sImg = assets[petItem.spritesheet];\n                            const cell = getSheetCellRect(sImg, petItem.col, petItem.row);\n                            petH = rect.h * pScale;\n                            petW = (cell.sw / cell.sh) * petH;\n                        }\n                        const hitPad = 20;\n                        if (lx >= petBaseX - hitPad && lx <= petBaseX + petW + hitPad &&\n                            ly >= petGroundY - hitPad && ly <= petGroundY + petH + hitPad) {\n                            petWiggle = { startTime: animTime, duration: 0.65 };\n                            spawnHearts(petBaseX + petW / 2, petGroundY + petH * 0.4, 10);\n                            return;\n                        }\n                    }\n                }\n\n                // Check character body hit\n                if (lx >= rect.x && lx <= rect.x + rect.w &&\n                    ly >= rect.y && ly <= rect.y + rect.h) {\n                    charWiggle = { startTime: animTime, duration: 0.6 };\n                    spawnHearts(lx, ly, 14);\n                }\n            }\n\n            function triggerPunch(category) {\n                const perPartCategories = ['pet', 'weapon', 'cape'];\n                const key = perPartCategories.includes(category) ? category : 'fullChar';\n                equipPunch[key] = { startTime: animTime, duration: PUNCH_DURATION };\n                // Sparkle burst at character center\n                spawnEquipParticles(logicalW / 2, logicalH * 0.38);\n            }\n\n            function getPunchScale(category) {\n                const p = equipPunch[category];\n                if (!p) return 1;\n                const elapsed = animTime - p.startTime;\n                if (elapsed > p.duration) { delete equipPunch[category]; return 1; }\n                const t = elapsed / p.duration;\n                // Quick scale up then ease back: sin curve\n                const scale = 1 + (PUNCH_SCALE - 1) * Math.sin(t * Math.PI);\n                return scale;\n            }\n\n            // \u2500\u2500 GPU Canvas Helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            // Converts a temporary canvas to a static <img> via data-URL, then\n            // immediately releases the GPU backing store by zeroing canvas dimensions.\n            function canvasToImg(cvs, cssStyle) {\n                const dataUrl = cvs.toDataURL('image/png');\n                cvs.width = 0;\n                cvs.height = 0;\n                const img = new Image();\n                img.src = dataUrl;\n                if (cssStyle) img.style.cssText = cssStyle;\n                return img;\n            }\n\n            // Runtime state\n            let equipped = {\n                face: config.startingOutfit.face || null,\n                top: config.startingOutfit.top || null,\n                bottom: config.startingOutfit.bottom || null,\n                dress: config.startingOutfit.dress || null,\n                weapon: config.startingOutfit.weapon || null,\n                cape: config.startingOutfit.cape || null,\n                pet: config.startingOutfit.pet || null\n            };\n            let gallery = []; // array of saved looks\n            let totalStars = 0; // cumulative stars earned from challenges\n            let activeCategory = null;\n\n            // Category definitions\n            const categories = [\n                { id: 'face', name: 'Face', icon: '\ud83d\ude0a' },\n                { id: 'top', name: 'Upper Body', icon: '\ud83d\udc55' },\n                { id: 'bottom', name: 'Lower Body', icon: '\ud83d\udc56' },\n                { id: 'dress', name: 'Full Body', icon: '\ud83d\udc57' },\n                { id: 'weapon', name: 'Handbag', icon: '\ud83d\udc5c' },\n                { id: 'cape', name: 'Aura', icon: '\u2728' },\n                { id: 'pet', name: 'Pet', icon: '\ud83d\udc3e' }\n            ];\n\n            // Build category nav\n            const categoryNav = document.getElementById('category-nav');\n            categoryNav.innerHTML = '';\n            categories.forEach(cat => {\n                const btn = document.createElement('button');\n                btn.className = 'category-btn';\n                btn.dataset.category = cat.id;\n                btn.innerHTML = `<span class=\"category-icon\">${cat.icon}</span>${cat.name}`;\n                categoryNav.appendChild(btn);\n            });\n\n            // Single delegated listener \u2014 always calls the current selectCategory (even after edit-mode reassignment)\n            categoryNav.addEventListener('click', e => {\n                const btn = e.target.closest('.category-btn');\n                if (!btn) return;\n                btn.classList.remove('btn-click');\n                void btn.offsetWidth;\n                btn.classList.add('btn-click');\n                selectCategory(btn.dataset.category);\n            });\n\n            // Equipped bar\n            const equippedBar = document.getElementById('equipped-bar');\n            const slotIcons = { face: '\ud83d\ude0a', top: '\ud83d\udc55', bottom: '\ud83d\udc56', dress: '\ud83d\udc57', weapon: '\ud83d\udc5c', cape: '\u2728', pet: '\ud83d\udc3e' };\n\n            function renderEquippedBar(highlightCategory) {\n                equippedBar.innerHTML = '';\n                categories.forEach(cat => {\n                    const itemId = equipped[cat.id];\n                    if (!itemId) return;\n                    const item = findItem(cat.id, itemId);\n                    if (!item) return;\n                    const slot = document.createElement('div');\n                    slot.className = 'equipped-slot';\n                    if (item.spritesheet && assets[item.spritesheet] && assets[item.spritesheet].naturalWidth > 0) {\n                        const dpr = window.devicePixelRatio || 1;\n                        const cvs = document.createElement('canvas');\n                        cvs.width = 66 * dpr; cvs.height = 66 * dpr;\n                        const c = cvs.getContext('2d');\n                        c.scale(dpr, dpr);\n                        const sImg = assets[item.spritesheet];\n                        const cell = getSheetCellRect(sImg, item.col, item.row);\n                        const scale = Math.min(66 / cell.sw, 66 / cell.sh);\n                        const dw = cell.sw * scale, dh = cell.sh * scale;\n                        c.drawImage(sImg, cell.sx, cell.sy, cell.sw, cell.sh, (66 - dw) / 2, (66 - dh) / 2, dw, dh);\n                        slot.appendChild(canvasToImg(cvs, 'width:66px;height:66px;pointer-events:none;'));\n                    } else {\n                        const label = document.createElement('span');\n                        label.className = 'equipped-slot-empty';\n                        label.textContent = slotIcons[cat.id] || '\u2022';\n                        slot.appendChild(label);\n                    }\n                    slot.addEventListener('click', () => {\n                        slot.classList.remove('btn-click');\n                        void slot.offsetWidth;\n                        slot.classList.add('btn-click');\n                        selectCategory(cat.id);\n                    });\n                    if (cat.id === highlightCategory) {\n                        requestAnimationFrame(() => slot.classList.add('new-equip'));\n                    }\n                    equippedBar.appendChild(slot);\n                });\n            }\n            renderEquippedBar();\n\n            // Load saved equipment state (play mode only)\n            if (mode !== 'edit') {\n                (async function loadEquippedState() {\n                    try {\n                        const response = await lib.getUserGameState();\n                        if (response.state) {\n                            // Restore saved equipment\n                            if (response.state.equipped) {\n                                equipped = response.state.equipped;\n                            } else {\n                                // Old format: state was directly the equipped object\n                                equipped = response.state;\n                            }\n                            // Restore gallery\n                            if (response.state.gallery) {\n                                gallery = response.state.gallery;\n                            }\n                            // Restore total stars\n                            if (response.state.totalStars !== undefined) {\n                                totalStars = response.state.totalStars;\n                            }\n                            renderEquippedBar();\n                            updateStarIndicator();\n                            lib.log('Loaded saved outfit');\n                        }\n                    } catch (error) {\n                        lib.log(`Failed to load outfit: ${error.message}`);\n                    }\n                })();\n            }\n\n            // Helper function to save equipped items (play mode only)\n            function saveEquipped() {\n                if (mode !== 'edit') {\n                    lib.saveUserGameState({ equipped, gallery, totalStars }).catch(err => {\n                        lib.log(`Failed to save outfit: ${err.message}`);\n                    });\n                }\n            }\n\n            // Helper function to save gallery (play mode only)\n            function saveGallery() {\n                if (mode !== 'edit') {\n                    lib.saveUserGameState({ equipped, gallery, totalStars }).catch(err => {\n                        lib.log(`Failed to save: ${err.message}`);\n                    });\n                }\n            }\n\n            // Star indicator element reference\n            let starIndicatorEl = null;\n\n            function updateStarIndicator(pop) {\n                if (starIndicatorEl) {\n                    const countEl = starIndicatorEl.querySelector('.star-count');\n                    countEl.textContent = totalStars;\n                    if (pop) {\n                        countEl.classList.remove('star-count-pop');\n                        void countEl.offsetWidth;\n                        countEl.classList.add('star-count-pop');\n                    }\n                }\n            }\n\n            // Item panel\n            const itemPanel = document.getElementById('item-panel');\n            const itemGrid = document.getElementById('item-grid');\n            const panelTitle = document.getElementById('panel-title');\n            const panelClose = document.getElementById('panel-close');\n\n            panelClose.addEventListener('click', () => {\n                activeCategory = null;\n                itemPanel.classList.remove('visible');\n                equippedBar.classList.remove('moved-top');\n                updateCategoryButtons();\n            });\n\n            // \u2500\u2500 Item grid scroll: mouse wheel + drag-to-scroll \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            // Mouse wheel \u2192 gentle horizontal scroll (one tick \u2248 one item width)\n            itemGrid.addEventListener('wheel', e => {\n                e.preventDefault();\n                const raw = e.deltaY !== 0 ? e.deltaY : e.deltaX;\n                // Normalize across deltaMode so one wheel tick scrolls ~120px (next card peeks in)\n                let scroll;\n                if (e.deltaMode === 1) {\n                    // Line mode: each line unit \u2192 ~40px, cap at 120\n                    scroll = Math.sign(raw) * Math.min(Math.abs(raw) * 40, 120);\n                } else {\n                    // Pixel mode: typical mouse tick is 100-120px raw; cap it at 120.\n                    // Small values (<50px) are trackpad smooth-scroll \u2014 pass through as-is.\n                    scroll = Math.abs(raw) >= 50 ? Math.sign(raw) * 120 : raw;\n                }\n                itemGrid.scrollLeft += scroll;\n            }, { passive: false });\n\n            // Drag-to-scroll (touch) \u2014 supplement native scroll with explicit handling\n            let gridTouchStartX = 0;\n            let gridTouchScrollStart = 0;\n\n            itemGrid.addEventListener('touchstart', e => {\n                gridTouchStartX = e.touches[0].clientX;\n                gridTouchScrollStart = itemGrid.scrollLeft;\n            }, { passive: true });\n\n            itemGrid.addEventListener('touchmove', e => {\n                const dx = e.touches[0].clientX - gridTouchStartX;\n                itemGrid.scrollLeft = gridTouchScrollStart - dx;\n            }, { passive: true });\n\n            function selectCategory(catId) {\n                activeCategory = catId;\n                const cat = categories.find(c => c.id === catId);\n                panelTitle.textContent = cat.name;\n                updateCategoryButtons();\n                renderItemGrid();\n                itemPanel.classList.add('visible');\n                equippedBar.classList.add('moved-top');\n            }\n\n            function updateCategoryButtons() {\n                document.querySelectorAll('.category-btn').forEach(btn => {\n                    btn.classList.toggle('active', btn.dataset.category === activeCategory);\n                });\n            }\n\n            // Ensure disabledItems exists in config\n            if (!config.disabledItems) config.disabledItems = {};\n\n            function isItemDisabled(category, itemId) {\n                return !!(config.disabledItems[category] && config.disabledItems[category][itemId]);\n            }\n\n            function toggleItemDisabled(category, itemId) {\n                if (!config.disabledItems[category]) config.disabledItems[category] = {};\n                config.disabledItems[category][itemId] = !config.disabledItems[category][itemId];\n                if (!config.disabledItems[category][itemId]) delete config.disabledItems[category][itemId];\n            }\n\n            function renderItemGrid() {\n                itemGrid.innerHTML = '';\n                if (!activeCategory) return;\n\n                // NONE card\n                const noneCard = document.createElement('div');\n                noneCard.className = 'item-card' + (equipped[activeCategory] === null ? ' equipped' : '');\n                const nonePreview = document.createElement('div');\n                nonePreview.className = 'item-card-preview';\n                nonePreview.style.cssText = 'display:flex;align-items:center;justify-content:center;font-size:36px;';\n                nonePreview.textContent = '\ud83d\udeab';\n                noneCard.appendChild(nonePreview);\n                const noneName = document.createElement('div');\n                noneName.className = 'item-card-name';\n                noneName.textContent = 'None';\n                noneCard.appendChild(noneName);\n                noneCard.addEventListener('click', () => { \n                    equipped[activeCategory] = null; \n                    renderItemGrid(); \n                    renderEquippedBar(); \n                    saveEquipped();\n                });\n                itemGrid.appendChild(noneCard);\n\n                const items = config.categories[activeCategory] || [];\n                // In play mode, filter out disabled items\n                const visibleItems = mode === 'edit' ? items : items.filter(item => !isItemDisabled(activeCategory, item.id));\n                visibleItems.forEach(item => {\n                    const card = document.createElement('div');\n                    const disabled = isItemDisabled(activeCategory, item.id);\n                    card.className = 'item-card' + (equipped[activeCategory] === item.id ? ' equipped' : '');\n                    if (disabled) card.style.opacity = '0.4';\n                    const preview = document.createElement('div');\n                    preview.className = 'item-card-preview';\n                    preview.appendChild(createItemPreview(activeCategory, item));\n                    card.appendChild(preview);\n                    const name = document.createElement('div');\n                    name.className = 'item-card-name';\n                    name.textContent = item.name;\n                    card.appendChild(name);\n\n                    // In edit mode, add enable/disable toggle\n                    if (mode === 'edit') {\n                        const toggle = document.createElement('button');\n                        toggle.style.cssText = 'position:absolute;top:4px;right:4px;border:none;border-radius:8px;padding:4px 8px;font-size:12px;font-weight:700;cursor:pointer;z-index:2;';\n                        toggle.style.background = disabled ? '#E74C3C' : '#27AE60';\n                        toggle.style.color = 'white';\n                        toggle.textContent = disabled ? 'OFF' : 'ON';\n                        toggle.addEventListener('click', (e) => {\n                            e.stopPropagation();\n                            toggleItemDisabled(activeCategory, item.id);\n                            renderItemGrid();\n                        });\n                        card.appendChild(toggle);\n                    }\n\n                    card.addEventListener('click', () => {\n                        card.classList.remove('flash-equip', 'btn-click');\n                        void card.offsetWidth;\n                        card.classList.add('flash-equip', 'btn-click');\n                        equipped[activeCategory] = item.id;\n                        triggerPunch(activeCategory);\n                        // If equipping a dress, clear top and bottom\n                        if (activeCategory === 'dress') {\n                            equipped.top = null;\n                            equipped.bottom = null;\n                        }\n                        // If equipping top or bottom, clear dress\n                        if (activeCategory === 'top' || activeCategory === 'bottom') {\n                            equipped.dress = null;\n                        }\n                        renderItemGrid();\n                        renderEquippedBar(activeCategory);\n                        saveEquipped();\n                    });\n                    itemGrid.appendChild(card);\n                });\n            }\n\n            function createItemPreview(category, item) {\n                const dpr = window.devicePixelRatio || 1;\n                const cvs = document.createElement('canvas');\n                cvs.width = 120 * dpr;\n                cvs.height = 120 * dpr;\n                const c = cvs.getContext('2d');\n                c.scale(dpr, dpr);\n                if (item.spritesheet && assets[item.spritesheet] && assets[item.spritesheet].naturalWidth > 0) {\n                    const img = assets[item.spritesheet];\n                    const cell = getSheetCellRect(img, item.col, item.row);\n                    const scale = Math.min(120 / cell.sw, 120 / cell.sh);\n                    const dw = cell.sw * scale;\n                    const dh = cell.sh * scale;\n                    c.drawImage(img, cell.sx, cell.sy, cell.sw, cell.sh, (120 - dw) / 2, (120 - dh) / 2, dw, dh);\n                } else if (item.assetId && assets[item.assetId] && assets[item.assetId].naturalWidth > 0) {\n                    c.drawImage(assets[item.assetId], 0, 0, 120, 120);\n                } else {\n                    drawItemOnCanvas(c, category, item, 60, 60, 0.75);\n                }\n                // Convert to static image and release GPU backing store\n                return canvasToImg(cvs, 'width:120px;height:120px;');\n            }\n\n            // Drawing functions for character parts\n            function drawItemOnCanvas(c, category, item, cx, cy, scale) {\n                c.save();\n                c.translate(cx, cy);\n                c.scale(scale, scale);\n                if (category === 'hair') drawHair(c, item, 0, -40);\n                else if (category === 'top') drawTop(c, item, 0, 20);\n                else if (category === 'bottom') drawBottom(c, item, 0, 60);\n                c.restore();\n            }\n\n            function drawHair(c, item, x, y) {\n                const color = item.color || '#3D2314';\n                const style = item.style || 'short';\n                c.fillStyle = color;\n                if (style === 'short') {\n                    c.beginPath();\n                    c.ellipse(x, y - 10, 42, 35, 0, Math.PI, 0);\n                    c.fill();\n                    c.fillRect(x - 42, y - 10, 84, 15);\n                } else if (style === 'long') {\n                    c.beginPath();\n                    c.ellipse(x, y - 10, 44, 38, 0, Math.PI, 0);\n                    c.fill();\n                    c.fillRect(x - 44, y - 10, 88, 15);\n                    // Long strands\n                    c.beginPath();\n                    c.ellipse(x - 35, y + 40, 12, 50, -0.1, 0, Math.PI * 2);\n                    c.fill();\n                    c.beginPath();\n                    c.ellipse(x + 35, y + 40, 12, 50, 0.1, 0, Math.PI * 2);\n                    c.fill();\n                } else if (style === 'spiky') {\n                    for (let i = -3; i <= 3; i++) {\n                        c.beginPath();\n                        c.moveTo(x + i * 12, y);\n                        c.lineTo(x + i * 12 - 8, y - 50 - Math.abs(i) * 5);\n                        c.lineTo(x + i * 12 + 8, y - 50 - Math.abs(i) * 5);\n                        c.closePath();\n                        c.fill();\n                    }\n                    c.beginPath();\n                    c.ellipse(x, y - 5, 42, 30, 0, Math.PI, 0);\n                    c.fill();\n                } else if (style === 'curly') {\n                    for (let i = 0; i < 8; i++) {\n                        const angle = (i / 8) * Math.PI * 2;\n                        const rx = Math.cos(angle) * 35;\n                        const ry = Math.sin(angle) * 30 - 15;\n                        c.beginPath();\n                        c.arc(x + rx, y + ry, 18, 0, Math.PI * 2);\n                        c.fill();\n                    }\n                } else if (style === 'ponytail') {\n                    c.beginPath();\n                    c.ellipse(x, y - 10, 42, 35, 0, Math.PI, 0);\n                    c.fill();\n                    c.fillRect(x - 42, y - 10, 84, 15);\n                    // Ponytail\n                    c.beginPath();\n                    c.ellipse(x + 30, y + 30, 15, 45, 0.3, 0, Math.PI * 2);\n                    c.fill();\n                    // Band\n                    c.fillStyle = '#FF6B9D';\n                    c.beginPath();\n                    c.arc(x + 25, y + 5, 6, 0, Math.PI * 2);\n                    c.fill();\n                }\n            }\n\n            function drawTop(c, item, x, y) {\n                const color = item.color || '#4A90D9';\n                const style = item.style || 'tshirt';\n                c.fillStyle = color;\n                if (style === 'tshirt') {\n                    // Body\n                    roundRect(c, x - 30, y - 20, 60, 50, 6);\n                    c.fill();\n                    // Sleeves\n                    c.fillRect(x - 45, y - 15, 20, 25);\n                    c.fillRect(x + 25, y - 15, 20, 25);\n                } else if (style === 'tank') {\n                    roundRect(c, x - 25, y - 20, 50, 50, 6);\n                    c.fill();\n                } else if (style === 'hoodie') {\n                    roundRect(c, x - 32, y - 22, 64, 55, 8);\n                    c.fill();\n                    c.fillRect(x - 48, y - 15, 22, 35);\n                    c.fillRect(x + 26, y - 15, 22, 35);\n                    // Hood detail\n                    c.fillStyle = darkenColor(color, 20);\n                    c.beginPath();\n                    c.arc(x, y - 18, 18, Math.PI, 0);\n                    c.fill();\n                    // Pocket\n                    c.fillStyle = darkenColor(color, 15);\n                    roundRect(c, x - 18, y + 12, 36, 16, 4);\n                    c.fill();\n                } else if (style === 'dress_shirt') {\n                    roundRect(c, x - 30, y - 20, 60, 52, 4);\n                    c.fill();\n                    c.fillRect(x - 45, y - 15, 20, 30);\n                    c.fillRect(x + 25, y - 15, 20, 30);\n                    // Buttons\n                    c.fillStyle = '#FFFFFF';\n                    for (let i = 0; i < 4; i++) {\n                        c.beginPath();\n                        c.arc(x, y - 10 + i * 12, 3, 0, Math.PI * 2);\n                        c.fill();\n                    }\n                    // Collar\n                    c.fillStyle = darkenColor(color, 10);\n                    c.beginPath();\n                    c.moveTo(x - 12, y - 20);\n                    c.lineTo(x, y - 10);\n                    c.lineTo(x + 12, y - 20);\n                    c.closePath();\n                    c.fill();\n                } else if (style === 'crop') {\n                    roundRect(c, x - 28, y - 18, 56, 35, 6);\n                    c.fill();\n                    c.fillRect(x - 40, y - 13, 16, 20);\n                    c.fillRect(x + 24, y - 13, 16, 20);\n                }\n            }\n\n            function drawBottom(c, item, x, y) {\n                const color = item.color || '#2C3E50';\n                const style = item.style || 'pants';\n                c.fillStyle = color;\n                if (style === 'pants') {\n                    c.beginPath();\n                    c.moveTo(x - 25, y - 10);\n                    c.lineTo(x + 25, y - 10);\n                    c.lineTo(x + 22, y + 45);\n                    c.lineTo(x + 5, y + 45);\n                    c.lineTo(x, y + 15);\n                    c.lineTo(x - 5, y + 45);\n                    c.lineTo(x - 22, y + 45);\n                    c.closePath();\n                    c.fill();\n                } else if (style === 'shorts') {\n                    c.beginPath();\n                    c.moveTo(x - 25, y - 10);\n                    c.lineTo(x + 25, y - 10);\n                    c.lineTo(x + 20, y + 20);\n                    c.lineTo(x + 3, y + 20);\n                    c.lineTo(x, y + 5);\n                    c.lineTo(x - 3, y + 20);\n                    c.lineTo(x - 20, y + 20);\n                    c.closePath();\n                    c.fill();\n                } else if (style === 'skirt') {\n                    c.beginPath();\n                    c.moveTo(x - 22, y - 10);\n                    c.lineTo(x + 22, y - 10);\n                    c.lineTo(x + 35, y + 35);\n                    c.lineTo(x - 35, y + 35);\n                    c.closePath();\n                    c.fill();\n                    // Pleats\n                    c.strokeStyle = darkenColor(color, 20);\n                    c.lineWidth = 1.5;\n                    for (let i = -2; i <= 2; i++) {\n                        c.beginPath();\n                        c.moveTo(x + i * 10, y - 5);\n                        c.lineTo(x + i * 14, y + 35);\n                        c.stroke();\n                    }\n                } else if (style === 'joggers') {\n                    c.beginPath();\n                    c.moveTo(x - 25, y - 10);\n                    c.lineTo(x + 25, y - 10);\n                    c.quadraticCurveTo(x + 28, y + 20, x + 18, y + 42);\n                    c.lineTo(x + 5, y + 42);\n                    c.lineTo(x, y + 12);\n                    c.lineTo(x - 5, y + 42);\n                    c.lineTo(x - 18, y + 42);\n                    c.quadraticCurveTo(x - 28, y + 20, x - 25, y - 10);\n                    c.closePath();\n                    c.fill();\n                    // Cuffs\n                    c.fillStyle = darkenColor(color, 15);\n                    c.fillRect(x - 18, y + 38, 13, 6);\n                    c.fillRect(x + 5, y + 38, 13, 6);\n                }\n            }\n\n            function roundRect(c, x, y, w, h, r) {\n                c.beginPath();\n                c.moveTo(x + r, y);\n                c.lineTo(x + w - r, y);\n                c.quadraticCurveTo(x + w, y, x + w, y + r);\n                c.lineTo(x + w, y + h - r);\n                c.quadraticCurveTo(x + w, y + h, x + w - r, y + h);\n                c.lineTo(x + r, y + h);\n                c.quadraticCurveTo(x, y + h, x, y + h - r);\n                c.lineTo(x, y + r);\n                c.quadraticCurveTo(x, y, x + r, y);\n                c.closePath();\n            }\n\n            function darkenColor(hex, amount) {\n                let r = parseInt(hex.slice(1, 3), 16);\n                let g = parseInt(hex.slice(3, 5), 16);\n                let b = parseInt(hex.slice(5, 7), 16);\n                r = Math.max(0, r - amount);\n                g = Math.max(0, g - amount);\n                b = Math.max(0, b - amount);\n                return `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`;\n            }\n\n            // Returns the character draw rect centered at cx,cy\n            function getCharRect(cx, cy) {\n                const scale = (config.characterScale !== undefined ? config.characterScale : 1.0);\n                const h = Math.min(logicalH * 0.62, 600) * scale;\n                const w = h * 0.5;\n                return { x: cx - w / 2, y: cy - h / 2, w, h };\n            }\n\n            // Returns a robust cell rect for 5x5 spritesheets using rounded boundaries.\n            // This avoids fractional source sampling (a common cause of blur/size drift on mobile).\n            function getSheetCellRect(img, col, row) {\n                const x0 = Math.round((col * img.naturalWidth) / 5);\n                const x1 = Math.round(((col + 1) * img.naturalWidth) / 5);\n                const y0 = Math.round((row * img.naturalHeight) / 5);\n                const y1 = Math.round(((row + 1) * img.naturalHeight) / 5);\n                return { sx: x0, sy: y0, sw: x1 - x0, sh: y1 - y0 };\n            }\n\n            // Draw image preserving aspect ratio, centered within rect, with optional yOffset and xOffset\n            function drawImageLayer(id, rect, yOffset, xOffset) {\n                const img = assets[id];\n                if (!img || img.naturalWidth === 0) return;\n                const yo = yOffset || 0;\n                const xo = xOffset || 0;\n                const iw = img.naturalWidth;\n                const ih = img.naturalHeight;\n                const scale = Math.min(rect.w / iw, rect.h / ih);\n                const dw = iw * scale;\n                const dh = ih * scale;\n                const dx = rect.x + (rect.w - dw) / 2 + xo;\n                const dy = rect.y + (rect.h - dh) / 2 + yo;\n                ctx.drawImage(img, dx, dy, dw, dh);\n            }\n\n            // Draw image preserving aspect ratio, placed relative to rect with offsets, fit by height\n            function drawImageLayerFitHeight(id, rect, yOffset, xOffset) {\n                const img = assets[id];\n                if (!img || img.naturalWidth === 0) return;\n                const yo = yOffset || 0;\n                const xo = xOffset || 0;\n                const scale = rect.h / img.naturalHeight;\n                const dw = img.naturalWidth * scale;\n                const dh = rect.h;\n                const dx = rect.x + (rect.w - dw) / 2 + xo;\n                const dy = rect.y + yo;\n                ctx.drawImage(img, dx, dy, dw, dh);\n            }\n\n            // Draw image using a FIXED reference scale so all clothing items render at identical size\n            // regardless of their own pixel dimensions. refScale = rect.h / refImg.naturalHeight.\n            function drawImageLayerRefScale(id, rect, refScale, yOffset, xOffset) {\n                const img = assets[id];\n                if (!img || img.naturalWidth === 0) return;\n                const yo = yOffset || 0;\n                const xo = xOffset || 0;\n                const dw = img.naturalWidth * refScale;\n                const dh = img.naturalHeight * refScale;\n                const dx = rect.x + (rect.w - dw) / 2 + xo;\n                const dy = rect.y + yo;\n                ctx.drawImage(img, dx, dy, dw, dh);\n            }\n\n            // Draw image at the EXACT same rendered size as the reference image (refImg).\n            // This guarantees pixel-perfect size matching regardless of the asset's own canvas dimensions.\n            // Use this for full-body overlays (dress) that must cover the character body 1:1.\n            function drawImageLayerMatchRef(id, rect, refImg, refScale, yOffset, xOffset) {\n                const img = assets[id];\n                if (!img || img.naturalWidth === 0) return;\n                if (!refImg || refImg.naturalWidth === 0) return;\n                const yo = yOffset || 0;\n                const xo = xOffset || 0;\n                // Drawn size of the reference image\n                const dw = refImg.naturalWidth * refScale;\n                const dh = refImg.naturalHeight * refScale;\n                const dx = rect.x + (rect.w - dw) / 2 + xo;\n                const dy = rect.y + yo;\n                ctx.drawImage(img, dx, dy, dw, dh);\n            }\n\n            // Safe offset reader \u2014 returns 0 for undefined/null, correctly handles negative values\n            function off(key) {\n                const v = config[key];\n                return (v !== undefined && v !== null) ? Number(v) : 0;\n            }\n\n            // Draw the full character using image layers\n            // Layer order (bottom to top):\n            //   0. Character shadow\n            //   1. Cape (behind everything)\n            //   2. character_full_body  \u2014 full base body, ALWAYS drawn (NONE state)\n            //   3. character_lower_body \u2014 legs/feet overlay, ALWAYS drawn (NONE state)\n            //   4. character_upper_body \u2014 torso/arms overlay, ALWAYS drawn (NONE state)\n            //   5. Bottom clothing overlay (if equipped and no dress)\n            //   6. Top clothing overlay (if equipped and no dress)\n            //   7. Dress overlay (full body clothing, replaces top+bottom)\n            //   8. character_head \u2014 ALWAYS drawn\n            //   9. Face overlay\n            //  10. Weapon\n            //  11. Pet shadow + Pet (with independent bounce)\n            function drawCharacter(cx, cy) {\n                const rect = getCharRect(cx, cy);\n\n                // 0. Character shadow \u2014 static ellipse at feet\n                const charShadowX = rect.x + rect.w / 2 + off('shadowXOffset');\n                const charShadowY = rect.y + rect.h * 0.97 + off('shadowYOffset');\n                const shadowSizeScale = (config.shadowSize !== undefined ? Number(config.shadowSize) : 1.0);\n                const charShadowW = rect.w * 0.75 * shadowSizeScale;\n                const charShadowH = rect.h * 0.06 * shadowSizeScale;\n                ctx.save();\n                ctx.fillStyle = 'rgba(0, 0, 0, 0.22)';\n                ctx.beginPath();\n                ctx.ellipse(charShadowX, charShadowY, charShadowW / 2, charShadowH / 2, 0, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.restore();\n\n                // \u2500\u2500 Aura: drawn OUTSIDE character breathing, with its own animation \u2500\u2500\n                if (equipped.cape) {\n                    const item = findItem('cape', equipped.cape);\n                    if (item) {\n                        ctx.save();\n                        const cScale = (config.capeScale !== undefined ? Number(config.capeScale) : 1.0);\n                        const capePunch = getPunchScale('cape');\n                        const capePivotX = rect.x + rect.w / 2;\n                        const capePivotY = rect.y + rect.h / 2;\n\n                        // Aura-specific animation: slow pulse + gentle float + subtle rotation\n                        const auraPulse = 1 + Math.sin(animTime * 1.8) * 0.06; // gentle scale pulse\n                        const auraFloat = Math.sin(animTime * 1.2) * 8; // float up/down\n                        const auraRot = Math.sin(animTime * 0.7) * 0.04; // subtle rotation\n                        const auraAlpha = 0.75 + Math.sin(animTime * 2.5) * 0.2; // opacity shimmer\n\n                        ctx.translate(capePivotX, capePivotY + auraFloat);\n                        ctx.rotate(auraRot);\n                        ctx.scale(auraPulse * capePunch, auraPulse * capePunch);\n                        ctx.globalAlpha = Math.max(0.4, Math.min(1, auraAlpha));\n                        ctx.translate(-capePivotX, -capePivotY);\n\n                        if (item.spritesheet && assets[item.spritesheet] && assets[item.spritesheet].naturalWidth > 0) {\n                            const sImg = assets[item.spritesheet];\n                            const cell = getSheetCellRect(sImg, item.col, item.row);\n                            const capeH = rect.h * cScale;\n                            const capeW = (cell.sw / cell.sh) * capeH;\n                            const capeX = rect.x + (rect.w - capeW) / 2 + off('capeXOffset');\n                            const capeY = rect.y + (rect.h - capeH) / 2 + off('capeYOffset');\n                            ctx.drawImage(sImg, cell.sx, cell.sy, cell.sw, cell.sh, capeX, capeY, capeW, capeH);\n                        } else if (item.assetId && assets[item.assetId] && assets[item.assetId].naturalWidth > 0) {\n                            const img = assets[item.assetId];\n                            const capeH = rect.h * cScale;\n                            const capeW = (img.naturalWidth / img.naturalHeight) * capeH;\n                            const capeX = rect.x + (rect.w - capeW) / 2 + off('capeXOffset');\n                            const capeY = rect.y + (rect.h - capeH) / 2 + off('capeYOffset');\n                            ctx.drawImage(img, capeX, capeY, capeW, capeH);\n                        }\n                        ctx.restore();\n                    }\n                }\n\n                // Start breathing scale + equip punch for the character body\n                ctx.save();\n                // Gently stretch upwards from baseline (0 to 0.03)\n                const stretch = (Math.sin(animTime * 2.4) + 1) * 0.015;\n                const charPunch = getPunchScale('fullChar');\n                // Tap wiggle\n                let wiggleOffsetX = 0, wiggleScalePop = 1;\n                if (charWiggle) {\n                    const elapsed = animTime - charWiggle.startTime;\n                    if (elapsed > charWiggle.duration) {\n                        charWiggle = null;\n                    } else {\n                        const t = elapsed / charWiggle.duration;\n                        wiggleOffsetX  = Math.sin(t * Math.PI * 5) * 22 * (1 - t);\n                        wiggleScalePop = 1 + Math.sin(t * Math.PI) * 0.18;\n                    }\n                }\n                ctx.translate(charShadowX, charShadowY);\n                const sway = Math.sin(animTime * 0.85) * 2.5;\n                ctx.translate(sway + wiggleOffsetX, 0);\n                ctx.scale(charPunch * wiggleScalePop, (1 + stretch) * charPunch * wiggleScalePop);\n                ctx.translate(-sway - wiggleOffsetX - charShadowX, -charShadowY);\n\n                const hasDress = !!(equipped.dress && findItem('dress', equipped.dress));\n\n                // Compute a single reference scale from character_full_body so ALL\n                // clothing/body layers share the exact same pixel scale, guaranteeing\n                // perfect size-matching regardless of each asset's own pixel dimensions.\n                const refImg = assets['character_full_body'];\n                const refScale = (refImg && refImg.naturalHeight > 0)\n                    ? rect.h / refImg.naturalHeight\n                    : rect.h / 600;\n\n                // (Aura is drawn independently above, outside character breathing animation)\n\n                // Helper: apply punch scale around a draw point\n                function applyPunchTransform(category, pivotX, pivotY) {\n                    const s = getPunchScale(category);\n                    if (s !== 1) {\n                        ctx.translate(pivotX, pivotY);\n                        ctx.scale(s, s);\n                        ctx.translate(-pivotX, -pivotY);\n                    }\n                }\n\n                // 2. Head/Face \u2014 drawn FIRST (below all base sprites) so bases render on top\n                // drawSpritesheetCellCentered keeps each cell at its natural 1/5-sheet dimensions\n                // scaled by refScale, so the artist's internal spacing is perfectly preserved.\n                const headImg = assets['character_head'];\n                ctx.save();\n                \n                if (equipped.face) {\n                    const faceItem = findItem('face', equipped.face);\n                    if (faceItem && faceItem.spritesheet && assets[faceItem.spritesheet] && assets[faceItem.spritesheet].naturalWidth > 0) {\n                        const faceRowExtraY = (faceItem.row === 1 ? off('headRow1YOffset') :\n                                           faceItem.row === 2 ? off('headRow2YOffset') :\n                                           faceItem.row === 3 ? off('headRow3YOffset') :\n                                           faceItem.row === 4 ? off('headRow4YOffset') : 0);\n                        drawSpritesheetCellCentered(faceItem.spritesheet, faceItem.col, faceItem.row, rect, refScale, off('headYOffset') + faceRowExtraY, off('headXOffset'));\n                    } else if (headImg && headImg.naturalWidth > 0) {\n                        drawImageLayerRefScale('character_head', rect, refScale, off('headYOffset'), off('headXOffset'));\n                    }\n                } else if (headImg && headImg.naturalWidth > 0) {\n                    drawImageLayerRefScale('character_head', rect, refScale, off('headYOffset'), off('headXOffset'));\n                }\n                ctx.restore();\n\n                // 3. character_full_body \u2014 the complete base body, ALWAYS drawn first\n                // 3. character_lower_body \u2014 lower body overlay, ALWAYS drawn on top of full_body\n                // 4. character_upper_body \u2014 upper body overlay, ALWAYS drawn on top\n                //    Together these three form the full visible base (NONE selected state).\n                const fullBodyImg  = assets['character_full_body'];\n                const lowerBodyImg = assets['character_lower_body'];\n                const upperBodyImg = assets['character_upper_body'];\n\n                // Determine which base system to use:\n                //   hasDress        \u2192 hide ALL base sprites (dress covers everything)\n                //   hasTop||hasBottom \u2192 \"separate\" mode: show upper+lower bases (minus the clothed slot), hide full_body\n                //   neither          \u2192 \"default\" mode: show only character_full_body\n                const hasTop    = !!(equipped.top    && findItem('top',    equipped.top));\n                const hasBottom = !!(equipped.bottom && findItem('bottom', equipped.bottom));\n                const useSeparateBases = hasTop || hasBottom;\n\n                // Offset baselines for each clothing zone\n                const dressBaseY = off('fullBodyYOffset');\n                const dressBaseX = off('fullBodyXOffset');\n                const topBaseY   = off('upperBodyYOffset');\n                const topBaseX   = off('upperBodyXOffset');\n                const botBaseY   = off('lowerBodyYOffset');\n                const botBaseX   = off('lowerBodyXOffset');\n\n                if (!hasDress) {\n                    if (useSeparateBases) {\n                        // Separate-base mode \u2014 full_body is hidden.\n                        // Layer order: lower base \u2192 bottom clothing \u2192 upper base \u2192 top clothing\n                        // This ensures the upper body base always sits on top of bottom clothing.\n\n                        // Step 1: Lower body base (only if no bottom clothing covers it)\n                        if (!hasBottom && lowerBodyImg && lowerBodyImg.naturalWidth > 0) {\n                            drawImageLayerRefScale('character_lower_body', rect, refScale, off('lowerBodyYOffset'), off('lowerBodyXOffset'));\n                        }\n\n                        // Step 2: Bottom clothing overlay\n                        if (hasBottom) {\n                            const item = findItem('bottom', equipped.bottom);\n                            if (item) {\n                                ctx.save();\n                                \n                                if (item.spritesheet && assets[item.spritesheet] && assets[item.spritesheet].naturalWidth > 0) {\n                                    drawSpritesheetItemOnChar(item.spritesheet, item.col, item.row, rect, refScale, botBaseY + off('bottomYOffset'), botBaseX);\n                                } else if (item.assetId && assets[item.assetId] && assets[item.assetId].naturalWidth > 0) {\n                                    drawImageLayerRefScale(item.assetId, rect, refScale, botBaseY + off('bottomYOffset'), botBaseX);\n                                } else {\n                                    ctx.save(); ctx.translate(cx, cy); ctx.scale(2.2, 2.2);\n                                    drawBottom(ctx, item, 0, 62); ctx.restore();\n                                }\n                                ctx.restore();\n                            }\n                        }\n\n                        // Step 3: Upper body base (drawn AFTER bottom clothing so it sits on top)\n                        if (!hasTop && upperBodyImg && upperBodyImg.naturalWidth > 0) {\n                            drawImageLayerRefScale('character_upper_body', rect, refScale, off('upperBodyYOffset'), off('upperBodyXOffset'));\n                        }\n\n                        // Step 4: Top clothing overlay\n                        if (hasTop) {\n                            const item = findItem('top', equipped.top);\n                            if (item) {\n                                ctx.save();\n                                \n                                if (item.spritesheet && assets[item.spritesheet] && assets[item.spritesheet].naturalWidth > 0) {\n                                    drawSpritesheetItemOnChar(item.spritesheet, item.col, item.row, rect, refScale, topBaseY + off('topYOffset'), topBaseX);\n                                } else if (item.assetId && assets[item.assetId] && assets[item.assetId].naturalWidth > 0) {\n                                    drawImageLayerRefScale(item.assetId, rect, refScale, topBaseY + off('topYOffset'), topBaseX);\n                                } else {\n                                    ctx.save(); ctx.translate(cx, cy); ctx.scale(2.2, 2.2);\n                                    drawTop(ctx, item, 0, 35); ctx.restore();\n                                }\n                                ctx.restore();\n                            }\n                        }\n                    } else {\n                        // Default / nothing equipped \u2014 show full body base only (no upper/lower)\n                        if (fullBodyImg && fullBodyImg.naturalWidth > 0) {\n                            drawImageLayerRefScale('character_full_body', rect, refScale, off('fullBodyYOffset'), off('fullBodyXOffset'));\n                        } else {\n                            const base = assets['character_base'];\n                            if (base && base.naturalWidth > 0) drawImageLayerFitHeight('character_base', rect, 0, 0);\n                            else drawFallbackCharacter(cx, cy);\n                        }\n                    }\n                }\n                // hasDress: no base sprites drawn at all \u2014 dress overlay covers the body\n\n                // 7. Dress overlay \u2014 drawn ON TOP of head and body bases\n                //    Supports both spritesheet cells and standalone assetId images.\n                if (hasDress) {\n                    const item = findItem('dress', equipped.dress);\n                    if (item) {\n                        ctx.save();\n                        \n                        if (item.spritesheet && assets[item.spritesheet] && assets[item.spritesheet].naturalWidth > 0) {\n                            drawSpritesheetItemOnChar(item.spritesheet, item.col, item.row, rect, refScale, dressBaseY + off('dressYOffset'), dressBaseX);\n                        } else if (item.assetId && assets[item.assetId] && assets[item.assetId].naturalWidth > 0) {\n                            drawImageLayerMatchRef(item.assetId, rect, refImg, refScale, dressBaseY + off('dressYOffset'), dressBaseX);\n                        }\n                        ctx.restore();\n                    }\n                }\n\n                // 9. Weapon \u2014 positioned to the side\n                if (equipped.weapon) {\n                    const item = findItem('weapon', equipped.weapon);\n                    if (item) {\n                        ctx.save();\n                        const wScale = (config.weaponScale !== undefined ? Number(config.weaponScale) : 0.55);\n                        const weaponX = rect.x + rect.w * 0.52 + off('weaponXOffset');\n                        const weaponY = rect.y + rect.h * 0.28 + off('weaponYOffset');\n                        \n                        const weaponPunch = getPunchScale('weapon');\n                        const weaponPivotX = weaponX;\n                        const weaponPivotY = weaponY;\n                        if (weaponPunch !== 1) {\n                            ctx.translate(weaponPivotX, weaponPivotY);\n                            ctx.scale(weaponPunch, weaponPunch);\n                            ctx.translate(-weaponPivotX, -weaponPivotY);\n                        }\n                        if (item.spritesheet && assets[item.spritesheet] && assets[item.spritesheet].naturalWidth > 0) {\n                            const sImg = assets[item.spritesheet];\n                            const cell = getSheetCellRect(sImg, item.col, item.row);\n                            const weaponH = rect.h * wScale;\n                            const weaponW = (cell.sw / cell.sh) * weaponH;\n                            ctx.drawImage(sImg, cell.sx, cell.sy, cell.sw, cell.sh, weaponX, weaponY, weaponW, weaponH);\n                        } else if (item.assetId && assets[item.assetId] && assets[item.assetId].naturalWidth > 0) {\n                            const img = assets[item.assetId];\n                            const weaponH = rect.h * wScale;\n                            const weaponW = (img.naturalWidth / img.naturalHeight) * weaponH;\n                            ctx.drawImage(img, weaponX, weaponY, weaponW, weaponH);\n                        }\n                        ctx.restore();\n                    }\n                }\n\n                ctx.restore(); // End breathing scale\n\n                // 10. Pet \u2014 beside character with independent bounce and dynamic shadow\n                if (equipped.pet) {\n                    const item = findItem('pet', equipped.pet);\n                    if (item) {\n                        const pScale = (config.petScale !== undefined ? Number(config.petScale) : 0.35);\n                        const petBaseX = rect.x + rect.w * 0.5 + off('petXOffset');\n                        const petGroundY = rect.y + rect.h * 0.6 + off('petYOffset');\n\n                        // Independent pet bounce (different frequency from character)\n                        const petBounceAmt = Math.abs(Math.sin(animTime * 3.2));\n                        const petBounceY = -petBounceAmt * 14; // bounces UP\n\n                        // Calculate pet dimensions first for shadow\n                        let petW = 0, petH = 0;\n                        if (item.spritesheet && assets[item.spritesheet] && assets[item.spritesheet].naturalWidth > 0) {\n                            const sImg = assets[item.spritesheet];\n                            const petCell = getSheetCellRect(sImg, item.col, item.row);\n                            petH = rect.h * pScale;\n                            petW = (petCell.sw / petCell.sh) * petH;\n                        } else if (item.assetId && assets[item.assetId] && assets[item.assetId].naturalWidth > 0) {\n                            const img = assets[item.assetId];\n                            petH = rect.h * pScale;\n                            petW = (img.naturalWidth / img.naturalHeight) * petH;\n                        }\n\n                        // Dynamic pet shadow \u2014 scales down when pet is in the air\n                        if (petW > 0 && petH > 0) {\n                            const shadowScale = 1 - petBounceAmt * 0.35; // 1.0 on ground, 0.65 at peak\n                            const petShadowW = petW * 0.6 * shadowScale;\n                            const petShadowH = petH * 0.12 * shadowScale;\n                            const petShadowX = petBaseX + petW / 2;\n                            const petShadowY = petGroundY + petH * 0.92;\n                            ctx.save();\n                            ctx.globalAlpha = 0.18 + 0.1 * (1 - petBounceAmt); // more opaque on ground\n                            ctx.fillStyle = 'rgba(0, 0, 0, 1)';\n                            ctx.beginPath();\n                            ctx.ellipse(petShadowX, petShadowY, petShadowW / 2, petShadowH / 2, 0, 0, Math.PI * 2);\n                            ctx.fill();\n                            ctx.restore();\n                        }\n\n                        // Draw pet at bounced position with squash\n                        const petDrawY = petGroundY + petBounceY;\n                        const petPunch = getPunchScale('pet');\n                        \n                        // Calculate squash factor based on bounce\n                        const squashAmount = Math.max(0, 1 - petBounceAmt * 5); // 0 to 1\n                        const scaleX = (1 + squashAmount * 0.06);\n                        const scaleY = (1 - squashAmount * 0.06);\n\n                        // Pet tap wiggle\n                        let petWiggleScale = 1, petWiggleRot = 0;\n                        if (petWiggle) {\n                            const elapsed = animTime - petWiggle.startTime;\n                            if (elapsed > petWiggle.duration) {\n                                petWiggle = null;\n                            } else {\n                                const t = elapsed / petWiggle.duration;\n                                petWiggleScale = 1 + Math.sin(t * Math.PI) * 0.38;\n                                petWiggleRot   = Math.sin(t * Math.PI * 5) * 0.38 * (1 - t);\n                            }\n                        }\n                        ctx.save();\n                        // Translate to bottom center of pet\n                        ctx.translate(petBaseX + petW / 2, petDrawY + petH);\n                        ctx.rotate(petWiggleRot);\n                        ctx.scale(scaleX * petPunch * petWiggleScale, scaleY * petPunch * petWiggleScale);\n\n                        if (item.spritesheet && assets[item.spritesheet] && assets[item.spritesheet].naturalWidth > 0) {\n                            const sImg = assets[item.spritesheet];\n                            const petCell2 = getSheetCellRect(sImg, item.col, item.row);\n                            ctx.drawImage(sImg, petCell2.sx, petCell2.sy, petCell2.sw, petCell2.sh, -petW / 2, -petH, petW, petH);\n                        } else if (item.assetId && assets[item.assetId] && assets[item.assetId].naturalWidth > 0) {\n                            const img = assets[item.assetId];\n                            ctx.drawImage(img, -petW / 2, -petH, petW, petH);\n                        }\n                        \n                        ctx.restore();\n                    }\n                }\n            }\n\n            // Fallback canvas character (no assets loaded)\n            function drawFallbackCharacter(cx, cy) {\n                const scale = 2.2;\n                ctx.save();\n                ctx.translate(cx, cy);\n                ctx.scale(scale, scale);\n                ctx.fillStyle = config.skinColor || '#FFDAB9';\n                ctx.fillRect(-12, 80, 10, 50);\n                ctx.fillRect(2, 80, 10, 50);\n                roundRect(ctx, -28, 20, 56, 65, 8); ctx.fill();\n                ctx.fillStyle = '#FFFFFF';\n                roundRect(ctx, -24, 55, 48, 30, 4); ctx.fill();\n                ctx.fillStyle = config.skinColor || '#FFDAB9';\n                ctx.fillRect(-40, 25, 14, 40); ctx.fillRect(26, 25, 14, 40);\n                ctx.beginPath(); ctx.arc(-33, 67, 8, 0, Math.PI * 2); ctx.fill();\n                ctx.beginPath(); ctx.arc(33, 67, 8, 0, Math.PI * 2); ctx.fill();\n                ctx.beginPath(); ctx.ellipse(0, -20, 38, 40, 0, 0, Math.PI * 2); ctx.fill();\n                ctx.fillStyle = '#2D2040';\n                ctx.beginPath(); ctx.ellipse(-12, -20, 6, 8, 0, 0, Math.PI * 2); ctx.fill();\n                ctx.beginPath(); ctx.ellipse(12, -20, 6, 8, 0, 0, Math.PI * 2); ctx.fill();\n                ctx.fillStyle = '#FFFFFF';\n                ctx.beginPath(); ctx.arc(-10, -23, 3, 0, Math.PI * 2); ctx.fill();\n                ctx.beginPath(); ctx.arc(14, -23, 3, 0, Math.PI * 2); ctx.fill();\n                ctx.strokeStyle = '#C0392B'; ctx.lineWidth = 2;\n                ctx.beginPath(); ctx.arc(0, -8, 6, 0.1, Math.PI - 0.1); ctx.stroke();\n                ctx.restore();\n            }\n\n            function findItem(category, id) {\n                return (config.categories[category] || []).find(i => i.id === id);\n            }\n\n            // Draw a single cell from a spritesheet at a VIEWPORT-RELATIVE size.\n            // Destination height = rect.h (full character height), width from cell aspect ratio.\n            // This is RESOLUTION-INDEPENDENT: the spritesheet's pixel count does NOT affect\n            // rendered size, so compressed CDN variants on mobile match desktop exactly.\n            function drawSpritesheetItemOnChar(sheetId, col, row, rect, refScale, yOffset, xOffset) {\n                const img = assets[sheetId];\n                if (!img || img.naturalWidth === 0) return;\n                const cell = getSheetCellRect(img, col, row);\n                // Use the cell's natural dimensions scaled by refScale \u2014 no forced height override.\n                // The spritesheets are crafted so their cell sizes are already perfectly proportioned.\n                const dw = cell.sw * refScale;\n                const dh = cell.sh * refScale;\n                const yo = yOffset || 0;\n                const xo = xOffset || 0;\n                const dx = rect.x + (rect.w - dw) / 2 + xo;\n                const dy = rect.y + yo;\n                ctx.drawImage(img, cell.sx, cell.sy, cell.sw, cell.sh, dx, dy, dw, dh);\n            }\n\n            // Draw a face/head spritesheet cell using the cell's own dimensions (not full-body size).\n            // Each cell is drawn at cellW*refScale \u00d7 cellH*refScale so the spritesheet spacing is\n            // perfectly preserved. The cell is horizontally centered and placed at rect.y + yOffset.\n            function drawSpritesheetCellCentered(sheetId, col, row, rect, refScale, yOffset, xOffset) {\n                const img = assets[sheetId];\n                if (!img || img.naturalWidth === 0) return;\n                const cell = getSheetCellRect(img, col, row);\n                const dw = cell.sw * refScale;\n                const dh = cell.sh * refScale;\n                const yo = yOffset || 0;\n                const xo = xOffset || 0;\n                const dx = rect.x + (rect.w - dw) / 2 + xo;\n                const dy = rect.y + yo;\n                ctx.drawImage(img, cell.sx, cell.sy, cell.sw, cell.sh, dx, dy, dw, dh);\n            }\n\n            // Background drawing\n            function drawBackground() {\n                const bg = assets['background_henesys'];\n                if (bg && bg.naturalWidth > 0) {\n                    ctx.drawImage(bg, 0, 0, logicalW, logicalH);\n                } else {\n                    // Fallback gradient \u2014 glamorous pink\n                    const grad = ctx.createLinearGradient(0, 0, 0, logicalH);\n                    grad.addColorStop(0, '#FFB6C1');\n                    grad.addColorStop(0.4, '#FF69B4');\n                    grad.addColorStop(0.7, '#FF1493');\n                    grad.addColorStop(1, '#C71585');\n                    ctx.fillStyle = grad;\n                    ctx.fillRect(0, 0, logicalW, logicalH);\n                }\n            }\n\n            // Animation\n            let animTime = 0;\n\n            // Colorful ambient sparkle colors\n            const SPARKLE_COLORS = ['#FF69B4','#FF1493','#FFD700','#FF85C2','#FFB6C1','#E0B0FF','#FF6EC7','#FFC0CB','#FFE4E1','#FFFFFF'];\n\n            function gameLoop(timestamp) {\n                animTime = timestamp * 0.001;\n                ctx.clearRect(0, 0, logicalW, logicalH);\n\n                drawBackground();\n                updateDrawAmbientSparkles();\n\n                const charX = logicalW / 2;\n                const charY = logicalH * 0.46;\n\n                // Occasional ambient glam particles floating up from character\n                if (Math.random() < 0.012) {\n                    spawnHearts(\n                        charX + (Math.random() - 0.5) * 90,\n                        charY - 20 + (Math.random() - 0.5) * 80,\n                        1\n                    );\n                }\n                // Occasional ambient hearts from pet\n                if (equipped.pet && Math.random() < 0.005) {\n                    const pRect = getCharRect(charX, charY);\n                    spawnHearts(\n                        pRect.x + pRect.w * 0.5 + off('petXOffset') + (Math.random() - 0.5) * 40,\n                        pRect.y + pRect.h * 0.55 + off('petYOffset') + (Math.random() - 0.5) * 30,\n                        1\n                    );\n                }\n\n                drawCharacter(charX, charY);\n                updateDrawEquipParticles();\n                updateDrawHeartParticles();\n\n                requestAnimationFrame(gameLoop);\n            }\n            requestAnimationFrame(gameLoop);\n\n            // \u2500\u2500 Tap / Click \u2192 Hearts & Wiggle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            {\n                function getLogicalTapPos(clientX, clientY) {\n                    const r = canvas.getBoundingClientRect();\n                    return {\n                        x: (clientX - r.left) * (logicalW / (r.width  || logicalW)),\n                        y: (clientY - r.top)  * (logicalH / (r.height || logicalH))\n                    };\n                }\n\n                // Mouse: browser natively suppresses click after a drag\n                canvas.addEventListener('click', (e) => {\n                    const p = getLogicalTapPos(e.clientX, e.clientY);\n                    handleTap(p.x, p.y);\n                });\n\n                // Touch: suppress if finger moved significantly (drag/scroll)\n                let _tx = 0, _ty = 0, _tMoved = false;\n                canvas.addEventListener('touchstart', (e) => {\n                    _tx = e.touches[0].clientX;\n                    _ty = e.touches[0].clientY;\n                    _tMoved = false;\n                }, { passive: true });\n                canvas.addEventListener('touchmove', (e) => {\n                    if (Math.abs(e.touches[0].clientX - _tx) > 9 ||\n                        Math.abs(e.touches[0].clientY - _ty) > 9) _tMoved = true;\n                }, { passive: true });\n                canvas.addEventListener('touchend', (e) => {\n                    if (_tMoved) return;\n                    const t = e.changedTouches[0];\n                    const p = getLogicalTapPos(t.clientX, t.clientY);\n                    handleTap(p.x, p.y);\n                }, { passive: true });\n            }\n\n            // \u2500\u2500 Gallery UI (play mode only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            if (mode !== 'edit') {\n                // Gallery button group container\n                const galleryBtnGroup = document.createElement('div');\n                galleryBtnGroup.className = 'gallery-btn-group';\n                document.querySelector('.ui-overlay').appendChild(galleryBtnGroup);\n\n                // \u2500\u2500 Star Counter Indicator \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                const starIndicator = document.createElement('div');\n                starIndicator.style.cssText = [\n                    'position:absolute;top:16px;right:16px',\n                    'background:linear-gradient(135deg,#4a0040,#7a0060)',\n                    'border-radius:20px;padding:8px 14px',\n                    'display:flex;align-items:center;gap:6px',\n                    'box-shadow:0 4px 16px rgba(255,105,180,0.3),0 0 0 2px rgba(255,215,0,0.4)',\n                    \"font-family:'Fredoka',sans-serif;font-size:18px;font-weight:700;color:white\",\n                    'z-index:50;pointer-events:none;'\n                ].join(';');\n                starIndicator.innerHTML = '<span style=\"font-size:20px;filter:drop-shadow(0 0 6px #FFD700)\">\u2b50</span><span class=\"star-count\" style=\"color:#FFD700;text-shadow:0 0 8px rgba(255,215,0,0.6);display:inline-block\">0</span>';\n                starIndicatorEl = starIndicator;\n                document.querySelector('.ui-overlay').appendChild(starIndicator);\n                updateStarIndicator();\n\n                // Random Look button\n                const randomLookBtn = document.createElement('button');\n                randomLookBtn.className = 'gallery-btn';\n                randomLookBtn.style.background = 'linear-gradient(135deg, #9B59B6, #E91E8A)';\n                randomLookBtn.style.color = 'white';\n                randomLookBtn.innerHTML = '<span class=\"btn-icon\">\ud83c\udfb2</span><span class=\"btn-label\">Random</span>';\n                galleryBtnGroup.appendChild(randomLookBtn);\n\n                randomLookBtn.addEventListener('click', () => {\n                    categories.forEach(cat => {\n                        const items = (config.categories[cat.id] || []).filter(item => !isItemDisabled(cat.id, item.id));\n                        if (items.length === 0) { equipped[cat.id] = null; return; }\n                        // 20% chance to leave slot empty\n                        if (Math.random() < 0.2) { equipped[cat.id] = null; return; }\n                        equipped[cat.id] = items[Math.floor(Math.random() * items.length)].id;\n                    });\n                    // Resolve dress vs top/bottom conflict\n                    if (equipped.dress) { equipped.top = null; equipped.bottom = null; }\n                    renderEquippedBar();\n                    renderItemGrid();\n                    saveEquipped();\n                    spawnEquipParticles(logicalW / 2, logicalH * 0.38);\n                    charWiggle = { startTime: animTime, duration: 0.6 };\n                });\n\n                // Save Look button\n                const saveLookBtn = document.createElement('button');\n                saveLookBtn.className = 'gallery-btn save-look';\n                saveLookBtn.style.background = 'linear-gradient(135deg, #FF1493, #FF69B4)';\n                saveLookBtn.style.color = 'white';\n                saveLookBtn.innerHTML = '<span class=\"btn-icon\">\ud83d\udcf8</span><span class=\"btn-label\">Save Look</span>';\n                galleryBtnGroup.appendChild(saveLookBtn);\n\n                saveLookBtn.addEventListener('click', () => {\n                    if (gallery.length >= 20) {\n                        saveLookBtn.innerHTML = '<span class=\"btn-icon\">\ud83d\udeab</span><span class=\"btn-label\">Gallery Full!</span>';\n                        saveLookBtn.style.background = '#E74C3C';\n                        saveLookBtn.style.color = 'white';\n                        setTimeout(() => {\n                            saveLookBtn.innerHTML = '<span class=\"btn-icon\">\ud83d\udcf8</span><span class=\"btn-label\">Save Look</span>';\n                            saveLookBtn.style.background = 'linear-gradient(135deg, #FF1493, #FF69B4)';\n                            saveLookBtn.style.color = 'white';\n                        }, 1500);\n                        return;\n                    }\n\n                    const name = `Look #${gallery.length + 1}`;\n                    const newLook = {\n                        id: Date.now().toString(),\n                        name,\n                        equipped: { ...equipped },\n                        savedAt: new Date().toISOString()\n                    };\n                    gallery.push(newLook);\n                    saveEquipped();\n\n                    saveLookBtn.innerHTML = '<span class=\"btn-icon\">\u2705</span><span class=\"btn-label\">Saved!</span>';\n                    saveLookBtn.style.background = '#27AE60';\n                    saveLookBtn.style.color = 'white';\n                    setTimeout(() => {\n                        saveLookBtn.innerHTML = '<span class=\"btn-icon\">\ud83d\udcf8</span><span class=\"btn-label\">Save Look</span>';\n                        saveLookBtn.style.background = 'linear-gradient(135deg, #FF1493, #FF69B4)';\n                        saveLookBtn.style.color = 'white';\n                    }, 1500);\n                });\n\n                // Gallery button\n                const galleryBtn = document.createElement('button');\n                galleryBtn.className = 'gallery-btn open-gallery';\n                galleryBtn.innerHTML = '<span class=\"btn-icon\">\ud83d\uddbc</span><span class=\"btn-label\">Gallery</span>';\n                galleryBtnGroup.appendChild(galleryBtn);\n\n                let galleryOverlayEl = null;\n\n                galleryBtn.addEventListener('click', () => {\n                    if (galleryOverlayEl) {\n                        galleryOverlayEl.remove();\n                        galleryOverlayEl = null;\n                        galleryBtn.style.background = '';\n                        galleryBtn.style.color = '';\n                    } else {\n                        openGalleryModal();\n                        galleryBtn.style.background = 'var(--primary)';\n                        galleryBtn.style.color = 'white';\n                    }\n                });\n\n                // Gallery modal functions\n                function openGalleryModal() {\n                    if (galleryOverlayEl) { galleryOverlayEl.remove(); galleryOverlayEl = null; }\n                    const overlay = document.createElement('div');\n                    galleryOverlayEl = overlay;\n                    overlay.className = 'gallery-overlay';\n\n                    const panel = document.createElement('div');\n                    panel.className = 'gallery-panel';\n\n                    const header = document.createElement('div');\n                    header.className = 'gallery-header';\n\n                    const title = document.createElement('div');\n                    title.className = 'gallery-title';\n                    title.textContent = '\ud83d\uddbc My Gallery';\n\n                    const closeBtn = document.createElement('button');\n                    closeBtn.className = 'gallery-close';\n                    closeBtn.textContent = '\u2715';\n                    closeBtn.addEventListener('click', () => {\n                        overlay.remove();\n                        galleryOverlayEl = null;\n                        galleryBtn.style.background = '';\n                        galleryBtn.style.color = '';\n                    });\n\n                    header.appendChild(title);\n                    header.appendChild(closeBtn);\n                    panel.appendChild(header);\n\n                    if (gallery.length === 0) {\n                        const empty = document.createElement('div');\n                        empty.className = 'gallery-empty';\n                        empty.textContent = 'No saved looks yet! Hit \ud83d\udcf8 Save Look to save your first outfit.';\n                        panel.appendChild(empty);\n                    } else {\n                        gallery.forEach(look => {\n                            const entry = createGalleryEntry(look, overlay);\n                            panel.appendChild(entry);\n                        });\n                    }\n\n                    overlay.appendChild(panel);\n                    gameWorld.appendChild(overlay);\n\n                    // Close on overlay click\n                    overlay.addEventListener('click', (e) => {\n                        if (e.target === overlay) {\n                            overlay.remove();\n                            galleryOverlayEl = null;\n                            galleryBtn.style.background = '';\n                            galleryBtn.style.color = '';\n                        }\n                    });\n                }\n\n                function createGalleryEntry(look, overlay) {\n                    const entry = document.createElement('div');\n                    entry.className = 'gallery-entry';\n\n                    // Preview\n                    const preview = document.createElement('div');\n                    preview.className = 'gallery-preview';\n                    \n                    const previewCanvas = createGalleryPreview(look.equipped);\n                    if (previewCanvas) {\n                        preview.appendChild(previewCanvas);\n                    } else {\n                        preview.style.fontSize = '32px';\n                        preview.textContent = '\ud83d\ude0a';\n                    }\n\n                    // Info\n                    const info = document.createElement('div');\n                    info.className = 'gallery-info';\n\n                    const name = document.createElement('div');\n                    name.className = 'gallery-name';\n                    name.textContent = look.name;\n\n                    const date = document.createElement('div');\n                    date.className = 'gallery-date';\n                    date.textContent = formatDate(look.savedAt);\n\n                    info.appendChild(name);\n                    info.appendChild(date);\n\n                    // Actions\n                    const actions = document.createElement('div');\n                    actions.className = 'gallery-actions';\n\n                    const wearBtn = document.createElement('button');\n                    wearBtn.className = 'gallery-action-btn wear';\n                    wearBtn.textContent = '\ud83d\udc57 Wear';\n                    wearBtn.addEventListener('click', () => {\n                        equipped = { ...look.equipped };\n                        renderEquippedBar();\n                        saveEquipped();\n                        overlay.remove();\n                        galleryOverlayEl = null;\n                        galleryBtn.style.background = '';\n                        galleryBtn.style.color = '';\n                    });\n\n                    const deleteBtn = document.createElement('button');\n                    deleteBtn.className = 'gallery-action-btn delete';\n                    deleteBtn.textContent = '\ud83d\uddd1';\n                    deleteBtn.addEventListener('click', () => {\n                        gallery = gallery.filter(l => l.id !== look.id);\n                        saveGallery();\n                        // Re-render gallery\n                        overlay.remove();\n                        galleryOverlayEl = null;\n                        openGalleryModal();\n                        galleryBtn.style.background = 'var(--primary)';\n                        galleryBtn.style.color = 'white';\n                    });\n\n                    actions.appendChild(wearBtn);\n                    actions.appendChild(deleteBtn);\n\n                    entry.appendChild(preview);\n                    entry.appendChild(info);\n                    entry.appendChild(actions);\n\n                    return entry;\n                }\n\n                function createGalleryPreview(equippedItems) {\n                    const faceId = equippedItems.face;\n                    if (!faceId) return null;\n\n                    const faceItem = findItem('face', faceId);\n                    if (!faceItem || !faceItem.spritesheet) return null;\n\n                    const img = assets[faceItem.spritesheet];\n                    if (!img || img.naturalWidth === 0) return null;\n\n                    const dpr = window.devicePixelRatio || 1;\n                    const cvs = document.createElement('canvas');\n                    cvs.width = 64 * dpr;\n                    cvs.height = 64 * dpr;\n                    const c = cvs.getContext('2d');\n                    c.scale(dpr, dpr);\n\n                    const cell = getSheetCellRect(img, faceItem.col, faceItem.row);\n                    const scale = Math.min(64 / cell.sw, 64 / cell.sh);\n                    const dw = cell.sw * scale;\n                    const dh = cell.sh * scale;\n                    c.drawImage(img, cell.sx, cell.sy, cell.sw, cell.sh, (64 - dw) / 2, (64 - dh) / 2, dw, dh);\n\n                    // Convert to static image and release GPU backing store\n                    return canvasToImg(cvs, 'width:64px;height:64px;');\n                }\n\n                function formatDate(isoString) {\n                    const date = new Date(isoString);\n                    const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n                    return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;\n                }\n            }\n\n            // \u2500\u2500 Challenge Feature \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            if (mode !== 'edit') {\n                let currentChallenge = null;\n                let currentChallengeMaxStars = 0; // best stars earned on the current challenge\n                let challengePopupEl = null;\n\n                // Add challenge button to gallery button group\n                const galleryBtnGroup2 = document.querySelector('.gallery-btn-group');\n                const challengeBtn = document.createElement('button');\n                challengeBtn.className = 'gallery-btn challenge-pulse';\n                challengeBtn.style.background = 'linear-gradient(135deg, #FFD700, #FF69B4)';\n                challengeBtn.style.color = '#4A0E3C';\n                challengeBtn.innerHTML = '<span class=\"btn-icon\">\ud83c\udfaf</span><span class=\"btn-label\">Challenge</span>';\n                galleryBtnGroup2.appendChild(challengeBtn);\n                challengeBtn.addEventListener('click', () => {\n                    if (challengePopupEl) {\n                        challengePopupEl.remove();\n                        challengePopupEl = null;\n                        challengeBtn.style.background = 'linear-gradient(135deg, #FFD700, #FF69B4)';\n                        challengeBtn.style.color = '#4A0E3C';\n                        challengeBtn.classList.add('challenge-pulse');\n                    } else {\n                        openChallengePopup();\n                        challengeBtn.style.background = 'linear-gradient(135deg, #E91E8A, #C71585)';\n                        challengeBtn.style.color = '#4A0E3C';\n                        challengeBtn.classList.remove('challenge-pulse');\n                    }\n                });\n\n                function generateChallenge() {\n                    const ch = { face: null, top: null, bottom: null, dress: null, weapon: null, cape: null, pet: null };\n                    // Randomly pick face, weapon, cape, pet (only from enabled items)\n                    ['face', 'weapon', 'cape', 'pet'].forEach(cat => {\n                        const items = (config.categories[cat] || []).filter(item => !isItemDisabled(cat, item.id));\n                        if (items.length) ch[cat] = items[Math.floor(Math.random() * items.length)].id;\n                    });\n                    // Body: either a dress OR top + bottom\n                    const useDress = Math.random() > 0.5;\n                    if (useDress) {\n                        const dresses = (config.categories['dress'] || []).filter(item => !isItemDisabled('dress', item.id));\n                        if (dresses.length) ch['dress'] = dresses[Math.floor(Math.random() * dresses.length)].id;\n                    } else {\n                        ['top', 'bottom'].forEach(cat => {\n                            const items = (config.categories[cat] || []).filter(item => !isItemDisabled(cat, item.id));\n                            if (items.length) ch[cat] = items[Math.floor(Math.random() * items.length)].id;\n                        });\n                    }\n                    return ch;\n                }\n\n                // Renders a full character preview into a new canvas using the challenge equipped state\n                function makeCharacterCanvas(eq, w, h) {\n                    const dpr = window.devicePixelRatio || 1;\n                    const cvs = document.createElement('canvas');\n                    cvs.width = w * dpr; cvs.height = h * dpr;\n                    cvs.style.width = w + 'px'; cvs.style.height = h + 'px';\n                    const c = cvs.getContext('2d');\n                    c.scale(dpr, dpr);\n                    c.imageSmoothingEnabled = true;\n\n                    const charH = h * 0.38;\n                    const charW = charH * 0.5;\n                    const rect = { x: (w - charW) / 2, y: (h - charH) / 2, w: charW, h: charH };\n                    const refImg = assets['character_full_body'];\n                    const refScale = (refImg && refImg.naturalHeight > 0) ? charH / refImg.naturalHeight : charH / 600;\n\n                    // Compute the main canvas character height so we can scale offsets proportionally\n                    const mainCharScale = (config.characterScale !== undefined ? config.characterScale : 1.0);\n                    const mainCharH = Math.min(logicalH * 0.62, 600) * mainCharScale;\n                    // offsetScale converts main-canvas pixel offsets \u2192 preview pixel offsets\n                    const offsetScale = charH / mainCharH;\n                    function so(key) { return off(key) * offsetScale; }\n\n                    function cellRect(img, col, row) {\n                        const x0 = Math.round((col * img.naturalWidth) / 5);\n                        const x1 = Math.round(((col + 1) * img.naturalWidth) / 5);\n                        const y0 = Math.round((row * img.naturalHeight) / 5);\n                        const y1 = Math.round(((row + 1) * img.naturalHeight) / 5);\n                        return { sx: x0, sy: y0, sw: x1 - x0, sh: y1 - y0 };\n                    }\n\n                    function drawCell(sheetId, col, row, xo, yo) {\n                        const img = assets[sheetId];\n                        if (!img || img.naturalWidth === 0) return;\n                        const cell = cellRect(img, col, row);\n                        const dw = cell.sw * refScale, dh = cell.sh * refScale;\n                        const dx = rect.x + (rect.w - dw) / 2 + (xo || 0);\n                        const dy = rect.y + (yo || 0);\n                        c.drawImage(img, cell.sx, cell.sy, cell.sw, cell.sh, dx, dy, dw, dh);\n                    }\n\n                    function drawImgRef(id, xo, yo) {\n                        const img = assets[id];\n                        if (!img || img.naturalWidth === 0) return;\n                        const dw = img.naturalWidth * refScale, dh = img.naturalHeight * refScale;\n                        c.drawImage(img, rect.x + (rect.w - dw) / 2 + (xo || 0), rect.y + (yo || 0), dw, dh);\n                    }\n\n                    const hasDress = !!(eq.dress && findItem('dress', eq.dress));\n                    const hasTop   = !!(eq.top    && findItem('top',    eq.top));\n                    const hasBottom= !!(eq.bottom  && findItem('bottom', eq.bottom));\n\n                    // Cape (behind)\n                    if (eq.cape) { const it = findItem('cape', eq.cape); if (it && it.spritesheet) drawCell(it.spritesheet, it.col, it.row, so('capeXOffset'), so('capeYOffset')); }\n\n                    // Face/Head\n                    if (eq.face) {\n                        const it = findItem('face', eq.face);\n                        if (it && it.spritesheet) {\n                            const ry = it.row === 1 ? so('headRow1YOffset') : it.row === 2 ? so('headRow2YOffset') : it.row === 3 ? so('headRow3YOffset') : it.row === 4 ? so('headRow4YOffset') : 0;\n                            drawCell(it.spritesheet, it.col, it.row, so('headXOffset'), so('headYOffset') + ry);\n                        }\n                    } else { drawImgRef('character_head', so('headXOffset'), so('headYOffset')); }\n\n                    // Body\n                    if (!hasDress) {\n                        if (hasTop || hasBottom) {\n                            if (!hasBottom) drawImgRef('character_lower_body', so('lowerBodyXOffset'), so('lowerBodyYOffset'));\n                            if (hasBottom)  { const it = findItem('bottom', eq.bottom); if (it && it.spritesheet) drawCell(it.spritesheet, it.col, it.row, 0, so('lowerBodyYOffset') + so('bottomYOffset')); }\n                            if (!hasTop)    drawImgRef('character_upper_body', so('upperBodyXOffset'), so('upperBodyYOffset'));\n                            if (hasTop)     { const it = findItem('top', eq.top);    if (it && it.spritesheet) drawCell(it.spritesheet, it.col, it.row, 0, so('upperBodyYOffset') + so('topYOffset')); }\n                        } else {\n                            drawImgRef('character_full_body', so('fullBodyXOffset'), so('fullBodyYOffset'));\n                        }\n                    } else {\n                        const it = findItem('dress', eq.dress);\n                        if (it && it.spritesheet) drawCell(it.spritesheet, it.col, it.row, 0, so('fullBodyYOffset') + so('dressYOffset'));\n                    }\n\n                    // Weapon\n                    if (eq.weapon) {\n                        const it = findItem('weapon', eq.weapon);\n                        if (it && it.spritesheet) {\n                            const img = assets[it.spritesheet];\n                            if (img && img.naturalWidth > 0) {\n                                const cell = cellRect(img, it.col, it.row);\n                                const wScale = Number(config.weaponScale || 0.55);\n                                const wh = charH * wScale, ww = (cell.sw / cell.sh) * wh;\n                                c.drawImage(img, cell.sx, cell.sy, cell.sw, cell.sh,\n                                    rect.x + rect.w * 0.52 + so('weaponXOffset'),\n                                    rect.y + charH * 0.28 + so('weaponYOffset'), ww, wh);\n                            }\n                        }\n                    }\n\n                    // Pet\n                    if (eq.pet) {\n                        const it = findItem('pet', eq.pet);\n                        if (it && it.spritesheet) {\n                            const img = assets[it.spritesheet];\n                            if (img && img.naturalWidth > 0) {\n                                const cell = cellRect(img, it.col, it.row);\n                                const pScale = Number(config.petScale || 0.35);\n                                const ph = charH * pScale, pw = (cell.sw / cell.sh) * ph;\n                                c.drawImage(img, cell.sx, cell.sy, cell.sw, cell.sh,\n                                    rect.x + rect.w * 0.5 + so('petXOffset'),\n                                    rect.y + charH * 0.6 + so('petYOffset'), pw, ph);\n                            }\n                        }\n                    }\n\n                    return cvs;\n                }\n\n                function makeChallengeThumb(category, itemId, size) {\n                    if (!itemId) return null;\n                    const item = findItem(category, itemId);\n                    if (!item || !item.spritesheet) return null;\n                    const img = assets[item.spritesheet];\n                    if (!img || img.naturalWidth === 0) return null;\n                    const dpr = window.devicePixelRatio || 1;\n                    const cvs = document.createElement('canvas');\n                    cvs.width = size * dpr; cvs.height = size * dpr;\n                    const c = cvs.getContext('2d');\n                    c.scale(dpr, dpr);\n                    const cell = getSheetCellRect(img, item.col, item.row);\n                    const scale = Math.min(size / cell.sw, size / cell.sh);\n                    const dw = cell.sw * scale, dh = cell.sh * scale;\n                    c.drawImage(img, cell.sx, cell.sy, cell.sw, cell.sh, (size - dw) / 2, (size - dh) / 2, dw, dh);\n                    // Convert to static image and release GPU backing store\n                    return canvasToImg(cvs, 'width:' + size + 'px;height:' + size + 'px;');\n                }\n\n                function scoreChallenge(ch) {\n                    let correct = 0, total = 0;\n                    ['face', 'top', 'bottom', 'dress', 'weapon', 'cape', 'pet'].forEach(cat => {\n                        if (ch[cat]) { total++; if (equipped[cat] === ch[cat]) correct++; }\n                    });\n                    return { correct, total };\n                }\n\n                function openChallengePopup(forceNew) {\n                    if (challengePopupEl) { challengePopupEl.remove(); challengePopupEl = null; }\n                    if (!currentChallenge || forceNew) {\n                        currentChallenge = generateChallenge();\n                        currentChallengeMaxStars = 0; // reset best score for new challenge\n                    }\n                    renderChallengePopup();\n                }\n\n                function renderChallengePopup() {\n                    if (challengePopupEl) { challengePopupEl.remove(); challengePopupEl = null; }\n\n                    const popup = document.createElement('div');\n                    popup.className = 'challenge-popup';\n                    challengePopupEl = popup;\n\n                    // Close button\n                    const closeBtn = document.createElement('button');\n                    closeBtn.className = 'challenge-close-btn';\n                    closeBtn.textContent = '\u2715';\n                    closeBtn.addEventListener('click', () => { popup.remove(); challengePopupEl = null; challengeBtn.style.background = ''; challengeBtn.style.color = ''; });\n                    popup.appendChild(closeBtn);\n\n                    // Title\n                    const title = document.createElement('div');\n                    title.className = 'challenge-popup-title';\n                    title.textContent = '\ud83c\udfaf Copy This Look!';\n                    popup.appendChild(title);\n\n                    // Required items grid \u2014 shows each target slot with live green highlight when matched\n                    const reqGrid = document.createElement('div');\n                    reqGrid.className = 'challenge-items-grid';\n                    const catOrder = ['face', 'dress', 'top', 'bottom', 'weapon', 'cape', 'pet'];\n                    const catEmojis = { face:'\ud83d\ude0a', top:'\ud83d\udc55', bottom:'\ud83d\udc56', dress:'\ud83d\udc57', weapon:'\u2694\ufe0f', cape:'\ud83e\udde3', pet:'\ud83d\udc3e' };\n                    const catLabelsShort = { face:'Face', top:'Top', bottom:'Bottom', dress:'Dress', weapon:'Handbag', cape:'Aura', pet:'Pet' };\n                    const reqSlotEls = {};\n                    catOrder.forEach(cat => {\n                        if (!currentChallenge[cat]) return;\n                        const slot = document.createElement('div');\n                        slot.className = 'challenge-item-slot';\n                        // Thumbnail\n                        const thumb = makeChallengeThumb(cat, currentChallenge[cat], 72);\n                        if (thumb) {\n                            thumb.style.cssText = 'width:72px;height:72px;display:block;';\n                            slot.appendChild(thumb);\n                        } else {\n                            const em = document.createElement('div');\n                            em.style.cssText = 'font-size:20px;line-height:1;';\n                            em.textContent = catEmojis[cat];\n                            slot.appendChild(em);\n                        }\n                        // Category label\n                        const lbl = document.createElement('div');\n                        lbl.className = 'challenge-item-label';\n                        lbl.textContent = catLabelsShort[cat];\n                        slot.appendChild(lbl);\n                        reqSlotEls[cat] = slot;\n                        reqGrid.appendChild(slot);\n                    });\n                    popup.appendChild(reqGrid);\n\n                    // Live-update slot borders: green = matched, default = not yet\n                    // Deferred one frame so popup.isConnected is true when the loop starts\n                    requestAnimationFrame(function pollSlots() {\n                        if (!popup.isConnected) return;\n                        Object.keys(reqSlotEls).forEach(cat => {\n                            const matched = equipped[cat] === currentChallenge[cat];\n                            reqSlotEls[cat].classList.toggle('matched', matched);\n                        });\n                        requestAnimationFrame(pollSlots);\n                    });\n\n                    // Mini character preview\n                    const previewWrap = document.createElement('div');\n                    previewWrap.style.cssText = 'display:flex;justify-content:center;align-items:center;background:linear-gradient(135deg,#e8f4fd,#f0e8fd);border-radius:16px;padding:0;position:relative;overflow:hidden;width:100%;aspect-ratio:1/0.85;margin:0 auto;';\n                    const charCvsRaw = makeCharacterCanvas(currentChallenge, 360, 420);\n                    const charCvs = canvasToImg(charCvsRaw, 'width:100%;height:100%;object-fit:contain;border-radius:16px;');\n                    previewWrap.appendChild(charCvs);\n                    popup.appendChild(previewWrap);\n\n                    const submitBtn = document.createElement('button');\n                    submitBtn.className = 'challenge-submit-btn';\n                    submitBtn.textContent = '\u2705 Submit Look';\n                    submitBtn.addEventListener('click', () => {\n                        popup.remove();\n                        challengePopupEl = null;\n                        showChallengeResult();\n                    });\n                    popup.appendChild(submitBtn);\n\n                    gameWorld.appendChild(popup);\n                }\n\n                // \u2500\u2500 Full-Screen Challenge Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                function showChallengeResult() {\n                    const { correct, total } = scoreChallenge(currentChallenge);\n                    const stars = Math.round((correct / total) * 5);\n\n                    // Add only the improvement over the best score for this challenge\n                    if (stars > 0) {\n                        const improvement = Math.max(0, stars - currentChallengeMaxStars);\n                        currentChallengeMaxStars = Math.max(currentChallengeMaxStars, stars);\n                        if (improvement > 0) {\n                            totalStars += improvement;\n                            updateStarIndicator(true);\n                            lib.saveUserGameState({ equipped, gallery, totalStars }).catch(err => lib.log(`Save err: ${err.message}`));\n                            lib.addPlayerScoreToLeaderboard(totalStars).catch(err => lib.log(`Leaderboard err: ${err.message}`));\n                        }\n                    }\n\n                    // Overlay backdrop\n                    const overlay = document.createElement('div');\n                    overlay.style.cssText = [\n                        'position:fixed;top:0;left:0;width:100%;height:100%;z-index:999',\n                        'display:flex;flex-direction:column;align-items:center;justify-content:center',\n                        'background:rgba(0,0,0,0);transition:background 0.45s ease',\n                        \"font-family:'Fredoka',sans-serif\"\n                    ].join(';');\n\n                    // Confetti canvas (behind card)\n                    const confettiCvs = document.createElement('canvas');\n                    confettiCvs.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;';\n                    confettiCvs.width  = logicalW;\n                    confettiCvs.height = logicalH;\n                    overlay.appendChild(confettiCvs);\n\n                    // Card\n                    const card = document.createElement('div');\n                    card.style.cssText = [\n                        'position:relative;z-index:1',\n                        'background:linear-gradient(160deg,#2d0028 0%,#5a0048 60%,#7a0060 100%)',\n                        'border-radius:36px;padding:28px 24px 24px',\n                        'width:92%;max-width:480px',\n                        'display:flex;flex-direction:column;align-items:center;gap:14px',\n                        'box-shadow:0 24px 64px rgba(0,0,0,0.6),0 0 0 1px rgba(255,255,255,0.08)',\n                        'transform:scale(0.4) translateY(80px);opacity:0',\n                        'transition:transform 0.55s cubic-bezier(0.34,1.56,0.64,1),opacity 0.4s ease'\n                    ].join(';');\n                    overlay.appendChild(card);\n\n                    // Title banner\n                    const bannerTexts = ['\ud83d\ude14 Keep Practicing!','\u26a1 Getting There!','\u2728 Nice Try!','\ud83d\udd25 So Close!','\ud83d\udcab Almost Perfect!','\ud83c\udfc6 Perfect Match!'];\n                    const bannerColors = ['#E74C3C','#E67E22','#E67E22','#F39C12','#27AE60','#27AE60'];\n                    const banner = document.createElement('div');\n                    banner.style.cssText = [\n                        `background:${bannerColors[stars]};border-radius:20px;padding:12px 28px`,\n                        'font-size:26px;font-weight:700;color:white;text-align:center',\n                        'box-shadow:0 4px 16px rgba(0,0,0,0.35)',\n                        'transform:scale(0);transition:transform 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.35s'\n                    ].join(';');\n                    banner.textContent = bannerTexts[stars];\n                    card.appendChild(banner);\n\n                    // Side-by-side previews: target vs player\n                    const previewsRow = document.createElement('div');\n                    previewsRow.style.cssText = 'display:flex;gap:8px;align-items:flex-end;justify-content:center;width:100%;overflow:hidden;';\n\n                    function makePreviewBlock(label, eq, isTarget) {\n                        const block = document.createElement('div');\n                        block.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:6px;flex:1;min-width:0;';\n                        const lbl = document.createElement('div');\n                        lbl.style.cssText = 'font-size:16px;font-weight:700;color:rgba(255,255,255,' + (isTarget ? '0.7' : '0.9') + ');letter-spacing:0.5px;';\n                        lbl.textContent = label;\n                        const rawCvs = makeCharacterCanvas(eq, 170, 210);\n                        // Convert to static image and release GPU backing store\n                        const previewImg = canvasToImg(rawCvs, 'width:100%;height:auto;aspect-ratio:170/210;border-radius:16px;background:#ffffff;border:none;display:block;');\n                        block.appendChild(lbl);\n                        block.appendChild(previewImg);\n                        return block;\n                    }\n\n                    previewsRow.appendChild(makePreviewBlock('TARGET', currentChallenge, true));\n\n                    // VS badge\n                    const vsBadge = document.createElement('div');\n                    vsBadge.style.cssText = 'font-size:18px;font-weight:700;color:rgba(255,255,255,0.5);padding-bottom:60px;';\n                    vsBadge.textContent = 'VS';\n                    previewsRow.appendChild(vsBadge);\n\n                    previewsRow.appendChild(makePreviewBlock('YOU', equipped, false));\n                    card.appendChild(previewsRow);\n\n                    // Per-slot match pips\n                    const pipsRow = document.createElement('div');\n                    pipsRow.style.cssText = 'display:flex;gap:6px;justify-content:center;flex-wrap:wrap;';\n                    const catLabels = { face:'Face', top:'Top', bottom:'Bottom', dress:'Dress', weapon:'Handbag', cape:'Aura', pet:'Pet' };\n                    ['face','dress','top','bottom','weapon','cape','pet'].forEach((cat, ci) => {\n                        if (!currentChallenge[cat]) return;\n                        const matched = equipped[cat] === currentChallenge[cat];\n                        const pip = document.createElement('div');\n                        pip.style.cssText = [\n                            `background:${matched ? '#27AE60' : '#E74C3C'}`,\n                            'border-radius:10px;padding:6px 14px',\n                            'display:flex;align-items:center;gap:4px',\n                            'font-size:15px;font-weight:700;color:white',\n                            'transform:scale(0)',\n                            `transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1) ${0.7 + ci*0.07}s`\n                        ].join(';');\n                        pip.textContent = `${matched ? '\u2713' : '\u2717'} ${catLabels[cat]}`;\n                        pipsRow.appendChild(pip);\n                        setTimeout(() => { pip.style.transform = 'scale(1)'; }, 50);\n                    });\n                    card.appendChild(pipsRow);\n\n                    // Stars row\n                    const starsRow = document.createElement('div');\n                    starsRow.style.cssText = 'display:flex;gap:6px;align-items:center;justify-content:center;';\n                    for (let i = 0; i < 5; i++) {\n                        const s = document.createElement('div');\n                        const filled = i < stars;\n                        s.style.cssText = [\n                            'font-size:52px;line-height:1',\n                            'transform:scale(0) rotate(-30deg)',\n                            `transition:transform 0.4s cubic-bezier(0.34,1.56,0.64,1) ${0.6 + i * 0.13}s`,\n                            `filter:${filled ? 'drop-shadow(0 0 10px #FFD700) drop-shadow(0 0 4px #FF8C00)' : 'grayscale(1) opacity(0.3)'}`\n                        ].join(';');\n                        s.textContent = filled ? '\u2b50' : '\u2606';\n                        starsRow.appendChild(s);\n                        setTimeout(() => { s.style.transform = 'scale(1) rotate(0deg)'; }, 50);\n                    }\n                    card.appendChild(starsRow);\n\n                    // Score text\n                    const scoreEl = document.createElement('div');\n                    scoreEl.style.cssText = 'color:rgba(255,255,255,0.8);font-size:20px;font-weight:600;';\n                    scoreEl.textContent = `${correct} out of ${total} items matched!`;\n                    card.appendChild(scoreEl);\n\n                    // Buttons\n                    const btnRow = document.createElement('div');\n                    btnRow.style.cssText = 'display:flex;gap:10px;width:100%;margin-top:4px;';\n\n                    const retryBtn = document.createElement('button');\n                    retryBtn.style.cssText = [\n                        'flex:1;padding:18px 0;border:2px solid rgba(255,255,255,0.25);border-radius:18px',\n                        'background:rgba(255,255,255,0.1);color:white',\n                        \"font-family:'Fredoka',sans-serif;font-size:20px;font-weight:700;cursor:pointer\",\n                        'transition:transform 0.15s,background 0.15s'\n                    ].join(';');\n                    retryBtn.textContent = '\ud83d\udd01 Retry';\n                    retryBtn.addEventListener('mouseenter', () => { retryBtn.style.background = 'rgba(255,255,255,0.2)'; });\n                    retryBtn.addEventListener('mouseleave', () => { retryBtn.style.background = 'rgba(255,255,255,0.1)'; });\n                    retryBtn.addEventListener('click', () => {\n                        overlay.style.opacity = '0';\n                        setTimeout(() => { overlay.remove(); openChallengePopup(false); }, 300);\n                    });\n\n                    const newChalBtn = document.createElement('button');\n                    const btnBg = stars === 5 ? 'linear-gradient(135deg,#27AE60,#2ECC71)' : stars >= 3 ? 'linear-gradient(135deg,#E67E22,#F39C12)' : 'linear-gradient(135deg,#8E44AD,#9B59B6)';\n                    newChalBtn.style.cssText = [\n                        `flex:1.4;padding:18px 0;border:none;border-radius:18px;background:${btnBg};color:white`,\n                        \"font-family:'Fredoka',sans-serif;font-size:20px;font-weight:700;cursor:pointer\",\n                        'box-shadow:0 4px 16px rgba(0,0,0,0.3);transition:transform 0.15s'\n                    ].join(';');\n                    newChalBtn.textContent = '\ud83d\udd04 New Challenge';\n                    newChalBtn.addEventListener('click', () => {\n                        overlay.style.opacity = '0';\n                        setTimeout(() => { overlay.remove(); openChallengePopup(true); }, 300);\n                    });\n\n                    [retryBtn, newChalBtn].forEach(b => {\n                        b.addEventListener('mousedown', () => { b.style.transform = 'scale(0.96)'; });\n                        b.addEventListener('mouseup',   () => { b.style.transform = 'scale(1)'; });\n                        b.addEventListener('touchstart',() => { b.style.transform = 'scale(0.96)'; }, {passive:true});\n                        b.addEventListener('touchend',  () => { b.style.transform = 'scale(1)'; }, {passive:true});\n                    });\n\n                    btnRow.appendChild(retryBtn);\n                    btnRow.appendChild(newChalBtn);\n                    card.appendChild(btnRow);\n\n                    // Hide retry on perfect match\n                    if (stars === 5) {\n                        retryBtn.style.display = 'none';\n                        newChalBtn.style.flex = '1';\n                    }\n\n                    gameWorld.appendChild(overlay);\n\n                    // Animate in\n                    requestAnimationFrame(() => {\n                        overlay.style.background = 'rgba(0,0,0,0.82)';\n                        setTimeout(() => {\n                            card.style.transform = 'scale(1) translateY(0)';\n                            card.style.opacity = '1';\n                            banner.style.transform = 'scale(1)';\n                        }, 80);\n                    });\n\n                    // Confetti for decent scores\n                    if (stars >= 3) runConfetti(confettiCvs, stars);\n                }\n\n                function runConfetti(canvas, stars) {\n                    const c2 = canvas.getContext('2d');\n                    const colors = ['#FF69B4','#FF1493','#FFD700','#FF85C2','#FFB6C1','#E0B0FF','#FF6EC7','#FFC0CB','#FFFFFF'];\n                    const count  = stars === 5 ? 150 : 80;\n                    const W = canvas.width, H = canvas.height;\n                    const particles = Array.from({length: count}, () => ({\n                        x: Math.random() * W,\n                        y: -20 - Math.random() * H * 0.5,\n                        vx: (Math.random() - 0.5) * 4,\n                        vy: Math.random() * 4 + 1.5,\n                        color: colors[Math.floor(Math.random() * colors.length)],\n                        sz: Math.random() * 9 + 4,\n                        rot: Math.random() * Math.PI * 2,\n                        rotV: (Math.random() - 0.5) * 0.15,\n                        shape: Math.random() > 0.5 ? 'rect' : 'circle',\n                        alpha: 1\n                    }));\n                    let frame = 0;\n                    (function animate() {\n                        c2.clearRect(0, 0, W, H);\n                        // Reverse for-loop: dead particles are swapped out immediately,\n                        // shrinking the array so subsequent frames iterate fewer elements.\n                        for (let i = particles.length - 1; i >= 0; i--) {\n                            const p = particles[i];\n                            p.x  += p.vx;\n                            p.y  += p.vy;\n                            p.vy += 0.07;\n                            p.rot += p.rotV;\n                            if (frame > 90) p.alpha -= 0.012;\n                            if (p.alpha <= 0) {\n                                // Swap with last element and truncate \u2014 O(1) removal, no array shift\n                                particles[i] = particles[particles.length - 1];\n                                particles.length--;\n                                continue;\n                            }\n                            c2.save();\n                            c2.globalAlpha = Math.max(0, p.alpha);\n                            c2.translate(p.x, p.y);\n                            c2.rotate(p.rot);\n                            c2.fillStyle = p.color;\n                            if (p.shape === 'rect') {\n                                c2.fillRect(-p.sz/2, -p.sz/4, p.sz, p.sz/2);\n                            } else {\n                                c2.beginPath(); c2.arc(0, 0, p.sz/2, 0, Math.PI*2); c2.fill();\n                            }\n                            c2.restore();\n                        }\n                        frame++;\n                        if (particles.length > 0) requestAnimationFrame(animate);\n                    })();\n                }\n            }\n\n            // Edit mode\n            if (mode === 'edit') {\n                const editControls = document.getElementById('edit-controls');\n                editControls.style.display = 'flex';\n\n                // \u2500\u2500 Drag-to-move layer offsets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                let isDragging = false;\n                let dragStartX = 0, dragStartY = 0;\n                let dragStartOffsetX = 0, dragStartOffsetY = 0;\n\n                // Map category \u2192 which config keys control its X/Y offset\n                function getDragKeys(layer) {\n                    const map = {\n                        top:         { y: 'topYOffset',       x: null },\n                        bottom:      { y: 'bottomYOffset',    x: null },\n                        dress:       { y: 'dressYOffset',     x: null },\n                        face:        { y: 'headYOffset',      x: 'headXOffset' },\n                        weapon:      { y: 'weaponYOffset',    x: 'weaponXOffset' },\n                        cape:        { y: 'capeYOffset',      x: 'capeXOffset' },\n                        pet:         { y: 'petYOffset',       x: 'petXOffset' },\n                        base_full:   { y: 'fullBodyYOffset',  x: 'fullBodyXOffset' },\n                        base_lower:  { y: 'lowerBodyYOffset', x: 'lowerBodyXOffset' },\n                        base_upper:  { y: 'upperBodyYOffset', x: 'upperBodyXOffset' },\n                        base_head:   { y: 'headYOffset',      x: 'headXOffset' },\n                        base_shadow: { y: 'shadowYOffset',    x: 'shadowXOffset' }\n                    };\n                    return map[layer] || null;\n                }\n\n                function getCanvasPos(e) {\n                    const r = canvas.getBoundingClientRect();\n                    const src = e.touches && e.touches.length > 0 ? e.touches[0] : e;\n                    return { x: src.clientX - r.left, y: src.clientY - r.top };\n                }\n\n                function onDragStart(e) {\n                    if (!activeCategory) return;\n                    const keys = getDragKeys(activeCategory);\n                    if (!keys) return;\n                    if (e.cancelable) e.preventDefault();\n                    isDragging = true;\n                    const pos = getCanvasPos(e);\n                    dragStartX = pos.x;\n                    dragStartY = pos.y;\n                    dragStartOffsetX = Number(config[keys.x] || 0);\n                    dragStartOffsetY = Number(config[keys.y] || 0);\n                }\n\n                function onDragMove(e) {\n                    if (!isDragging || !activeCategory) return;\n                    const keys = getDragKeys(activeCategory);\n                    if (!keys) return;\n                    if (e.cancelable) e.preventDefault();\n                    const pos = getCanvasPos(e);\n                    if (keys.y !== null) config[keys.y] = Math.round(dragStartOffsetY + (pos.y - dragStartY));\n                    if (keys.x !== null) config[keys.x] = Math.round(dragStartOffsetX + (pos.x - dragStartX));\n                }\n\n                function onDragEnd() { isDragging = false; }\n\n                canvas.addEventListener('mousedown',  onDragStart);\n                canvas.addEventListener('mousemove',  onDragMove);\n                canvas.addEventListener('mouseup',    onDragEnd);\n                canvas.addEventListener('touchstart', onDragStart, { passive: false });\n                canvas.addEventListener('touchmove',  onDragMove,  { passive: false });\n                canvas.addEventListener('touchend',   onDragEnd);\n\n                // \u2500\u2500 Save Offsets button (always visible) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                const saveOffsetsBtn = document.createElement('button');\n                saveOffsetsBtn.className = 'edit-btn';\n                saveOffsetsBtn.textContent = '\ud83d\udcbe Save Offsets';\n                saveOffsetsBtn.addEventListener('click', () => {\n                    // Offsets are already live in config (window.gameConfig).\n                    // This button provides explicit confirmation to the user.\n                    saveOffsetsBtn.textContent = '\u2705 Saved!';\n                    saveOffsetsBtn.style.background = '#27AE60';\n                    setTimeout(() => {\n                        saveOffsetsBtn.textContent = '\ud83d\udcbe Save Offsets';\n                        saveOffsetsBtn.style.background = '';\n                    }, 1800);\n                });\n                editControls.appendChild(saveOffsetsBtn);\n\n                // \u2500\u2500 Render edit control buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                // Base-layer selector buttons\n                const baseLayers = [\n                    { id: 'base_full',  label: '\ud83e\uddcd Full Body' },\n                    { id: 'base_lower', label: '\ud83e\uddb5 Lower Body' },\n                    { id: 'base_upper', label: '\ud83d\udc64 Upper Body' },\n                    { id: 'base_head',  label: '\ud83d\udde3 Head' },\n                    { id: 'base_shadow',label: '\u26ab Shadow' }\n                ];\n\n                function renderEditButtons() {\n                    // Preserve the save button; rebuild everything else\n                    editControls.innerHTML = '';\n                    editControls.appendChild(saveOffsetsBtn);\n\n                    // Base layer section header\n                    const baseHeader = document.createElement('div');\n                    baseHeader.style.cssText = 'background:rgba(255,255,255,0.92);border-radius:12px;padding:6px 10px;font-family:Fredoka,sans-serif;font-size:15px;font-weight:700;color:#2D2040;text-align:center;box-shadow:0 2px 10px rgba(0,0,0,0.1);';\n                    baseHeader.textContent = '\u2014 Base Layers \u2014';\n                    editControls.appendChild(baseHeader);\n\n                    baseLayers.forEach(bl => {\n                        const btn = document.createElement('button');\n                        btn.className = 'edit-btn' + (activeCategory === bl.id ? '' : '');\n                        btn.textContent = bl.label;\n                        btn.style.background = activeCategory === bl.id ? '#27AE60' : '';\n                        btn.style.fontSize = '15px';\n                        btn.style.padding = '9px 14px';\n                        btn.addEventListener('click', () => {\n                            activeCategory = bl.id;\n                            itemPanel.classList.remove('visible');\n                            updateCategoryButtons();\n                            renderEditButtons();\n                        });\n                        editControls.appendChild(btn);\n                    });\n\n                    if (activeCategory) {\n                        const keys = getDragKeys(activeCategory);\n                        if (keys) {\n                            const hint = document.createElement('div');\n                            hint.style.cssText = [\n                                'background:rgba(255,255,255,0.92)',\n                                'border-radius:12px',\n                                'padding:8px 14px',\n                                'font-family:Fredoka,sans-serif',\n                                'font-size:16px',\n                                'color:#2D2040',\n                                'text-align:center',\n                                'box-shadow:0 2px 10px rgba(0,0,0,0.15)',\n                                'line-height:1.4'\n                            ].join(';');\n                            hint.innerHTML = keys.x\n                                ? '\u2194\u2195 Drag canvas<br>to move item'\n                                : '\u2195 Drag canvas<br>to move item';\n                            editControls.appendChild(hint);\n                        }\n\n                        if (equipped[activeCategory]) {\n                            const removeBtn = document.createElement('button');\n                            removeBtn.className = 'edit-btn danger';\n                            removeBtn.textContent = '\ud83d\uddd1 Remove';\n                            removeBtn.addEventListener('click', () => removeItem(activeCategory, equipped[activeCategory]));\n                            editControls.appendChild(removeBtn);\n                        }\n\n                        const addBtn = document.createElement('button');\n                        addBtn.className = 'edit-btn';\n                        addBtn.textContent = '+ Add Item';\n                        addBtn.addEventListener('click', () => addItem(activeCategory));\n                        editControls.appendChild(addBtn);\n                    }\n                }\n\n                selectCategory = function(catId) {\n                    activeCategory = catId;\n                    const cat = categories.find(c => c.id === catId);\n                    panelTitle.textContent = cat.name;\n                    updateCategoryButtons();\n                    renderItemGrid();\n                    itemPanel.classList.add('visible');\n                    renderEditButtons();\n                };\n\n                // No rebind needed \u2014 categoryNav uses event delegation and always calls the current selectCategory\n\n                panelClose.addEventListener('click', () => {\n                    activeCategory = null;\n                    updateCategoryButtons();\n                    renderEditButtons();\n                });\n\n                function addItem(category) {\n                    const items = config.categories[category];\n                    const id = category + '_' + Date.now();\n                    const colors = ['#FF6B6B','#4ECDC4','#45B7D1','#96CEB4','#FFEAA7','#DDA0DD','#98D8C8','#F7DC6F'];\n                    const styles = {\n                        face: ['default'],\n                        top: ['tshirt','tank','hoodie','dress_shirt','crop'],\n                        bottom: ['pants','shorts','skirt','joggers'],\n                        dress: ['full'],\n                        weapon: ['sword'],\n                        cape: ['basic'],\n                        pet: ['default']\n                    };\n                    const styleOptions = styles[category] || ['default'];\n                    const newItem = {\n                        id,\n                        name: 'New ' + (items.length + 1),\n                        color: colors[Math.floor(Math.random() * colors.length)],\n                        style: styleOptions[Math.floor(Math.random() * styleOptions.length)]\n                    };\n                    items.push(newItem);\n                    config.categories[category] = items;\n                    renderItemGrid();\n                }\n\n                function removeItem(category, itemId) {\n                    config.categories[category] = config.categories[category].filter(i => i.id !== itemId);\n                    if (equipped[category] === itemId) equipped[category] = null;\n                    renderItemGrid();\n                    renderEditButtons();\n                }\n\n                // Also support face category drag via base_head button\n                // (already handled since getDragKeys('face') now returns headYOffset/headXOffset)\n\n                // Initial render\n                renderEditButtons();\n            }\n\n            // Show game parameters \u2014 all offset keys are flat top-level config keys\n            const flatOffsetDefaults = {\n                capeYOffset: 0, capeXOffset: 0, capeScale: 1.0,\n                bottomYOffset: 0, dressYOffset: 0, topYOffset: 0,\n                fullBodyYOffset: 0, fullBodyXOffset: 0,\n                lowerBodyYOffset: 0, lowerBodyXOffset: 0,\n                upperBodyYOffset: 0, upperBodyXOffset: 0,\n                headYOffset: 0, headXOffset: 0,\n                headRow1YOffset: 0, headRow2YOffset: 0, headRow3YOffset: 0, headRow4YOffset: 0,\n                weaponYOffset: 0, weaponXOffset: 0, weaponScale: 0.55,\n                petYOffset: 0, petXOffset: 0, petScale: 0.35,\n                shadowYOffset: config.shadowYOffset || 0,\n                shadowXOffset: config.shadowXOffset || 0,\n                shadowSize: 1.0\n            };\n            Object.keys(flatOffsetDefaults).forEach(k => {\n                if (config[k] === undefined || config[k] === null) config[k] = flatOffsetDefaults[k];\n            });\n\n            lib.showGameParameters({\n                name: 'Avatar Studio',\n                params: {\n                    'Skin Color':           { key: 'skinColor',       type: 'color',  onChange: v => { config.skinColor = v; } },\n                    'Character Scale':      { key: 'characterScale',  type: 'slider', min: 0.3,  max: 1.5,  step: 0.01, onChange: v => { config.characterScale  = Number(v); } },\n                    'Shadow Y Offset':      { key: 'shadowYOffset',   type: 'slider', min: -300, max: 500, step: 1, onChange: v => { config.shadowYOffset     = Number(v); } },\n                    'Shadow X Offset':      { key: 'shadowXOffset',   type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.shadowXOffset     = Number(v); } },\n                    'Shadow Size':          { key: 'shadowSize',      type: 'slider', min: 0.1,  max: 3.0, step: 0.05, onChange: v => { config.shadowSize       = Number(v); } },\n                    'Head Y Offset':        { key: 'headYOffset',     type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.headYOffset      = Number(v); } },\n                    'Top Y Offset':         { key: 'topYOffset',      type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.topYOffset        = Number(v); } },\n                    'Bottom Y Offset':      { key: 'bottomYOffset',   type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.bottomYOffset     = Number(v); } },\n                    'Dress Y Offset':       { key: 'dressYOffset',    type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.dressYOffset      = Number(v); } },\n                    'Full Body Y Offset':   { key: 'fullBodyYOffset',  type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.fullBodyYOffset   = Number(v); } },\n                    'Full Body X Offset':   { key: 'fullBodyXOffset',  type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.fullBodyXOffset   = Number(v); } },\n                    'Lower Body Y Offset':  { key: 'lowerBodyYOffset', type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.lowerBodyYOffset  = Number(v); } },\n                    'Lower Body X Offset':  { key: 'lowerBodyXOffset', type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.lowerBodyXOffset  = Number(v); } },\n                    'Upper Body Y Offset':  { key: 'upperBodyYOffset', type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.upperBodyYOffset  = Number(v); } },\n                    'Upper Body X Offset':  { key: 'upperBodyXOffset', type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.upperBodyXOffset  = Number(v); } },\n                    'Head X Offset':        { key: 'headXOffset',      type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.headXOffset       = Number(v); } },\n                    'Face Row 1 Y Fix':     { key: 'headRow1YOffset',  type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.headRow1YOffset  = Number(v); } },\n                    'Face Row 2 Y Fix':     { key: 'headRow2YOffset',  type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.headRow2YOffset  = Number(v); } },\n                    'Face Row 3 Y Fix':     { key: 'headRow3YOffset',  type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.headRow3YOffset  = Number(v); } },\n                    'Face Row 4 Y Fix':     { key: 'headRow4YOffset',  type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.headRow4YOffset  = Number(v); } },\n                    'Cape Y Offset':        { key: 'capeYOffset',     type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.capeYOffset       = Number(v); } },\n                    'Cape X Offset':        { key: 'capeXOffset',     type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.capeXOffset       = Number(v); } },\n                    'Cape Scale':           { key: 'capeScale',       type: 'slider', min: 0.1,  max: 2.0,  step: 0.01, onChange: v => { config.capeScale       = Number(v); } },\n                    'Weapon Y Offset':      { key: 'weaponYOffset',   type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.weaponYOffset     = Number(v); } },\n                    'Weapon X Offset':      { key: 'weaponXOffset',   type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.weaponXOffset     = Number(v); } },\n                    'Weapon Scale':         { key: 'weaponScale',     type: 'slider', min: 0.1,  max: 2.0,  step: 0.01, onChange: v => { config.weaponScale     = Number(v); } },\n                    'Pet Y Offset':         { key: 'petYOffset',      type: 'slider', min: -300, max: 300, step: 1, onChange: v => { config.petYOffset        = Number(v); } },\n                    'Pet X Offset':         { key: 'petXOffset',      type: 'slider', min: -400, max: 400, step: 1, onChange: v => { config.petXOffset        = Number(v); } },\n                    'Pet Scale':            { key: 'petScale',        type: 'slider', min: 0.1,  max: 2.0,  step: 0.01, onChange: v => { config.petScale        = Number(v); } }\n                }\n            });\n        }\n    </script>\n</body>\n</html>", "game_config": "{\n  \"skinColor\": \"#FFDAB9\",\n  \"startingOutfit\": {\n    \"face\": null,\n    \"top\": null,\n    \"bottom\": null,\n    \"dress\": null,\n    \"weapon\": null,\n    \"cape\": null,\n    \"pet\": null\n  },\n  \"categories\": {\n    \"face\": [\n      {\n        \"col\": 0,\n        \"id\": \"head_0\",\n        \"name\": \"Pixie Cut\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"head_1\",\n        \"name\": \"Tousled Brown\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"head_2\",\n        \"name\": \"Honey Bob\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"head_3\",\n        \"name\": \"Fiery Spikes\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"head_4\",\n        \"name\": \"Ocean Waves\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 0,\n        \"id\": \"head_5\",\n        \"name\": \"Midnight Flow\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"head_6\",\n        \"name\": \"Chestnut Waves\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"head_7\",\n        \"name\": \"Golden Straight\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"head_8\",\n        \"name\": \"Bubblegum Curls\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"head_9\",\n        \"name\": \"Space Buns\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 0,\n        \"id\": \"head_10\",\n        \"name\": \"Elegant Updo\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"head_11\",\n        \"name\": \"Slick Back\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"head_12\",\n        \"name\": \"Lavender Buns\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"head_13\",\n        \"name\": \"Platinum Bob\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"head_14\",\n        \"name\": \"Ginger Buzz\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 0,\n        \"id\": \"head_15\",\n        \"name\": \"Cute Messy Bun\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"head_16\",\n        \"name\": \"Teal Dream\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"head_17\",\n        \"name\": \"Rainbow Updo\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"head_18\",\n        \"name\": \"Silver Fishtail\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"head_19\",\n        \"name\": \"Dark Ringlets\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 0,\n        \"id\": \"head_20\",\n        \"name\": \"Mint Braids\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"head_21\",\n        \"name\": \"Split Dye\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"head_22\",\n        \"name\": \"Golden Locs\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"head_23\",\n        \"name\": \"Frost Spikes\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_heads\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"head_24\",\n        \"name\": \"Violet Updo\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_heads\"\n      }\n    ],\n    \"top\": [\n      {\n        \"id\": \"upper_0\",\n        \"name\": \"White Cami\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 0,\n        \"row\": 0\n      },\n      {\n        \"id\": \"upper_1\",\n        \"name\": \"Classic Black Tee\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 1,\n        \"row\": 0\n      },\n      {\n        \"id\": \"upper_2\",\n        \"name\": \"Cherry Red Top\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 2,\n        \"row\": 0\n      },\n      {\n        \"id\": \"upper_3\",\n        \"name\": \"Cozy Green Knit\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 3,\n        \"row\": 0\n      },\n      {\n        \"id\": \"upper_4\",\n        \"name\": \"Groovy Tie-Dye\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 4,\n        \"row\": 0\n      },\n      {\n        \"id\": \"upper_5\",\n        \"name\": \"Blue Plaid Shirt\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 0,\n        \"row\": 1\n      },\n      {\n        \"id\": \"upper_6\",\n        \"name\": \"Heather Pullover\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 1,\n        \"row\": 1\n      },\n      {\n        \"id\": \"upper_7\",\n        \"name\": \"Vintage Denim Jacket\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 2,\n        \"row\": 1\n      },\n      {\n        \"id\": \"upper_8\",\n        \"name\": \"Sporty Jersey\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 3,\n        \"row\": 1\n      },\n      {\n        \"id\": \"upper_9\",\n        \"name\": \"Varsity Baseball Tee\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 4,\n        \"row\": 1\n      },\n      {\n        \"id\": \"upper_10\",\n        \"name\": \"Cream Cable Knit\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 0,\n        \"row\": 2\n      },\n      {\n        \"id\": \"upper_11\",\n        \"name\": \"Streetwear Hoodie\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 1,\n        \"row\": 2\n      },\n      {\n        \"id\": \"upper_12\",\n        \"name\": \"Preppy Polo\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 2,\n        \"row\": 2\n      },\n      {\n        \"id\": \"upper_13\",\n        \"name\": \"Biker Leather Jacket\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 3,\n        \"row\": 2\n      },\n      {\n        \"id\": \"upper_14\",\n        \"name\": \"Sunshine Stripes\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 4,\n        \"row\": 2\n      },\n      {\n        \"id\": \"upper_15\",\n        \"name\": \"Red Track Jacket\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 0,\n        \"row\": 3\n      },\n      {\n        \"id\": \"upper_16\",\n        \"name\": \"Navy Oversized Hoodie\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 1,\n        \"row\": 3\n      },\n      {\n        \"id\": \"upper_17\",\n        \"name\": \"Forest Turtleneck\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 2,\n        \"row\": 3\n      },\n      {\n        \"id\": \"upper_18\",\n        \"name\": \"Sky Blue Crop\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 3,\n        \"row\": 3\n      },\n      {\n        \"id\": \"upper_19\",\n        \"name\": \"Kitty Graphic Tee\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 4,\n        \"row\": 3\n      },\n      {\n        \"id\": \"upper_20\",\n        \"name\": \"Sunny Floral Blouse\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 0,\n        \"row\": 4\n      },\n      {\n        \"id\": \"upper_21\",\n        \"name\": \"Lavender Hoodie\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 1,\n        \"row\": 4\n      },\n      {\n        \"id\": \"upper_22\",\n        \"name\": \"Preppy Argyle Vest\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 2,\n        \"row\": 4\n      },\n      {\n        \"id\": \"upper_23\",\n        \"name\": \"Mint Bomber Jacket\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 3,\n        \"row\": 4\n      },\n      {\n        \"id\": \"upper_24\",\n        \"name\": \"Pink Ruffle Blouse\",\n        \"spritesheet\": \"spritesheet_upper\",\n        \"col\": 4,\n        \"row\": 4\n      }\n    ],\n    \"bottom\": [\n      {\n        \"col\": 0,\n        \"id\": \"lower_0\",\n        \"name\": \"Classic Blue Jeans\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"lower_1\",\n        \"name\": \"Utility Cargo Shorts\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"lower_2\",\n        \"name\": \"Khaki Chinos\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"lower_3\",\n        \"name\": \"Sporty Track Pants\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"lower_4\",\n        \"name\": \"Floral Print Pants\",\n        \"row\": 0,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 0,\n        \"id\": \"lower_5\",\n        \"name\": \"Denim Mini Skirt\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"lower_6\",\n        \"name\": \"Sleek Black Leggings\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"lower_7\",\n        \"name\": \"Olive Bermudas\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"lower_8\",\n        \"name\": \"Blue Cropped Capris\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"lower_9\",\n        \"name\": \"Garden Floral Skirt\",\n        \"row\": 1,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 0,\n        \"id\": \"lower_10\",\n        \"name\": \"Tailored Black Slacks\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"lower_11\",\n        \"name\": \"Pleated School Skirt\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"lower_12\",\n        \"name\": \"Gray Mini Skirt\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"lower_13\",\n        \"name\": \"Checkered Crops\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"lower_14\",\n        \"name\": \"Jeans & Ankle Boots\",\n        \"row\": 2,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 0,\n        \"id\": \"lower_15\",\n        \"name\": \"Beach Board Shorts\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"lower_16\",\n        \"name\": \"Sporty Gym Shorts\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"lower_17\",\n        \"name\": \"White Tennis Skirt\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"lower_18\",\n        \"name\": \"Cozy Sweatpants\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"lower_19\",\n        \"name\": \"Relaxed Fit Jeans\",\n        \"row\": 3,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 0,\n        \"id\": \"lower_20\",\n        \"name\": \"Pink Ruffle Skirt\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 1,\n        \"id\": \"lower_21\",\n        \"name\": \"Chic Wide-Leg Pants\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 2,\n        \"id\": \"lower_22\",\n        \"name\": \"Military Cargo Pants\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 3,\n        \"id\": \"lower_23\",\n        \"name\": \"Vintage Knickers\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_lower\"\n      },\n      {\n        \"col\": 4,\n        \"id\": \"lower_24\",\n        \"name\": \"Retro Bell Bottoms\",\n        \"row\": 4,\n        \"spritesheet\": \"spritesheet_lower\"\n      }\n    ],\n    \"dress\": [\n      {\n        \"id\": \"full_0\",\n        \"name\": \"Dreamhouse Gala\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 0,\n        \"row\": 0\n      },\n      {\n        \"id\": \"full_1\",\n        \"name\": \"Malibu Roller Date\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 1,\n        \"row\": 0\n      },\n      {\n        \"id\": \"full_2\",\n        \"name\": \"Lilac Power Set\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 2,\n        \"row\": 0\n      },\n      {\n        \"id\": \"full_3\",\n        \"name\": \"Western Chic\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 3,\n        \"row\": 0\n      },\n      {\n        \"id\": \"full_4\",\n        \"name\": \"Star Voyager\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 4,\n        \"row\": 0\n      },\n      {\n        \"id\": \"full_5\",\n        \"name\": \"Prima Ballerina\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 0,\n        \"row\": 1\n      },\n      {\n        \"id\": \"full_6\",\n        \"name\": \"Pink Clinic Hero\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 1,\n        \"row\": 1\n      },\n      {\n        \"id\": \"full_7\",\n        \"name\": \"Ocean Pearl Mermaid\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 2,\n        \"row\": 1\n      },\n      {\n        \"id\": \"full_8\",\n        \"name\": \"Royal Pink Princess\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 3,\n        \"row\": 1\n      },\n      {\n        \"id\": \"full_9\",\n        \"name\": \"Neon Stage Idol\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 4,\n        \"row\": 1\n      },\n      {\n        \"id\": \"full_10\",\n        \"name\": \"Enchanted Rose Gown\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 0,\n        \"row\": 2\n      },\n      {\n        \"id\": \"full_11\",\n        \"name\": \"Court Ace Dress\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 1,\n        \"row\": 2\n      },\n      {\n        \"id\": \"full_12\",\n        \"name\": \"Emerald Evening Shine\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 2,\n        \"row\": 2\n      },\n      {\n        \"id\": \"full_13\",\n        \"name\": \"Safari Explorer Chic\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 3,\n        \"row\": 2\n      },\n      {\n        \"id\": \"full_14\",\n        \"name\": \"Diamond Bridal Elegance\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 4,\n        \"row\": 2\n      },\n      {\n        \"id\": \"full_15\",\n        \"name\": \"Disco Mirror Spark\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 0,\n        \"row\": 3\n      },\n      {\n        \"id\": \"full_16\",\n        \"name\": \"CEO Glam Suit\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 1,\n        \"row\": 3\n      },\n      {\n        \"id\": \"full_17\",\n        \"name\": \"Cheer Squad Sparkle\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 2,\n        \"row\": 3\n      },\n      {\n        \"id\": \"full_18\",\n        \"name\": \"Moonlight Witch Glam\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 3,\n        \"row\": 3\n      },\n      {\n        \"id\": \"full_19\",\n        \"name\": \"Vintage Swing Rose\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 4,\n        \"row\": 3\n      },\n      {\n        \"id\": \"full_20\",\n        \"name\": \"Sporty Glow Set\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 0,\n        \"row\": 4\n      },\n      {\n        \"id\": \"full_21\",\n        \"name\": \"Garden Blossom Party\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 1,\n        \"row\": 4\n      },\n      {\n        \"id\": \"full_22\",\n        \"name\": \"Sky Captain Uniform\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 2,\n        \"row\": 4\n      },\n      {\n        \"id\": \"full_23\",\n        \"name\": \"Sunset Tropic Maxi\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 3,\n        \"row\": 4\n      },\n      {\n        \"id\": \"full_24\",\n        \"name\": \"Holo Pop Superstar\",\n        \"spritesheet\": \"spritesheet_fullbody\",\n        \"col\": 4,\n        \"row\": 4\n      }\n    ],\n    \"shoes\": [\n      {\n        \"id\": \"shoes_red_sneakers\",\n        \"name\": \"Red Sneakers\",\n        \"assetId\": \"shoes_red_sneakers\",\n        \"color\": \"#DC143C\",\n        \"style\": \"sneakers\"\n      },\n      {\n        \"id\": \"shoes_brown_boots\",\n        \"name\": \"Brown Boots\",\n        \"assetId\": \"shoes_brown_boots\",\n        \"color\": \"#8B4513\",\n        \"style\": \"boots\"\n      }\n    ],\n    \"weapon\": [\n      {\n        \"id\": \"wpn_0\",\n        \"name\": \"Pink Tote\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 0,\n        \"row\": 0\n      },\n      {\n        \"id\": \"wpn_1\",\n        \"name\": \"Barbie Crossbody\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 1,\n        \"row\": 0\n      },\n      {\n        \"id\": \"wpn_2\",\n        \"name\": \"Fashion Print Tote\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 2,\n        \"row\": 0\n      },\n      {\n        \"id\": \"wpn_3\",\n        \"name\": \"Pink Sparkle Clutch\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 3,\n        \"row\": 0\n      },\n      {\n        \"id\": \"wpn_4\",\n        \"name\": \"Gold Quilted Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 4,\n        \"row\": 0\n      },\n      {\n        \"id\": \"wpn_5\",\n        \"name\": \"Purple Fur Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 0,\n        \"row\": 1\n      },\n      {\n        \"id\": \"wpn_6\",\n        \"name\": \"Mirror Backpack\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 1,\n        \"row\": 1\n      },\n      {\n        \"id\": \"wpn_7\",\n        \"name\": \"Purple Palace Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 2,\n        \"row\": 1\n      },\n      {\n        \"id\": \"wpn_8\",\n        \"name\": \"Pink Bow Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 3,\n        \"row\": 1\n      },\n      {\n        \"id\": \"wpn_9\",\n        \"name\": \"Silver Case\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 4,\n        \"row\": 1\n      },\n      {\n        \"id\": \"wpn_10\",\n        \"name\": \"Teal Paw Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 0,\n        \"row\": 2\n      },\n      {\n        \"id\": \"wpn_11\",\n        \"name\": \"Medical Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 1,\n        \"row\": 2\n      },\n      {\n        \"id\": \"wpn_12\",\n        \"name\": \"Lightning Clutch\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 2,\n        \"row\": 2\n      },\n      {\n        \"id\": \"wpn_13\",\n        \"name\": \"Beach Tote\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 3,\n        \"row\": 2\n      },\n      {\n        \"id\": \"wpn_14\",\n        \"name\": \"Holographic Barbie\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 4,\n        \"row\": 2\n      },\n      {\n        \"id\": \"wpn_15\",\n        \"name\": \"Mermaid Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 0,\n        \"row\": 3\n      },\n      {\n        \"id\": \"wpn_16\",\n        \"name\": \"Butterfly Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 1,\n        \"row\": 3\n      },\n      {\n        \"id\": \"wpn_17\",\n        \"name\": \"Adventure Satchel\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 2,\n        \"row\": 3\n      },\n      {\n        \"id\": \"wpn_18\",\n        \"name\": \"Galaxy Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 3,\n        \"row\": 3\n      },\n      {\n        \"id\": \"wpn_19\",\n        \"name\": \"Pearl Clutch\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 4,\n        \"row\": 3\n      },\n      {\n        \"id\": \"wpn_20\",\n        \"name\": \"Roller Skate Bag\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 0,\n        \"row\": 4\n      },\n      {\n        \"id\": \"wpn_21\",\n        \"name\": \"Checkered Tote\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 1,\n        \"row\": 4\n      },\n      {\n        \"id\": \"wpn_22\",\n        \"name\": \"Disco Necklace\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 2,\n        \"row\": 4\n      },\n      {\n        \"id\": \"wpn_23\",\n        \"name\": \"Hair Dryer\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 3,\n        \"row\": 4\n      },\n      {\n        \"id\": \"wpn_24\",\n        \"name\": \"Pink Piano\",\n        \"spritesheet\": \"spritesheet_weapons\",\n        \"col\": 4,\n        \"row\": 4\n      }\n    ],\n    \"wings\": [],\n    \"pet\": [\n      {\n        \"id\": \"pet_0\",\n        \"name\": \"Pompom Poodle\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 0,\n        \"row\": 0\n      },\n      {\n        \"id\": \"pet_1\",\n        \"name\": \"Cloud Unicorn\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 1,\n        \"row\": 0\n      },\n      {\n        \"id\": \"pet_2\",\n        \"name\": \"Sunset Flamingo\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 2,\n        \"row\": 0\n      },\n      {\n        \"id\": \"pet_3\",\n        \"name\": \"Velvet Bunny\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 3,\n        \"row\": 0\n      },\n      {\n        \"id\": \"pet_4\",\n        \"name\": \"Starlight Kitty\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 4,\n        \"row\": 0\n      },\n      {\n        \"id\": \"pet_5\",\n        \"name\": \"Pocket Chihuahua\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 0,\n        \"row\": 1\n      },\n      {\n        \"id\": \"pet_6\",\n        \"name\": \"Mini Pegasus\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 1,\n        \"row\": 1\n      },\n      {\n        \"id\": \"pet_7\",\n        \"name\": \"Prism Butterfly\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 2,\n        \"row\": 1\n      },\n      {\n        \"id\": \"pet_8\",\n        \"name\": \"Carousel Pony\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 3,\n        \"row\": 1\n      },\n      {\n        \"id\": \"pet_9\",\n        \"name\": \"Fashion Parrot\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 4,\n        \"row\": 1\n      },\n      {\n        \"id\": \"pet_10\",\n        \"name\": \"Bubblegum Elephant\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 0,\n        \"row\": 2\n      },\n      {\n        \"id\": \"pet_11\",\n        \"name\": \"Disco Orb\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 1,\n        \"row\": 2\n      },\n      {\n        \"id\": \"pet_12\",\n        \"name\": \"Candy Dolphin\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 2,\n        \"row\": 2\n      },\n      {\n        \"id\": \"pet_13\",\n        \"name\": \"Swan Princess\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 3,\n        \"row\": 2\n      },\n      {\n        \"id\": \"pet_14\",\n        \"name\": \"Jewel Peacock\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 4,\n        \"row\": 2\n      },\n      {\n        \"id\": \"pet_15\",\n        \"name\": \"Plush Teddy\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 0,\n        \"row\": 3\n      },\n      {\n        \"id\": \"pet_16\",\n        \"name\": \"Sparkle Koi\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 1,\n        \"row\": 3\n      },\n      {\n        \"id\": \"pet_17\",\n        \"name\": \"Glitter Seal Pup\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 2,\n        \"row\": 3\n      },\n      {\n        \"id\": \"pet_18\",\n        \"name\": \"Pink Cruiser\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 3,\n        \"row\": 3\n      },\n      {\n        \"id\": \"pet_19\",\n        \"name\": \"Purse Pup\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 4,\n        \"row\": 3\n      },\n      {\n        \"id\": \"pet_20\",\n        \"name\": \"Rainbow Filly\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 0,\n        \"row\": 4\n      },\n      {\n        \"id\": \"pet_21\",\n        \"name\": \"Fairy Pix\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 1,\n        \"row\": 4\n      },\n      {\n        \"id\": \"pet_22\",\n        \"name\": \"Crystal Dragonet\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 2,\n        \"row\": 4\n      },\n      {\n        \"id\": \"pet_23\",\n        \"name\": \"Glam Fox\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 3,\n        \"row\": 4\n      },\n      {\n        \"id\": \"pet_24\",\n        \"name\": \"Cupcake Companion\",\n        \"spritesheet\": \"spritesheet_pets\",\n        \"col\": 4,\n        \"row\": 4\n      }\n    ],\n    \"cape\": [\n      {\n        \"id\": \"cape_0\",\n        \"name\": \"Pink Butterfly Wings\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 0,\n        \"row\": 0\n      },\n      {\n        \"id\": \"cape_1\",\n        \"name\": \"Rainbow Visor\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 1,\n        \"row\": 0\n      },\n      {\n        \"id\": \"cape_2\",\n        \"name\": \"Checkered Cape\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 2,\n        \"row\": 0\n      },\n      {\n        \"id\": \"cape_3\",\n        \"name\": \"Heart & Lipstick\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 3,\n        \"row\": 0\n      },\n      {\n        \"id\": \"cape_4\",\n        \"name\": \"Barbie Script\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 4,\n        \"row\": 0\n      },\n      {\n        \"id\": \"cape_5\",\n        \"name\": \"Jetpack Wings\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 0,\n        \"row\": 1\n      },\n      {\n        \"id\": \"cape_6\",\n        \"name\": \"Karaoke Mic\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 1,\n        \"row\": 1\n      },\n      {\n        \"id\": \"cape_7\",\n        \"name\": \"Rainbow Butterfly\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 2,\n        \"row\": 1\n      },\n      {\n        \"id\": \"cape_8\",\n        \"name\": \"Crystal Crown\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 3,\n        \"row\": 1\n      },\n      {\n        \"id\": \"cape_9\",\n        \"name\": \"Galaxy Swirl\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 4,\n        \"row\": 1\n      },\n      {\n        \"id\": \"cape_10\",\n        \"name\": \"B Butterfly Wings\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 0,\n        \"row\": 2\n      },\n      {\n        \"id\": \"cape_11\",\n        \"name\": \"Pink Dollhouse\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 1,\n        \"row\": 2\n      },\n      {\n        \"id\": \"cape_12\",\n        \"name\": \"Swan Wings\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 2,\n        \"row\": 2\n      },\n      {\n        \"id\": \"cape_13\",\n        \"name\": \"Pink Ribbons\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 3,\n        \"row\": 2\n      },\n      {\n        \"id\": \"cape_14\",\n        \"name\": \"Medical Kit\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 4,\n        \"row\": 2\n      },\n      {\n        \"id\": \"cape_15\",\n        \"name\": \"Steampunk Wings\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 0,\n        \"row\": 3\n      },\n      {\n        \"id\": \"cape_16\",\n        \"name\": \"Pink Tutu\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 1,\n        \"row\": 3\n      },\n      {\n        \"id\": \"cape_17\",\n        \"name\": \"Barbie Logo\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 2,\n        \"row\": 3\n      },\n      {\n        \"id\": \"cape_18\",\n        \"name\": \"Barbie Friends\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 3,\n        \"row\": 3\n      },\n      {\n        \"id\": \"cape_19\",\n        \"name\": \"Camera Lens\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 4,\n        \"row\": 3\n      },\n      {\n        \"id\": \"cape_20\",\n        \"name\": \"Angel Wings\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 0,\n        \"row\": 4\n      },\n      {\n        \"id\": \"cape_21\",\n        \"name\": \"Pink Wardrobe\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 1,\n        \"row\": 4\n      },\n      {\n        \"id\": \"cape_22\",\n        \"name\": \"Star Trail\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 2,\n        \"row\": 4\n      },\n      {\n        \"id\": \"cape_23\",\n        \"name\": \"Rainbow Glasses\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 3,\n        \"row\": 4\n      },\n      {\n        \"id\": \"cape_24\",\n        \"name\": \"Pink Galaxy Swirl\",\n        \"spritesheet\": \"spritesheet_capes\",\n        \"col\": 4,\n        \"row\": 4\n      }\n    ],\n    \"hat\": [\n      {\n        \"assetId\": \"hat_blue_cap\",\n        \"color\": \"#4169E1\",\n        \"id\": \"hat_blue_cap\",\n        \"name\": \"Blue Cap\",\n        \"style\": \"cap\"\n      },\n      {\n        \"assetId\": \"hat_wizard_hat\",\n        \"color\": \"#800080\",\n        \"id\": \"hat_wizard_hat\",\n        \"name\": \"Wizard Hat\",\n        \"style\": \"wizard\"\n      }\n    ],\n    \"hair\": []\n  },\n  \"offsets\": {\n    \"wings\": {\n      \"y\": 0\n    },\n    \"bottom\": {\n      \"y\": 0\n    },\n    \"dress\": {\n      \"y\": 0\n    },\n    \"top\": {\n      \"y\": 0\n    },\n    \"face\": {\n      \"y\": 61\n    },\n    \"weapon\": {\n      \"y\": 0\n    },\n    \"pet\": {\n      \"y\": 0,\n      \"x\": 0\n    }\n  },\n  \"characterScale\": 0.4,\n  \"wingsYOffset\": 0,\n  \"bottomYOffset\": 0,\n  \"dressYOffset\": 0,\n  \"topYOffset\": 0,\n  \"faceYOffset\": 0,\n  \"weaponYOffset\": 159,\n  \"petYOffset\": 15,\n  \"petXOffset\": 30,\n  \"capeYOffset\": 0,\n  \"headYOffset\": -87,\n  \"hatYOffset\": 0,\n  \"shoesYOffset\": 0,\n  \"weaponXOffset\": -162,\n  \"weaponScale\": 0.58,\n  \"petScale\": 1,\n  \"capeXOffset\": 0,\n  \"capeScale\": 1.6,\n  \"fullBodyYOffset\": 122,\n  \"fullBodyXOffset\": 0,\n  \"lowerBodyYOffset\": 200,\n  \"lowerBodyXOffset\": -4,\n  \"upperBodyYOffset\": 120,\n  \"upperBodyXOffset\": 0,\n  \"headXOffset\": -3,\n  \"headRow1YOffset\": 23,\n  \"headRow2YOffset\": 5,\n  \"headRow3YOffset\": 5,\n  \"headRow4YOffset\": 5,\n  \"shadowSize\": 1.7,\n  \"shadowXOffset\": 0,\n  \"shadowYOffset\": 114,\n  \"disabledItems\": {\n    \"face\": {\n      \"head_0\": true,\n      \"head_1\": true,\n      \"head_3\": true,\n      \"head_20\": true,\n      \"head_22\": true,\n      \"head_23\": true\n    },\n    \"top\": {\n      \"upper_14\": true,\n      \"upper_21\": true\n    },\n    \"cape\": {\n      \"cape_2\": true,\n      \"cape_5\": true,\n      \"cape_11\": true,\n      \"cape_16\": true,\n      \"cape_1\": true\n    },\n    \"weapon\": {\n      \"wpn_1\": true,\n      \"wpn_3\": true,\n      \"wpn_9\": true,\n      \"wpn_12\": true,\n      \"wpn_15\": true,\n      \"wpn_16\": true,\n      \"wpn_17\": true,\n      \"wpn_22\": true,\n      \"wpn_23\": true,\n      \"wpn_24\": true\n    }\n  }\n}", "game_design_document": "## Core Game Loop and Mechanics\n\n**MapleStory Avatar Studio** is a character customization experience that captures the beloved dress-up aspect of MapleStory. Players create and style their own unique character by mixing and matching clothing, hairstyles, accessories, and more.\n\n**Core Loop:**\n1. Player starts with a base character model in underwear/basic outfit\n2. Player browses through categorized wardrobe options (tops, bottoms, shoes, hair, face, accessories)\n3. Player taps items to equip them on their character\n4. Character updates in real-time to show the new look\n5. Player can save their favorite looks and create new ones\n6. Player can cycle through different poses/expressions for their character\n\n**What Makes It Engaging:**\n- Instant visual feedback as items are equipped\n- Vast combination possibilities create unique personal expression\n- Nostalgic MapleStory aesthetic appeals to fans\n- Low-pressure creative sandbox with no fail state\n- Satisfying to experiment and discover new combinations\n\n## Game Perspective\n\n**Fixed-screen** with a centered character display. The character model occupies the center of the screen while UI panels for item selection appear around the edges. This allows players to see their full character while browsing options.\n\n## Game World and Level Design\n\nThe game takes place in a single screen divided into functional zones:\n\n**Character Display Area (Center):**\n- Large, prominent character model showing the current outfit\n- Character stands on a simple platform/ground\n- Background can be toggled between different MapleStory-themed scenes (Henesys, Ellinia, etc.)\n\n**Category Navigation (Left Side):**\n- Vertical list of wardrobe categories with icons\n- Categories: Hair, Face, Top, Bottom, Shoes, Weapon, Cape, Hat, Overall (one-piece outfits)\n- Tapping a category opens its item grid\n\n**Item Selection Panel (Right Side/Bottom):**\n- Grid of thumbnail previews for items in the selected category\n- Scrollable to accommodate many items\n- Each item shows a small preview icon\n- Currently equipped item is highlighted\n\n**Action Bar (Top):**\n- \"New Look\" button (clears all items, resets to base)\n- \"Random\" button (equips random items from each category)\n- Background selector dropdown\n- Pose/expression selector\n\n## Edit Mode Design\n\nEdit mode allows creators to customize the wardrobe and game experience:\n\n**Editable Properties:**\n- **Item Collections:** Add/remove items in each category, upload custom item sprites\n- **Color Palettes:** Adjust skin tones, base colors available for character\n- **Background Options:** Add/remove background scene options\n- **UI Theme:** Adjust panel colors, button styles to match desired aesthetic\n- **Starting Outfit:** Set which items the character starts with\n- **Category Names:** Rename categories to fit different themes (e.g., \"Armor\" instead of \"Top\")\n\n**Constraints:**\n- Minimum 3 items per category to ensure variety\n- Item sprites must be transparent PNGs sized appropriately for the character model\n- At least one background option must be available\n\n## Control Design\n\n**Mobile (Touch - Primary):**\n- **Tap category button** \u2192 Opens that category's item grid\n- **Tap item thumbnail** \u2192 Equips that item on character\n- **Tap equipped item again** \u2192 Unequips it (returns to base/underwear for that slot)\n- **Swipe on item grid** \u2192 Scroll through available items\n- **Tap action buttons** \u2192 Execute actions (New Look, Random, etc.)\n- **Tap background/pose selectors** \u2192 Open dropdown menus\n\n**Desktop (Mouse/Keyboard):**\n- **Click category button** \u2192 Opens that category's item grid\n- **Click item thumbnail** \u2192 Equips that item on character\n- **Click equipped item again** \u2192 Unequips it\n- **Mouse wheel on item grid** \u2192 Scroll through items\n- **Click action buttons** \u2192 Execute actions\n- **Number keys 1-9** \u2192 Quick-select categories\n- **Arrow keys** \u2192 Navigate through items in current category\n- **Enter/Space** \u2192 Equip highlighted item\n- **Escape** \u2192 Clear current category selection\n\nAll controls are designed for easy one-handed mobile use with large touch targets (minimum 96x96px).\n\n## Win/Lose Conditions\n\nThis is a creative sandbox with no traditional win/lose conditions. Success is defined by player satisfaction with their created look. The game encourages experimentation and personal expression without pressure or failure states.\n\n**Engagement Hooks:**\n- Discovering new item combinations\n- Creating themed outfits (warrior, mage, thief, etc.)\n- Recreating favorite MapleStory characters\n- Designing original characters\n\n## Art Style and Visual Theme\n\n**Art Style:** Authentic MapleStory-inspired 2D sprite art with a charming, colorful anime aesthetic. Characters should have the distinctive chibi proportions and vibrant style that MapleStory is known for.\n\n**Visual Theme:**\n- Bright, saturated colors typical of MapleStory\n- Clean UI with rounded corners and soft shadows\n- Item thumbnails clearly show what each piece looks like\n- Character sprites should be large enough to see details (approximately 300-400px tall)\n- Backgrounds feature iconic MapleStory locations with soft, painterly style\n\n**Asset Specifications:**\n- **Character Base:** Chibi-style character with large head, small body proportions\n- **Clothing Layers:** Each item is a separate sprite that overlays on the base character\n- **Consistent Scale:** All items must match the character's proportions\n- **Transparent Backgrounds:** All character and item sprites use transparency\n- **UI Elements:** Soft, rounded panels with subtle gradients matching MapleStory's friendly aesthetic\n\n**Color Palette:**\n- Warm, inviting colors for UI (soft blues, gentle purples, warm yellows)\n- High contrast for readability\n- Consistent with MapleStory's vibrant, optimistic tone", "persistent_storage_config": {"leaderboard": {"sort_ascending": false}}, "game_metadata": {"game_id": "01KVG8CR3YPGSP4FA3RCNTCTS6", "title": "BARBIE Styling Studio", "description": "Created with Astrocade", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_be6cd1b3-f977-4b08-af30-04c3333dc123.webp", "created_at": 1781883365, "updated_at": 1793219029558.3499, "art_style": null, "is_remix": true, "base_game_id": "01KV8C9GA3YZ2CSC8616HKNCXW"}, "task_plan": null}