{"asset_map": {"wooden_frame_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wooden_frame_texture_fec8a8c5-4f23-40d5-8210-cc15f8482aa2.webp", "type": "image", "prompt": "Generate a high-quality, seamless wood grain texture for a puzzle frame. Base color dark brown (#3E2723) with natural grain patterns and subtle highlights in lighter brown (#5D4037). Fine horizontal grain lines, polished and clean appearance, realistic but stylized to fit a digital game. Fully tileable in both directions with no visible seams.", "description": "A seamless dark brown wood grain texture for the decorative puzzle frame.", "subtype": "other", "aspect_ratio": [1024, 1024], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "victory_confetti_particle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/victory_confetti_particle_66c59755-35ce-4538-81af-cfc5f43a2973.webp", "type": "image", "prompt": "Generate a collection of simple, colorful confetti particles on a transparent background. Small irregular polygon shapes like torn paper pieces with slightly rounded edges. Bright saturated colors including gold, blue, red, and green. Flat modern style with a very subtle top-to-bottom gradient. Clean edges, professional game-ready visual effect.", "description": "A colorful confetti particle used for the level completion celebration effect.", "subtype": "effect", "aspect_ratio": [881, 1246], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "snap_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/snap_sound_4fe145db-8c09-4fa6-858d-d9db4183bf88.mp3", "type": "audio", "prompt": "A short, satisfying 'snap' or 'click' sound effect of two polished wooden blocks meeting. High-quality tactile percussion, crisp attack, very subtle natural wood resonance, clean and non-harsh. 0.2 seconds duration, suitable for a tactile puzzle game interface.", "description": "A crisp, wooden click sound effect for when puzzle pieces snap into place.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0.41, "loop": false, "animation": null}, "victory_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/victory_sound_89e7cedb-cbe9-4523-bd45-3a5bb4e31868.mp3", "type": "audio", "prompt": "A short, celebratory victory fanfare, 2 seconds long. Features a bright ascending xylophone and glockenspiel melody, ending with a clear, resonant chime. Warm, magical, and rewarding tone. No heavy drums, clear high-end shimmer, classic puzzle game success sound.", "description": "A cheerful and uplifting musical chime to celebrate level completion.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.18, "loop": false, "animation": null}, "piece_pickup_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/piece_pickup_sound_66e277d4-1a2b-49af-9811-76026e66cb7e.mp3", "type": "audio", "prompt": "A very brief, quiet wooden 'tap' or 'pop' sound effect. High-quality recording of a light finger tap on a hollow wooden surface. Soft attack, medium-high pitch, unobtrusive and gentle for frequent UI feedback.", "description": "A subtle, soft wooden tap sound for selecting a puzzle piece.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0.73, "loop": false, "animation": null}, "rotation_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/rotation_sound_74131014-b07f-4c2a-88df-72f953f13291.mp3", "type": "audio", "prompt": "A quick, smooth wooden movement sound effect. A blend of a light 'whoosh' and a subtle wooden scrape on a flat surface. Represents a 45-degree rotation. Mechanical yet organic, short duration, medium pitch, clean and satisfying.", "description": "A quick wooden sliding sound for rotating pieces in 45-degree increments.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.48, "loop": false, "animation": null}, "tangram_fish": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777705040483_01KQKQMEK3FTH9RTY25TY14RTS.TangramPuzzleFish(1).jpg", "type": "image", "prompt": null, "description": "Fish-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a fish silhouette.", "subtype": null, "aspect_ratio": [390, 390], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tangram_cat": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777706250083_01KQKRSBV3BJC3P03FJXS9KSYB.TangramPuzzleCat(1).png", "type": "image", "prompt": null, "description": "A cat-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a sitting cat silhouette.", "subtype": null, "aspect_ratio": [420, 420], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tangram_boat": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777706346576_01KQKRWA2GRZWVJEXFFW700KCZ.TangramPuzzleBoat(1).jpg", "type": "image", "prompt": null, "description": "A boat-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a sailboat silhouette.", "subtype": null, "aspect_ratio": [444, 444], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_flower": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777706439845_01KQKRZ553MBQWEPXYKDP00H1R.TangramPuzzleFlower(1).jpg", "type": "image", "prompt": null, "description": "A flower-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a flower silhouette.", "subtype": null, "aspect_ratio": [440, 440], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_swan": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777706561571_01KQKS2W13Y8QKYC3QFGJ4FZ4N.TangramPuzzleSwan(1).webp", "type": "image", "prompt": null, "description": "A swan-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a swan silhouette.", "subtype": null, "aspect_ratio": [454, 454], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tangram_house": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777707820098_01KQKT9922XHQGJZW2SAE6CCTX.64637_house2_oa_lg.gif", "type": "image", "prompt": null, "description": "A house-shaped tangram puzzle guide showing the internal divisions of the seven geometric pieces arranged to form a house silhouette.", "subtype": null, "aspect_ratio": [1024, 907], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tangram_rabbit": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777707925368_01KQKTCFVR6KMA3X8BD6HNZ5S7.64657_rabbit1_oa_lg.gif", "type": "image", "prompt": null, "description": "A rabbit-shaped tangram puzzle guide showing the internal geometry of seven pieces arranged to form a rabbit silhouette.", "subtype": null, "aspect_ratio": [488, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_runner": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777708226196_01KQKTNNMMWSV48327HHAB6BJK.easy-tangrams-puzzle-7-shapes-2023-01-25(1).png", "type": "image", "prompt": null, "description": "A tangram puzzle guide of a running person showing the 7 geometric pieces arranged to form a runner silhouette.", "subtype": null, "aspect_ratio": [301, 315], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_candle": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777708297364_01KQKTQV4MATE35SNA60GS9N28.64606_candle_oa_lg.gif", "type": "image", "prompt": null, "description": "A candle-shaped tangram puzzle guide showing the internal geometry of seven pieces arranged to form a candle silhouette.", "subtype": null, "aspect_ratio": [412, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tangram_horse": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777708338528_01KQKTS3B02ZMFZHB17FBG2819.Tangram_Horse(1).png", "type": "image", "prompt": null, "description": "A horse-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a horse silhouette.", "subtype": null, "aspect_ratio": [272, 272], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_dog": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777719661914_01KQM5JNATZRXSGERAFNW100N8.64614_dog_oa_sm.gif", "type": "image", "prompt": null, "description": "A dog-shaped tangram puzzle guide showing the internal geometry of seven pieces arranged to form a dog silhouette.", "subtype": null, "aspect_ratio": [320, 224], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tangram_bat": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777719834820_01KQM5QY64AF4BEBQHH85XVKTZ.Bat-tangram(1).jpg", "type": "image", "prompt": null, "description": "A bat-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a bat silhouette with spread wings.", "subtype": null, "aspect_ratio": [224, 121], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_rocket": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777720014500_01KQM5XDN4H2NZ8DP1ZNHG8SV0.Gemini_Generated_Image_cvi6licvi6licvi6.png", "type": "image", "prompt": null, "description": "A rocket or house-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a rocket/house silhouette.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "cart": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/tangram_plane_d0fdeead-26e7-45f3-99da-acfc04e8e425.webp", "type": "image", "prompt": "Transform the input image by removing the checkered pixelated background and replacing it with a solid, plain white background. Keep the black outlines and internal divisions of the airplane tangram puzzle exactly as they appear in image 1. Maintain identical line weight and geometric precision for the seven tangram pieces forming the plane silhouette.", "description": "A clean version of the airplane tangram guide with a solid white background instead of checkered pixels.", "subtype": "other", "aspect_ratio": [1088, 960], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tangram_rooster": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777723302038_01KQM91R4PDWYN3K1MXAXWJ4AX.f28475d2-a662-4715-8626-10f2571f9792(1).png", "type": "image", "prompt": null, "description": "A rooster-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a rooster silhouette.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_bridge": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777723517226_01KQM98A9A0RK4FQ5B9KYWVFER.Gemini_Generated_Image_qk8fqmqk8fqmqk8f.png", "type": "image", "prompt": null, "description": "A bridge-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a bridge or table silhouette.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "pig": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777724132281_01KQM9V2XRYVASEB2DNTEWX6MA.Gemini_Generated_Image_pkgawypkgawypkga.png", "type": "image", "prompt": null, "description": "A tangram puzzle guide showing the outline of seven geometric pieces arranged to form an airplane silhouette.", "subtype": null, "aspect_ratio": [1187, 896], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "cow": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777723764309_01KQM9FVJN3S52TGF6NXYVR3GW.Gemini_Generated_Image_qyqi73qyqi73qyqi.png", "type": "image", "prompt": null, "description": "A dog-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a sitting dog silhouette.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "chair": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777723686986_01KQM9DG2AEH1KKJM003T48AXV.Gemini_Generated_Image_j1ojwkj1ojwkj1oj.png", "type": "image", "prompt": null, "description": "A boot or shoe-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a boot silhouette.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "bird": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777724530180_01KQMA77G4S4SYD0X3ME088NEB.Gemini_Generated_Image_ay6rbyay6rbyay6r.png", "type": "image", "prompt": null, "description": "A duck-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a duck silhouette.", "subtype": null, "aspect_ratio": [1235, 864], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "submit_button": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/submit_button_bca6caf6-df8e-488e-ab36-7b5dd3157532.webp", "type": "image", "prompt": "Generate a polished 3D-styled UI button with the word 'Submit' centered in a bold white font. The button should have a vibrant green color scheme (#4CAF50), featuring beveled edges, high-gloss highlights, and realistic depth/shading. Match the exact shape, 3D perspective, and lighting style of the reset_button. The background of the button itself is solid green, but the asset should be on a transparent background for game integration.", "description": "A polished green 3D-styled 'Submit' button designed to match the 'Reset' button style.", "subtype": "ui", "aspect_ratio": [1229, 566], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "reset_button": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/reset_button_c7478254-718f-4bd6-a4a5-5a53219b33c3.webp", "type": "image", "prompt": "Modify the existing red/orange 3D-style button to include the word 'Reset' written in the center. The text must be bold, white, and clean, using a modern sans-serif font. Ensure the text is perfectly centered and has a subtle drop shadow to enhance the 3D effect. The font size should be approximately 45% of the button's height. Match the polished, glossy visual style and perspective of the green submit button in image 2, while maintaining the red color from image 1.", "description": "A polished red/orange 3D-styled button with the text 'Reset' centered in a bold white font.", "subtype": "ui", "aspect_ratio": [1248, 571], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "collection_button": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/collection_button_5b94185c-f14b-4268-96ea-bf9fa9788f8d.webp", "type": "image", "prompt": "Generate a sleek square UI button with a polished 3D effect, featuring a clean book icon in the center. The button's background, lighting, and material should perfectly match the style of image 1. High resolution, professional game UI asset, transparent background.", "description": "A sleek square UI button with a book icon, matching the 3D polished style of the leaderboard button.", "subtype": "ui", "aspect_ratio": [1024, 949], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "leaderboard_button": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/leaderboard_button_ab2124d6-671e-4734-8264-f3410f94f0ab.webp", "type": "image", "prompt": "Generate a polished 3D-styled UI button with a purple/violet color scheme, matching the style and beveling of collection_button. The button should be square-shaped and feature a bright gold/yellow trophy icon \ud83c\udfc5 centered on its face. Include professional lighting with subtle highlights and shadows to enhance the 3D effect. The design should be clean and game-ready on a transparent background.", "description": "A square leaderboard button with a gold trophy icon, styled to match the collection_button.", "subtype": "ui", "aspect_ratio": [1024, 950], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "wood_background": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/wood_background_f7879f67-8d58-4453-a688-f40d915d18de.webp", "type": "image", "prompt": "Generate a 9:16 tall image of a warm, polished wooden game board background. The texture should feature medium brown oak or walnut wood grain running vertically. The design includes a prominent 3D beveled wooden frame around the edges, creating a raised panel effect with a recessed center area. Lighting is coming from the top-left, casting subtle highlights on the top and left bevels and soft shadows on the bottom and right. The look is clean, professional, and high-resolution, suitable for a mobile tangram puzzle game.", "description": "A polished, warm wood game board background with a 3D beveled frame for a mobile puzzle game.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "tangram_master_title": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/tangram_master_title_81508338-9677-498d-8c76-4b12b1675544.webp", "type": "image", "prompt": "Generate a high-fidelity 3D game title logo reading 'TANGRAM MASTER'. Use a bold geometric sans-serif font with sharp 45-degree beveled edges. The front faces of the letters are polished reflective gold-brass. The 3D extrusion sides feature a detailed dark walnut wood grain texture. Position with a slight 3D perspective tilt. Cinematic lighting from top-left creating bright specular highlights on metallic edges and soft rim lighting. Realistic soft drop-shadow. The style is premium, tactile, and sophisticated. Transparent background.", "description": "High-fidelity 3D game logo 'TANGRAM MASTER' with gold-brass faces and walnut wood sides.", "subtype": "ui", "aspect_ratio": [982, 345], "compression_suggestion": "low", "duration_secs": null, "loop": null, "animation": null}, "premium_board_background": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/premium_board_background_39d8be8b-2ccb-43c4-a507-a06804b2b199.webp", "type": "image", "prompt": "Generate a high-resolution 9:16 portrait orientation illustration of a premium game board. The background is a rich, dark hardwood panel with a polished grain and beveled edges. In the top section, a 'template of all possibilities' consisting of interlocking blank geometric outlines (triangles, squares, rhombuses) with a soft golden glow. In the bottom section, seven 3D tangram pieces arranged into a sculpture: a large gold/brass triangle, a large green triangle, a small blue triangle, a medium purple triangle, a small red triangle, an orange rhombus, and a walnut square. Each piece has realistic beveled edges, reflective surfaces, and casts subtle shadows on the wood. Cinematic lighting from the top-left, warm museum-style ambiance, high-fidelity materials, no text or UI elements. Match the aesthetic of image 1.", "description": "A high-resolution premium wooden game board background with a cinematic lighting and a sculptural tangram arrangement.", "subtype": "background", "aspect_ratio": [832, 1248], "compression_suggestion": "original", "duration_secs": null, "loop": null, "animation": null}, "play_button": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/play_button_b54087c2-976f-414e-966f-6df149597b33.webp", "type": "image", "prompt": "Generate a UI game asset: a high-quality 3D polished wooden 'PLAY' button. The button is a rounded rectangle made of rich dark walnut wood grain. It features bold white '\u25b6 PLAY' text with a subtle engraved effect. Lighting is coming from the top-left, creating bright specular highlights on the upper beveled edges and deep shadows on the bottom edges for a tactile 3D look. Match the aesthetic of 'premium_board_background'. Transparent background, high resolution, professional game UI design.", "description": "A polished, premium walnut 3D play button with beveled edges and white text.", "subtype": "ui", "aspect_ratio": [716, 359], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tangram_stallion": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777770315877_01KQNNWG359Z9C2BMMM3T1P1FJ.Gemini_Generated_Image_h66eu2h66eu2h66e.png", "type": "image", "prompt": null, "description": "A stallion/horse-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a running horse silhouette.", "subtype": null, "aspect_ratio": [1264, 843], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_sitting_cat": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777770512687_01KQNP2G9FE2P5M4E95M8HN9CB.Gemini_Generated_Image_s3m0tbs3m0tbs3m0.png", "type": "image", "prompt": null, "description": "A sitting cat-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a cat silhouette with pointed ears.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "animation": null}, "tangram_airplane": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777770913836_01KQNPER1C0VGA2K18VZACCDT2.Gemini_Generated_Image_qn1k0tqn1k0tqn1k.png", "type": "image", "prompt": null, "description": "A tangram puzzle guide showing the outline of seven geometric pieces arranged to form an airplane silhouette.", "subtype": null, "aspect_ratio": [1588, 656], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_arrow": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777771322046_01KQNPV6NY6EEY5MFMNSP0GTWG.Gemini_Generated_Image_be5crebe5crebe5c.png", "type": "image", "prompt": null, "description": "An arrow-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form an arrow pointing right.", "subtype": null, "aspect_ratio": [1264, 843], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_question_mark": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777771853274_01KQNQBDETGG2WZXYBZPZJWTS0.Gemini_Generated_Image_bpay1pbpay1pbpay.png", "type": "image", "prompt": null, "description": "A question mark-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a question mark silhouette.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}, "tangram_person": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/Google_112794858149694705343/1777772075167_01KQNQJ64ZB4NHMD1V1MSAZ6JB.Gemini_Generated_Image_m9mcgdm9mcgdm9mc.png", "type": "image", "prompt": null, "description": "A person/figure-shaped tangram puzzle guide showing the outline of seven geometric pieces arranged to form a standing person silhouette.", "subtype": null, "aspect_ratio": [1024, 1024], "compression_suggestion": "high", "duration_secs": null, "loop": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Tangram Master</title>\n    <link href=\"https://fonts.googleapis.com/css2?family=Fredoka:wght@500;600;700&family=Poppins:wght@400;600;700&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            --space-sm: 16px;\n            --space-md: 24px;\n            --bg-color: #E8E8E8;\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: 'Poppins', sans-serif;\n        }\n\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n            background-color: var(--bg-color);\n        }\n\n        #game-canvas {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            cursor: default;\n            will-change: transform;\n        }\n        \n        #game-canvas.can-grab {\n            cursor: grab;\n        }\n        \n        #game-canvas.grabbing {\n            cursor: grabbing;\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        .title-section {\n            position: absolute;\n            top: 8px;\n            left: 0;\n            width: 100%;\n            text-align: center;\n            pointer-events: none;\n        }\n\n        .left-info-panel {\n            position: absolute;\n            top: 120px;\n            left: 70px;\n            display: flex;\n            flex-direction: column;\n            gap: 10px;\n            align-items: flex-start;\n            pointer-events: none;\n        }\n        \n        .left-info-panel > * {\n            pointer-events: auto;\n        }\n\n        .right-info-panel {\n            position: absolute;\n            top: 130px;\n            right: 80px;\n            display: flex;\n            flex-direction: column;\n            gap: 10px;\n            align-items: flex-end;\n            pointer-events: none;\n        }\n        \n        .right-info-panel > * {\n            pointer-events: auto;\n        }\n\n        .game-title {\n            font-family: 'Fredoka', 'Poppins', sans-serif;\n            font-size: 52px;\n            font-weight: 700;\n            color: #3E2723;\n            margin: 0;\n            text-shadow: 2px 2px 0px rgba(255,255,255,0.5), 3px 3px 6px rgba(0,0,0,0.15);\n            letter-spacing: -1px;\n            display: none; /* Hidden - using title logo instead */\n        }\n\n        .level-indicator {\n            font-family: 'Fredoka', 'Poppins', sans-serif;\n            font-size: 18px;\n            font-weight: 600;\n            color: #3E2723;\n            margin: 0;\n            background: linear-gradient(180deg, #F5E6D3 0%, #D4C4B0 100%);\n            padding: 12px 16px;\n            border-radius: 12px;\n            display: inline-block;\n            border: 3px solid #5D4037;\n            box-shadow: \n                0 4px 8px rgba(0,0,0,0.25),\n                inset 0 2px 0 rgba(255,255,255,0.5),\n                inset 0 -2px 0 rgba(0,0,0,0.1);\n            text-shadow: 1px 1px 0 rgba(255,255,255,0.5);\n        }\n\n        .timer-display {\n            font-family: 'Fredoka', 'Poppins', sans-serif;\n            font-size: 24px;\n            font-weight: 700;\n            color: #3E2723;\n            margin: 0;\n            background: linear-gradient(180deg, #FFF8E1 0%, #FFECB3 100%);\n            padding: 12px 18px;\n            border-radius: 12px;\n            display: inline-block;\n            border: 3px solid #8D6E63;\n            box-shadow: \n                0 4px 8px rgba(0,0,0,0.25),\n                inset 0 2px 0 rgba(255,255,255,0.6),\n                inset 0 -2px 0 rgba(0,0,0,0.1);\n            letter-spacing: 1px;\n            text-shadow: 1px 1px 0 rgba(255,255,255,0.5);\n        }\n\n        .guide-panel {\n            position: absolute;\n            top: 120px;\n            left: 50%;\n            transform: translateX(-50%);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            background: linear-gradient(180deg, #FAFAFA 0%, #E8E0D5 100%);\n            border-radius: 14px;\n            padding: 10px;\n            border: 4px solid #5D4037;\n            box-shadow: \n                0 6px 12px rgba(0,0,0,0.3),\n                inset 0 2px 0 rgba(255,255,255,0.7),\n                inset 0 -3px 0 rgba(0,0,0,0.15);\n            pointer-events: auto;\n        }\n\n        .guide-panel::before {\n            content: '';\n            position: absolute;\n            top: 4px;\n            left: 4px;\n            right: 4px;\n            bottom: 4px;\n            border-radius: 10px;\n            border: 2px solid rgba(255,255,255,0.4);\n            pointer-events: none;\n        }\n\n        .guide-panel img {\n            width: 110px;\n            height: 110px;\n            object-fit: contain;\n            border-radius: 8px;\n            background: #FFFFFF;\n            padding: 6px;\n            box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);\n        }\n\n        .guide-label {\n            font-family: 'Fredoka', 'Poppins', sans-serif;\n            font-size: 13px;\n            font-weight: 600;\n            color: #5D4037;\n            margin-top: 6px;\n            text-transform: uppercase;\n            letter-spacing: 1px;\n            text-shadow: 1px 1px 0 rgba(255,255,255,0.5);\n        }\n\n        .control-buttons {\n            position: absolute;\n            display: none;\n            flex-direction: row;\n            gap: 12px;\n            z-index: 100;\n            will-change: transform, opacity;\n        }\n\n        .control-btn {\n            width: 96px;\n            height: 96px;\n            border-radius: 20px;\n            background: linear-gradient(180deg, #FFFFFF 0%, #F0F0F0 100%);\n            border: 2px solid #424242;\n            font-size: 36px;\n            color: #424242;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: transform var(--transition-fast) var(--ease-out),\n                        background var(--transition-fast) var(--ease-out),\n                        box-shadow var(--transition-fast) var(--ease-out);\n            box-shadow: 0 6px 12px rgba(0,0,0,0.2), 0 2px 4px rgba(0,0,0,0.1);\n            -webkit-tap-highlight-color: transparent;\n        }\n\n        .control-btn:active {\n            transform: scale(0.92);\n            background: linear-gradient(180deg, #E8E8E8 0%, #D8D8D8 100%);\n            box-shadow: 0 2px 4px rgba(0,0,0,0.15);\n        }\n\n        .control-btn:focus-visible {\n            outline: 3px solid #4169E1;\n            outline-offset: 2px;\n        }\n\n        .victory-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.5);\n            display: none;\n            align-items: center;\n            justify-content: center;\n            flex-direction: column;\n            z-index: 200;\n        }\n\n        .victory-text {\n            font-family: 'Fredoka', 'Poppins', sans-serif;\n            font-size: 44px;\n            font-weight: 700;\n            color: #FFD700;\n            text-shadow: 0 0 20px rgba(255,215,0,0.6), 2px 2px 8px rgba(0,0,0,0.5);\n            animation: victoryPulse 1s ease-in-out infinite;\n            text-align: center;\n            padding: 0 24px;\n        }\n\n        .play-again-btn {\n            margin-top: 40px;\n            padding: 20px 56px;\n            font-size: 26px;\n            font-weight: 600;\n            font-family: 'Poppins', sans-serif;\n            background: linear-gradient(180deg, #66BB6A 0%, #4CAF50 100%);\n            color: white;\n            border: none;\n            border-radius: 16px;\n            cursor: pointer;\n            box-shadow: 0 6px 16px rgba(76,175,80,0.4), 0 2px 4px rgba(0,0,0,0.2);\n            transition: transform var(--transition-fast) var(--ease-out),\n                        box-shadow var(--transition-fast) var(--ease-out);\n            -webkit-tap-highlight-color: transparent;\n            min-height: 72px;\n            min-width: 200px;\n        }\n\n        .play-again-btn:active {\n            transform: scale(0.95);\n            box-shadow: 0 3px 8px rgba(76,175,80,0.3), 0 1px 2px rgba(0,0,0,0.15);\n        }\n\n        .play-again-btn:focus-visible {\n            outline: 3px solid #FFD700;\n            outline-offset: 2px;\n        }\n\n        @keyframes victoryPulse {\n            0%, 100% { transform: scale(1); }\n            50% { transform: scale(1.05); }\n        }\n\n        .settings-btn {\n            position: absolute;\n            top: 20px;\n            right: 20px;\n            width: 56px;\n            height: 56px;\n            border-radius: 50%;\n            background: rgba(255, 255, 255, 0.9);\n            border: 2px solid #424242;\n            font-size: 28px;\n            cursor: pointer;\n            display: none;\n            align-items: center;\n            justify-content: center;\n            z-index: 50;\n        }\n\n        .reset-btn {\n            position: absolute;\n            bottom: 20px;\n            left: 20px;\n            width: 180px;\n            height: 80px;\n            border: none;\n            background: none;\n            cursor: pointer;\n            z-index: 50;\n            background-size: 100% 100%;\n            background-repeat: no-repeat;\n            background-position: center;\n            transition: transform var(--transition-fast) var(--ease-out),\n                        filter var(--transition-fast) var(--ease-out);\n            -webkit-tap-highlight-color: transparent;\n            font-size: 0;\n        }\n\n        .reset-btn:active {\n            transform: scale(0.95);\n            filter: brightness(0.9);\n        }\n\n        .reset-btn:focus-visible {\n            outline: 3px solid #4169E1;\n            outline-offset: 2px;\n        }\n\n        .submit-btn {\n            position: absolute;\n            bottom: 20px;\n            right: 20px;\n            width: 180px;\n            height: 80px;\n            border: none;\n            background: none;\n            cursor: pointer;\n            z-index: 50;\n            background-size: 100% 100%;\n            background-repeat: no-repeat;\n            background-position: center;\n            transition: transform var(--transition-fast) var(--ease-out),\n                        filter var(--transition-fast) var(--ease-out);\n            -webkit-tap-highlight-color: transparent;\n            font-size: 0;\n        }\n\n        .submit-btn:active {\n            transform: scale(0.95);\n            filter: brightness(0.9);\n        }\n\n        .submit-btn:focus-visible {\n            outline: 3px solid #FFD700;\n            outline-offset: 2px;\n        }\n\n        .feedback-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.6);\n            display: none;\n            align-items: center;\n            justify-content: center;\n            flex-direction: column;\n            z-index: 190;\n        }\n\n        .feedback-card {\n            background: white;\n            border-radius: 24px;\n            padding: 32px 48px;\n            text-align: center;\n            box-shadow: 0 10px 40px rgba(0,0,0,0.3);\n            max-width: 85%;\n        }\n\n        .feedback-score {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 64px;\n            font-weight: 700;\n            margin: 16px 0;\n        }\n\n        .feedback-score.perfect {\n            color: #4CAF50;\n        }\n\n        .feedback-score.good {\n            color: #FF9800;\n        }\n\n        .feedback-score.needs-work {\n            color: #F44336;\n        }\n\n        .feedback-message {\n            font-family: 'Poppins', sans-serif;\n            font-size: 22px;\n            color: #424242;\n            margin-bottom: 24px;\n        }\n\n        .feedback-close-btn {\n            padding: 14px 40px;\n            font-size: 18px;\n            font-weight: 600;\n            font-family: 'Poppins', sans-serif;\n            background: linear-gradient(180deg, #FFFFFF 0%, #F5F5F5 100%);\n            border: 2px solid #9E9E9E;\n            border-radius: 12px;\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-out);\n        }\n\n        .feedback-close-btn:active {\n            transform: scale(0.95);\n        }\n\n        .level-selector {\n            position: absolute;\n            top: 140px;\n            left: 5%;\n            right: 5%;\n            display: none;\n            gap: 8px;\n            z-index: 50;\n            overflow-x: auto;\n            overflow-y: hidden;\n            padding: 8px 12px;\n            -webkit-overflow-scrolling: touch;\n            scrollbar-width: none;\n            scroll-behavior: smooth;\n            white-space: nowrap;\n            touch-action: pan-x;\n            pointer-events: auto;\n        }\n        .level-selector::-webkit-scrollbar {\n            display: none;\n        }\n\n        .level-btn {\n            width: 52px;\n            height: 52px;\n            min-width: 52px;\n            border-radius: 14px;\n            background: linear-gradient(180deg, #FFFFFF 0%, #F0F0F0 100%);\n            border: 3px solid #9E9E9E;\n            font-size: 20px;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: all var(--transition-fast) var(--ease-out);\n            box-shadow: 0 4px 8px rgba(0,0,0,0.15);\n            -webkit-tap-highlight-color: transparent;\n        }\n\n        .level-btn.active {\n            border-color: #4CAF50;\n            background: linear-gradient(180deg, #E8F5E9 0%, #C8E6C9 100%);\n            box-shadow: 0 4px 12px rgba(76,175,80,0.3);\n        }\n\n        .level-btn.locked {\n            opacity: 0.5;\n            cursor: not-allowed;\n            background: linear-gradient(180deg, #E0E0E0 0%, #BDBDBD 100%);\n        }\n\n        .level-btn.completed {\n            border-color: #FFD700;\n            background: linear-gradient(180deg, #FFF8E1 0%, #FFECB3 100%);\n        }\n\n        .level-btn:not(.locked):active {\n            transform: scale(0.92);\n        }\n\n        .level-btn .check-icon {\n            position: absolute;\n            top: -4px;\n            right: -4px;\n            width: 18px;\n            height: 18px;\n            background: #4CAF50;\n            border-radius: 50%;\n            font-size: 11px;\n            color: white;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n        \n        .level-btn .lock-icon {\n            font-size: 16px;\n            color: #757575;\n        }\n\n        .collection-btn {\n            position: relative;\n            width: 64px;\n            height: 64px;\n            border: none;\n            background: none;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            z-index: 50;\n            background-size: contain;\n            background-repeat: no-repeat;\n            background-position: center;\n            transition: transform var(--transition-fast) var(--ease-out),\n                        filter var(--transition-fast) var(--ease-out);\n            -webkit-tap-highlight-color: transparent;\n            font-size: 0;\n        }\n\n        .collection-btn:active {\n            transform: scale(0.95);\n            filter: brightness(0.9);\n        }\n\n        .collection-btn:focus-visible {\n            outline: 3px solid #4169E1;\n            outline-offset: 2px;\n        }\n\n        .leaderboard-btn {\n            position: relative;\n            width: 64px;\n            height: 64px;\n            border: none;\n            background: none;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            z-index: 50;\n            background-size: contain;\n            background-repeat: no-repeat;\n            background-position: center;\n            transition: transform var(--transition-fast) var(--ease-out),\n                        filter var(--transition-fast) var(--ease-out);\n            -webkit-tap-highlight-color: transparent;\n            font-size: 0;\n        }\n\n        .leaderboard-btn:active {\n            transform: scale(0.95);\n            filter: brightness(0.9);\n        }\n        \n        .leaderboard-btn:focus-visible {\n            outline: 3px solid #4169E1;\n            outline-offset: 2px;\n        }\n\n        .collection-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.85);\n            display: none;\n            flex-direction: column;\n            z-index: 300;\n            overflow-y: auto;\n            touch-action: pan-y;\n            pointer-events: auto;\n        }\n\n        .collection-header {\n            padding: 24px;\n            text-align: center;\n            position: sticky;\n            top: 0;\n            background: linear-gradient(180deg, rgba(62, 39, 35, 0.98) 0%, rgba(62, 39, 35, 0.9) 100%);\n            z-index: 10;\n        }\n\n        .collection-title {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 36px;\n            font-weight: 700;\n            color: #FFD700;\n            margin: 0;\n            text-shadow: 2px 2px 4px rgba(0,0,0,0.3);\n        }\n\n        .collection-subtitle {\n            font-family: 'Poppins', sans-serif;\n            font-size: 16px;\n            color: #BCAAA4;\n            margin: 8px 0 0 0;\n        }\n        .collection-close-btn {\n            position: absolute;\n            top: 20px;\n            right: 20px;\n            width: 48px;\n            height: 48px;\n            border-radius: 50%;\n            background: rgba(255,255,255,0.1);\n            border: 2px solid #BCAAA4;\n            font-size: 24px;\n            color: #BCAAA4;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: all var(--transition-fast) var(--ease-out);\n        }\n\n        .collection-close-btn:hover,\n        .collection-close-btn:active {\n            background: rgba(255,255,255,0.2);\n            color: white;\n            border-color: white;\n        }\n\n        .collection-grid {\n            display: grid;\n            grid-template-columns: repeat(2, 1fr);\n            gap: 20px;\n            padding: 20px 24px 40px;\n        }\n\n        .collection-item {\n            background: linear-gradient(180deg, #FFFFFF 0%, #F5F5F5 100%);\n            border-radius: 16px;\n            overflow: hidden;\n            box-shadow: 0 6px 20px rgba(0,0,0,0.3);\n            transition: transform var(--transition-fast) var(--ease-out);\n        }\n\n        .collection-item.locked {\n            opacity: 0.5;\n            filter: grayscale(100%);\n        }\n\n        .collection-item:not(.locked):active {\n            transform: scale(0.96);\n        }\n\n        .collection-item-image {\n            width: 100%;\n            aspect-ratio: 1;\n            background: #FAFAFA;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            position: relative;\n            overflow: hidden;\n        }\n\n        .collection-item-image img {\n            max-width: 85%;\n            max-height: 85%;\n            object-fit: contain;\n        }\n\n        .collection-item.locked .collection-item-image::after {\n            content: '\ud83d\udd12';\n            position: absolute;\n            font-size: 48px;\n            opacity: 0.8;\n        }\n\n        .collection-item-info {\n            padding: 12px;\n            text-align: center;\n            background: linear-gradient(180deg, #5D4037 0%, #4E342E 100%);\n        }\n\n        .collection-item-name {\n            font-family: 'Poppins', sans-serif;\n            font-size: 14px;\n            font-weight: 600;\n            color: white;\n            margin: 0;\n        }\n\n        .collection-item-status {\n            font-size: 11px;\n            color: #BCAAA4;\n            margin: 4px 0 0 0;\n        }\n\n        .collection-item.completed .collection-item-status {\n            color: #81C784;\n        }\n\n        .collection-empty {\n            grid-column: 1 / -1;\n            text-align: center;\n            padding: 60px 20px;\n            color: #BCAAA4;\n            font-family: 'Poppins', sans-serif;\n        }\n\n        .collection-empty-icon {\n            font-size: 64px;\n            margin-bottom: 16px;\n        }\n\n        .collection-empty-text {\n            font-size: 18px;\n        }\n\n        .leaderboard-overlay {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.85);\n            display: none;\n            flex-direction: column;\n            z-index: 300;\n            overflow-y: auto;\n            touch-action: pan-y;\n            pointer-events: auto;\n        }\n\n        .leaderboard-header {\n            padding: 24px;\n            text-align: center;\n            position: sticky;\n            top: 0;\n            background: linear-gradient(180deg, rgba(62, 39, 35, 0.98) 0%, rgba(62, 39, 35, 0.9) 100%);\n            z-index: 10;\n        }\n\n        .leaderboard-title {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 36px;\n            font-weight: 700;\n            color: #FFD700;\n            margin: 0;\n            text-shadow: 2px 2px 4px rgba(0,0,0,0.3);\n        }\n\n        .leaderboard-subtitle {\n            font-family: 'Poppins', sans-serif;\n            font-size: 14px;\n            color: #BCAAA4;\n            margin: 8px 0 0 0;\n        }\n\n        .leaderboard-close-btn {\n            position: absolute;\n            top: 20px;\n            right: 20px;\n            width: 48px;\n            height: 48px;\n            border-radius: 50%;\n            background: rgba(255,255,255,0.1);\n            border: 2px solid #BCAAA4;\n            font-size: 24px;\n            color: #BCAAA4;\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            transition: all var(--transition-fast) var(--ease-out);\n        }\n\n        .leaderboard-close-btn:hover,\n        .leaderboard-close-btn:active {\n            background: rgba(255,255,255,0.2);\n            color: white;\n            border-color: white;\n        }\n\n        .leaderboard-list {\n            padding: 20px 24px 40px;\n            display: flex;\n            flex-direction: column;\n            gap: 12px;\n        }\n\n        .leaderboard-entry {\n            background: linear-gradient(180deg, #FFFFFF 0%, #F5F5F5 100%);\n            border-radius: 12px;\n            padding: 16px 20px;\n            display: flex;\n            align-items: center;\n            gap: 16px;\n            box-shadow: 0 4px 12px rgba(0,0,0,0.2);\n            transition: transform var(--transition-fast) var(--ease-out);\n        }\n\n        .leaderboard-entry.current-user {\n            background: linear-gradient(180deg, #FFF8E1 0%, #FFECB3 100%);\n            border: 2px solid #FFD700;\n            box-shadow: 0 6px 16px rgba(255,215,0,0.3);\n        }\n\n        .leaderboard-rank {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 24px;\n            font-weight: 700;\n            color: #5D4037;\n            min-width: 40px;\n            text-align: center;\n        }\n\n        .leaderboard-rank.top-1 {\n            color: #FFD700;\n        }\n\n        .leaderboard-rank.top-2 {\n            color: #C0C0C0;\n        }\n\n        .leaderboard-rank.top-3 {\n            color: #CD7F32;\n        }\n\n        .leaderboard-profile {\n            width: 48px;\n            height: 48px;\n            border-radius: 50%;\n            background: #E0E0E0;\n            overflow: hidden;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n        }\n\n        .leaderboard-profile img {\n            width: 100%;\n            height: 100%;\n            object-fit: cover;\n        }\n\n        .leaderboard-profile-placeholder {\n            font-size: 24px;\n        }\n\n        .leaderboard-info {\n            flex: 1;\n            min-width: 0;\n        }\n\n        .leaderboard-username {\n            font-family: 'Poppins', sans-serif;\n            font-size: 18px;\n            font-weight: 600;\n            color: #3E2723;\n            margin: 0;\n            overflow: hidden;\n            text-overflow: ellipsis;\n            white-space: nowrap;\n        }\n\n        .leaderboard-entry.current-user .leaderboard-username {\n            color: #F57F17;\n        }\n\n        .leaderboard-score {\n            font-family: 'Fredoka', sans-serif;\n            font-size: 22px;\n            font-weight: 700;\n            color: #4CAF50;\n            min-width: 80px;\n            text-align: right;\n        }\n\n        .leaderboard-loading {\n            padding: 60px 20px;\n            text-align: center;\n            color: #BCAAA4;\n            font-family: 'Poppins', sans-serif;\n            font-size: 18px;\n        }\n\n        .leaderboard-empty {\n            padding: 60px 20px;\n            text-align: center;\n            color: #BCAAA4;\n            font-family: 'Poppins', sans-serif;\n        }\n\n        .leaderboard-empty-icon {\n            font-size: 64px;\n            margin-bottom: 16px;\n        }\n\n        .leaderboard-empty-text {\n            font-size: 18px;\n        }\n\n        /* Title Screen Styles */\n        .title-screen {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 500;\n            background-color: #1a0f0a;\n        }\n\n        .title-screen.hidden {\n            display: none;\n        }\n\n        .title-screen-bg {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            object-fit: cover;\n            opacity: 0;\n            transition: opacity 0.8s ease-in;\n        }\n\n        .title-screen-bg.loaded {\n            opacity: 1;\n        }\n\n        .title-screen-content {\n            position: relative;\n            z-index: 1;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            gap: 60px;\n            padding: 40px;\n            opacity: 0;\n            transform: translateY(20px);\n            transition: opacity 0.6s ease-out 0.4s, transform 0.6s ease-out 0.4s;\n        }\n\n        .title-screen-content.visible {\n            opacity: 1;\n            transform: translateY(0);\n        }\n\n        .title-logo {\n            max-width: 90%;\n            width: 600px;\n            height: auto;\n            filter: \n                drop-shadow(0 0 20px rgba(255, 215, 0, 0.8))\n                drop-shadow(0 0 40px rgba(255, 215, 0, 0.6))\n                drop-shadow(0 0 60px rgba(255, 193, 7, 0.4))\n                drop-shadow(0 8px 24px rgba(0,0,0,0.6));\n            animation: titleFloat 3s ease-in-out infinite, titleGlow 2s ease-in-out infinite alternate;\n        }\n\n        @keyframes titleGlow {\n            0% {\n                filter: \n                    drop-shadow(0 0 20px rgba(255, 215, 0, 0.8))\n                    drop-shadow(0 0 40px rgba(255, 215, 0, 0.6))\n                    drop-shadow(0 0 60px rgba(255, 193, 7, 0.4))\n                    drop-shadow(0 8px 24px rgba(0,0,0,0.6));\n            }\n            100% {\n                filter: \n                    drop-shadow(0 0 30px rgba(255, 215, 0, 1))\n                    drop-shadow(0 0 50px rgba(255, 215, 0, 0.8))\n                    drop-shadow(0 0 80px rgba(255, 193, 7, 0.6))\n                    drop-shadow(0 8px 24px rgba(0,0,0,0.6));\n            }\n        }\n\n        @keyframes titleFloat {\n            0%, 100% { transform: translateY(0); }\n            50% { transform: translateY(-10px); }\n        }\n\n        .play-btn {\n            width: 400px;\n            height: 120px;\n            border: none;\n            background: none;\n            cursor: pointer;\n            background-size: contain;\n            background-repeat: no-repeat;\n            background-position: center;\n            transition: transform 0.15s ease-out, filter 0.15s ease-out;\n            -webkit-tap-highlight-color: transparent;\n            font-size: 0;\n        }\n\n        .play-btn:hover {\n            transform: scale(1.02);\n            filter: brightness(1.1);\n        }\n\n        .play-btn:active {\n            transform: scale(0.98);\n            filter: brightness(0.95);\n        }\n\n        .title-tagline {\n            font-family: 'Poppins', sans-serif;\n            font-size: 18px;\n            color: rgba(255, 248, 225, 0.9);\n            text-align: center;\n            max-width: 80%;\n            text-shadow: 1px 1px 4px rgba(0,0,0,0.5);\n            letter-spacing: 1px;\n        }\n\n        .title-loading {\n            font-family: 'Poppins', sans-serif;\n            font-size: 16px;\n            color: rgba(255, 248, 225, 0.7);\n            position: absolute;\n            bottom: 60px;\n        }\n\n        .title-loading.hidden {\n            display: none;\n        }\n\n        /* Loading Screen */\n        .loading-screen {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background-color: #000000;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            z-index: 600;\n            transition: opacity 0.5s ease-out;\n        }\n\n        .loading-screen.hidden {\n            opacity: 0;\n            pointer-events: none;\n        }\n\n        .loading-spinner {\n            width: 60px;\n            height: 60px;\n            border: 4px solid rgba(255, 255, 255, 0.2);\n            border-top-color: #FFD700;\n            border-radius: 50%;\n            animation: spin 1s linear infinite;\n        }\n\n        .loading-text {\n            font-family: 'Poppins', sans-serif;\n            font-size: 18px;\n            color: rgba(255, 255, 255, 0.8);\n            margin-top: 24px;\n            letter-spacing: 2px;\n        }\n\n        @keyframes spin {\n            to { transform: rotate(360deg); }\n        }\n    </style>\n</head>\n<body>\n    <div id=\"game-world\">\n        <!-- Loading Screen -->\n        <div class=\"loading-screen\" id=\"loading-screen\">\n            <div class=\"loading-spinner\"></div>\n            <p class=\"loading-text\">LOADING</p>\n        </div>\n\n        <!-- Title Screen -->\n        <div class=\"title-screen\" id=\"title-screen\">\n            <img class=\"title-screen-bg\" id=\"title-bg\" src=\"\" alt=\"\">\n            <div class=\"title-screen-content\" id=\"title-content\">\n                <img class=\"title-logo\" id=\"title-logo\" src=\"\" alt=\"Tangram Master\">\n                <p class=\"title-tagline\">Arrange the seven ancient tans to master each puzzle</p>\n                <button class=\"play-btn\" id=\"play-btn\">\u25b6 PLAY</button>\n            </div>\n            <p class=\"title-loading\" id=\"title-loading\">Loading...</p>\n        </div>\n        <canvas id=\"game-canvas\"></canvas>\n        <div class=\"ui-overlay\">\n            <div class=\"title-section\">\n                <h1 class=\"game-title\">Tangram Master</h1>\n                <div class=\"guide-panel\" id=\"guide-panel\">\n                    <img id=\"guide-image\" src=\"\" alt=\"Guide\">\n                    <span class=\"guide-label\">GUIDE</span>\n                </div>\n            </div>\n            <div class=\"left-info-panel\">\n                <button class=\"collection-btn\" id=\"collection-btn\" title=\"Collection\">My Collection</button>\n                <button class=\"leaderboard-btn\" id=\"leaderboard-btn\" title=\"Leaderboard\">\ud83c\udfc5</button>\n            </div>\n            <div class=\"right-info-panel\">\n                <p class=\"level-indicator\" id=\"level-indicator\">Level 1: The Square</p>\n                <p class=\"timer-display\" id=\"timer-display\">\u23f1\ufe0f 00:00</p>\n            </div>\n            <div class=\"leaderboard-overlay\" id=\"leaderboard-overlay\">\n                <div class=\"leaderboard-header\">\n                    <h2 class=\"leaderboard-title\">Leaderboard</h2>\n                    <p class=\"leaderboard-subtitle\">Fastest Average Times</p>\n                    <button class=\"leaderboard-close-btn\" id=\"leaderboard-close-btn\">\u2715</button>\n                </div>\n                <div class=\"leaderboard-list\" id=\"leaderboard-list\">\n                    <div class=\"leaderboard-loading\">Loading...</div>\n                </div>\n            </div>\n            <div class=\"collection-overlay\" id=\"collection-overlay\">\n                <div class=\"collection-header\">\n                    <h2 class=\"collection-title\">My Collection</h2>\n                    <p class=\"collection-subtitle\" id=\"collection-subtitle\">0 / 14 Completed</p>\n                    <button class=\"collection-close-btn\" id=\"collection-close-btn\">\u2715</button>\n                </div>\n                <div class=\"collection-grid\" id=\"collection-grid\"></div>\n            </div>\n            <div class=\"level-selector\" id=\"level-selector\"></div>\n            <div class=\"control-buttons\" id=\"control-buttons\">\n                <button class=\"control-btn\" id=\"rotate-left\">\u21b6</button>\n                <button class=\"control-btn\" id=\"rotate-right\">\u21b7</button>\n                <button class=\"control-btn\" id=\"flip-btn\" style=\"display: none;\">\u21c4</button>\n            </div>\n            <button class=\"submit-btn\" id=\"submit-btn\">Submit</button>\n            <button class=\"reset-btn\" id=\"reset-btn\">Reset</button>\n            <div class=\"feedback-overlay\" id=\"feedback-overlay\">\n                <div class=\"feedback-card\">\n                    <div class=\"feedback-message\" id=\"feedback-message\">Analyzing...</div>\n                    <div class=\"feedback-score\" id=\"feedback-score\">0%</div>\n                    <button class=\"feedback-close-btn\" id=\"feedback-close-btn\">Continue</button>\n                </div>\n            </div>\n            <div class=\"victory-overlay\" id=\"victory-overlay\">\n                <div class=\"victory-text\" id=\"victory-text\">LEVEL COMPLETED!</div>\n                <button class=\"play-again-btn\" id=\"play-again-btn\">Next Level</button>\n            </div>\n        </div>\n    </div>\n\n    <script>\n        /*\n         * TANGRAM MASTER\n         * A classic 2D Tangram puzzle game where players arrange seven geometric\n         * pieces (tans) to fit within a target outline. Features drag-and-drop\n         * controls, rotation, flip (for parallelogram), and snap-to-grid mechanics.\n         */\n\n        async function run(mode) {\n            const gameWorld = document.getElementById('game-world');\n            const canvas = document.getElementById('game-canvas');\n            const ctx = canvas.getContext('2d');\n            const controlButtons = document.getElementById('control-buttons');\n            const rotateLeftBtn = document.getElementById('rotate-left');\n            const rotateRightBtn = document.getElementById('rotate-right');\n            const flipBtn = document.getElementById('flip-btn');\n            const victoryOverlay = document.getElementById('victory-overlay');\n            const playAgainBtn = document.getElementById('play-again-btn');\n            const resetBtn = document.getElementById('reset-btn');\n            const submitBtn = document.getElementById('submit-btn');\n            const feedbackOverlay = document.getElementById('feedback-overlay');\n            const feedbackScore = document.getElementById('feedback-score');\n            const feedbackMessage = document.getElementById('feedback-message');\n            const feedbackCloseBtn = document.getElementById('feedback-close-btn');\n            const levelIndicator = document.getElementById('level-indicator');\n            const levelSelector = document.getElementById('level-selector');\n            const collectionBtn = document.getElementById('collection-btn');\n            const collectionOverlay = document.getElementById('collection-overlay');\n            const collectionCloseBtn = document.getElementById('collection-close-btn');\n            const collectionGrid = document.getElementById('collection-grid');\n            const collectionSubtitle = document.getElementById('collection-subtitle');\n            const timerDisplay = document.getElementById('timer-display');\n            const leaderboardBtn = document.getElementById('leaderboard-btn');\n            const leaderboardOverlay = document.getElementById('leaderboard-overlay');\n            const leaderboardCloseBtn = document.getElementById('leaderboard-close-btn');\n            const leaderboardList = document.getElementById('leaderboard-list');\n            const guidePanel = document.getElementById('guide-panel');\n            const guideImage = document.getElementById('guide-image');\n\n            // Canvas sizing\n            function resizeCanvas() {\n                canvas.width = gameWorld.clientWidth;\n                canvas.height = gameWorld.clientHeight;\n            }\n            resizeCanvas();\n            window.addEventListener('resize', resizeCanvas);\n\n            // Constants\n            const CANVAS_WIDTH = 720;\n            const CANVAS_HEIGHT = 1280;\n            const TARGET_SIZE = 440;\n            const TARGET_X = (CANVAS_WIDTH - TARGET_SIZE) / 2;\n            const TARGET_Y = 340;\n            const FRAME_THICKNESS = 16;\n            \n            // Unit calculation based on Tangram proportions\n            // Fixed at original size (based on 300px square) - independent of play area size\n            const ORIGINAL_SQUARE_SIZE = 300;\n            const UNIT = ORIGINAL_SQUARE_SIZE / (2 * Math.SQRT2);\n\n            // Level definitions with guide images\n            const LEVELS = [\n                { id: 1, name: 'The Fish', icon: '\ud83d\udc1f', guideAsset: 'tangram_fish' },\n                { id: 2, name: 'The Cat', icon: '\ud83d\udc31', guideAsset: 'tangram_cat' },\n                { id: 3, name: 'The Boat', icon: '\u26f5', guideAsset: 'tangram_boat' },\n                { id: 4, name: 'The Flower', icon: '\ud83c\udf38', guideAsset: 'tangram_flower' },\n                { id: 5, name: 'The Swan', icon: '\ud83e\udda2', guideAsset: 'tangram_swan' },\n                { id: 6, name: 'The House', icon: '\ud83c\udfe0', guideAsset: 'tangram_house' },\n                { id: 7, name: 'The Rabbit', icon: '\ud83d\udc30', guideAsset: 'tangram_rabbit' },\n                { id: 8, name: 'The Runner', icon: '\ud83c\udfc3', guideAsset: 'tangram_runner' },\n                { id: 9, name: 'The Candle', icon: '\ud83d\udd6f\ufe0f', guideAsset: 'tangram_candle' },\n                { id: 10, name: 'The Horse', icon: '\ud83d\udc34', guideAsset: 'tangram_horse' },\n                { id: 11, name: 'The Dog', icon: '\ud83d\udc15', guideAsset: 'tangram_dog' },\n                { id: 12, name: 'The Bat', icon: '\ud83e\udd87', guideAsset: 'tangram_bat' },\n                { id: 13, name: 'The Rocket', icon: '\ud83d\ude80', guideAsset: 'tangram_rocket' },\n                { id: 14, name: 'The Plane', icon: '\u2708\ufe0f', guideAsset: 'cart' },\n                { id: 15, name: 'The Rooster', icon: '\ud83d\udc13', guideAsset: 'tangram_rooster' },\n                { id: 16, name: 'The Bridge', icon: '\ud83c\udf09', guideAsset: 'tangram_bridge' },\n                { id: 17, name: 'The Boot', icon: '\ud83d\udc62', guideAsset: 'chair' },\n                { id: 18, name: 'The Cow', icon: '\ud83d\udc04', guideAsset: 'cow' },\n                { id: 19, name: 'The Pig', icon: '\ud83d\udc37', guideAsset: 'pig' },\n                { id: 20, name: 'The Duck', icon: '\ud83e\udd86', guideAsset: 'bird' },\n                { id: 21, name: 'The Stallion', icon: '\ud83d\udc0e', guideAsset: 'tangram_stallion' },\n                { id: 22, name: 'The Kitty', icon: '\ud83d\udc08', guideAsset: 'tangram_sitting_cat' },\n                { id: 23, name: 'The Jet', icon: '\u2708\ufe0f', guideAsset: 'tangram_airplane' },\n                { id: 24, name: 'The Arrow', icon: '\u27a1\ufe0f', guideAsset: 'tangram_arrow' },\n                { id: 25, name: 'The Question', icon: '\u2753', guideAsset: 'tangram_question_mark' },\n                { id: 26, name: 'The Figure', icon: '\ud83e\uddcd', guideAsset: 'tangram_person' }\n            ];\n            \n            // Piece type definitions with base vertices (centered at origin)\n            // The hypotenuse for triangles is typically along the diagonal y=x\n            const PIECE_TYPES = {\n                largeTriangle1: {\n                    color: '#2D5016',\n                    vertices: () => {\n                        const leg = UNIT * 2;\n                        return [\n                            { x: -leg/2, y: leg/2 },\n                            { x: leg/2, y: leg/2 },\n                            { x: -leg/2, y: -leg/2 }\n                        ];\n                    }\n                },\n                largeTriangle2: {\n                    color: '#F4C430',\n                    vertices: () => {\n                        const leg = UNIT * 2;\n                        return [\n                            { x: -leg/2, y: leg/2 },\n                            { x: leg/2, y: leg/2 },\n                            { x: -leg/2, y: -leg/2 }\n                        ];\n                    }\n                },\n                mediumTriangle: {\n                    color: '#6B4C9A',\n                    vertices: () => {\n                        const leg = UNIT * Math.SQRT2;\n                        return [\n                            { x: -leg/2, y: leg/2 },\n                            { x: leg/2, y: leg/2 },\n                            { x: -leg/2, y: -leg/2 }\n                        ];\n                    }\n                },\n                smallTriangle1: {\n                    color: '#DC143C',\n                    vertices: () => {\n                        const leg = UNIT;\n                        return [\n                            { x: -leg/2, y: leg/2 },\n                            { x: leg/2, y: leg/2 },\n                            { x: -leg/2, y: -leg/2 }\n                        ];\n                    }\n                },\n                smallTriangle2: {\n                    color: '#4169E1',\n                    vertices: () => {\n                        const leg = UNIT;\n                        return [\n                            { x: -leg/2, y: leg/2 },\n                            { x: leg/2, y: leg/2 },\n                            { x: -leg/2, y: -leg/2 }\n                        ];\n                    }\n                },\n                square: {\n                    color: '#8B4513',\n                    vertices: () => {\n                        const side = UNIT;\n                        return [\n                            { x: -side/2, y: -side/2 },\n                            { x: side/2, y: -side/2 },\n                            { x: side/2, y: side/2 },\n                            { x: -side/2, y: side/2 }\n                        ];\n                    }\n                },\n                parallelogram: {\n                    color: '#FF8C00',\n                    vertices: () => {\n                        const base = UNIT;\n                        const height = UNIT;\n                        const skew = height / 2;\n                        return [\n                            { x: -base/2 + skew, y: -height/2 },\n                            { x: base/2 + skew, y: -height/2 },\n                            { x: base/2 - skew, y: height/2 },\n                            { x: -base/2 - skew, y: height/2 }\n                        ];\n                    }\n                }\n            };\n\n            // Default config\n            const defaultConfig = {\n                pieceColors: {\n                    largeTriangle1: '#2D5016',\n                    largeTriangle2: '#F4C430',\n                    mediumTriangle: '#6B4C9A',\n                    smallTriangle1: '#DC143C',\n                    smallTriangle2: '#4169E1',\n                    square: '#8B4513',\n                    parallelogram: '#FF8C00'\n                },\n                backgroundColor: '#E8E8E8',\n                frameStyle: 'classic',\n                snapSensitivity: 25,\n                currentLevel: 1,\n                completedLevels: []\n            };\n\n            // Initialize gameConfig if needed\n            if (!window.gameConfig || Object.keys(window.gameConfig).length === 0) {\n                Object.assign(window.gameConfig, defaultConfig);\n            }\n            \n            // Ensure new config fields exist\n            if (!window.gameConfig.currentLevel) window.gameConfig.currentLevel = 1;\n            if (!window.gameConfig.completedLevels) window.gameConfig.completedLevels = [];\n\n            // Runtime state\n            let pieces = [];\n            let selectedPieceIndex = -1;\n            let isDragging = false;\n            let dragOffset = { x: 0, y: 0 };\n            let gameWon = false;\n            let hoveredPieceIndex = -1;\n            let lastTime = 0;\n            let confettiParticles = [];\n            let showGuide = true;\n            let currentLevelIndex = window.gameConfig.currentLevel - 1;\n            let completedLevels = [...window.gameConfig.completedLevels];\n            \n            // Performance optimization caches\n            let vertexCache = new Map();\n            let gradientCache = new Map();\n            let lastTimerUpdate = 0;\n            let frameCount = 0;\n            \n            // Timer state\n            let timerStartTime = 0;\n            let timerElapsed = 0;\n            let timerRunning = false;\n            \n            // Leaderboard state - track completion times for each level\n            let levelCompletionTimes = {}; // { levelId: timeInSeconds }\n\n            // Guide image cache\n            let guideImageCache = {};\n            // Audio context and buffers\n            let audioContext = null;\n            let audioBuffers = {};\n\n            // Asset cache\n            const assetCache = {};\n\n            // Loading screen state\n            const loadingScreen = document.getElementById('loading-screen');\n\n            // Title screen state\n            let titleScreenActive = true;\n            const titleScreen = document.getElementById('title-screen');\n            const titleBg = document.getElementById('title-bg');\n            const titleLogo = document.getElementById('title-logo');\n            const titleContent = document.getElementById('title-content');\n            const titleLoading = document.getElementById('title-loading');\n            const playBtn = document.getElementById('play-btn');\n\n            // Preload title screen assets first (background loads before other title assets)\n            async function preloadTitleAssets() {\n                // Load background first\n                const bgAsset = lib.getAsset('premium_board_background');\n                if (bgAsset && bgAsset.url) {\n                    await new Promise((resolve) => {\n                        titleBg.onload = () => {\n                            titleBg.classList.add('loaded');\n                            resolve();\n                        };\n                        titleBg.onerror = resolve;\n                        titleBg.src = bgAsset.url;\n                    });\n                }\n\n                const promises = [];\n\n                // Then load title logo\n                const logoAsset = lib.getAsset('tangram_master_title');\n                if (logoAsset && logoAsset.url) {\n                    promises.push(new Promise((resolve) => {\n                        titleLogo.onload = resolve;\n                        titleLogo.onerror = resolve;\n                        titleLogo.src = logoAsset.url;\n                    }));\n                }\n\n                // Preload play button\n                const playAsset = lib.getAsset('play_button');\n                if (playAsset && playAsset.url) {\n                    promises.push(new Promise((resolve) => {\n                        const img = new Image();\n                        img.onload = resolve;\n                        img.onerror = resolve;\n                        img.src = playAsset.url;\n                    }));\n                    playBtn.style.backgroundImage = `url('${playAsset.url}')`;\n                }\n                \n                if (promises.length > 0) {\n                    await Promise.all(promises);\n                }\n\n                // Show content after assets loaded\n                titleLoading.classList.add('hidden');\n                titleContent.classList.add('visible');\n            }\n\n            // Preload assets\n            async function preloadAssets() {\n                const imageAssets = ['wooden_frame_texture', 'victory_confetti_particle',\n                    'tangram_fish', 'tangram_cat', 'tangram_boat', 'tangram_flower', 'tangram_swan',\n                    'tangram_house', 'tangram_rabbit', 'tangram_runner', 'tangram_candle', 'tangram_horse', 'tangram_dog', 'tangram_bat', 'tangram_rocket', 'cart', 'tangram_rooster', 'tangram_bridge', 'chair', 'cow', 'pig', 'bird', 'tangram_stallion', 'tangram_sitting_cat', 'tangram_airplane', 'tangram_arrow', 'tangram_question_mark', 'tangram_person', 'submit_button', 'reset_button', 'collection_button', 'leaderboard_button', 'wood_background', 'premium_board_background', 'tangram_master_title', 'play_button'\n                ];\n                const audioAssets = ['snap_sound', 'victory_sound', 'piece_pickup_sound', 'rotation_sound'];\n\n                // Load images in parallel\n                const imagePromises = imageAssets.map(async (id) => {\n                    try {\n                        const assetInfo = lib.getAsset(id);\n                        if (assetInfo && assetInfo.url) {\n                            const img = new Image();\n                            img.crossOrigin = 'anonymous';\n                            await new Promise((resolve, reject) => {\n                                img.onload = resolve;\n                                img.onerror = resolve; // Resolve on error so one failure doesn't block others\n                                img.src = assetInfo.url;\n                            });\n                            assetCache[id] = img;\n                        }\n                    } catch (e) {\n                        lib.log(`Failed to load image asset: ${id}`);\n                    }\n                });\n                await Promise.all(imagePromises);\n\n                // Initialize audio context\n                try {\n                    audioContext = new (window.AudioContext || window.webkitAudioContext)();\n                    \n                    const audioPromises = audioAssets.map(async (id) => {\n                        try {\n                            const assetInfo = lib.getAsset(id);\n                            if (assetInfo && assetInfo.url) {\n                                const response = await fetch(assetInfo.url);\n                                const arrayBuffer = await response.arrayBuffer();\n                                audioBuffers[id] = await audioContext.decodeAudioData(arrayBuffer);\n                            }\n                        } catch (e) {\n                            lib.log(`Failed to load audio asset: ${id}`);\n                        }\n                    });\n                    await Promise.all(audioPromises);\n                } catch (e) {\n                    lib.log('Audio context not available');\n                }\n            }\n\n            // Play sound\n            function playSound(id) {\n                if (audioContext && audioBuffers[id]) {\n                    try {\n                        if (audioContext.state === 'suspended') {\n                            audioContext.resume();\n                        }\n                        const source = audioContext.createBufferSource();\n                        source.buffer = audioBuffers[id];\n                        source.connect(audioContext.destination);\n                        source.start(0);\n                    } catch (e) {\n                        lib.log(`Error playing sound: ${id}`);\n                    }\n                }\n            }\n\n            // Initialize pieces in scattered positions\n            function initializePieces() {\n                const pieceTypes = Object.keys(PIECE_TYPES);\n                pieces = [];\n                \n                // Scatter positions in the bottom area (800-1200px)\n                const scatterPositions = [\n                    { x: 120, y: 920 },\n                    { x: 280, y: 880 },\n                    { x: 440, y: 920 },\n                    { x: 580, y: 880 },\n                    { x: 180, y: 1060 },\n                    { x: 360, y: 1100 },\n                    { x: 540, y: 1060 }\n                ];\n\n                pieceTypes.forEach((type, index) => {\n                    pieces.push({\n                        type: type,\n                        x: scatterPositions[index].x,\n                        y: scatterPositions[index].y,\n                        rotation: Math.floor(Math.random() * 8) * 45,\n                        flipped: false\n                    });\n                });\n            }\n\n            // Get transformed vertices for a piece (with caching)\n            function getTransformedVertices(piece) {\n                // Create cache key from piece state\n                const cacheKey = `${piece.type}_${piece.x.toFixed(1)}_${piece.y.toFixed(1)}_${piece.rotation}_${piece.flipped}`;\n                \n                if (vertexCache.has(cacheKey)) {\n                    return vertexCache.get(cacheKey);\n                }\n                \n                const baseVertices = PIECE_TYPES[piece.type].vertices();\n                const rad = piece.rotation * Math.PI / 180;\n                const cos = Math.cos(rad);\n                const sin = Math.sin(rad);\n\n                const result = baseVertices.map(v => {\n                    let x = v.x;\n                    let y = v.y;\n\n                    // Apply flip for parallelogram\n                    if (piece.flipped) {\n                        x = -x;\n                    }\n\n                    // Apply rotation\n                    const rx = x * cos - y * sin;\n                    const ry = x * sin + y * cos;\n\n                    // Apply translation\n                    return {\n                        x: rx + piece.x,\n                        y: ry + piece.y\n                    };\n                });\n                \n                vertexCache.set(cacheKey, result);\n                return result;\n            }\n            \n            // Clear vertex cache each frame\n            function clearFrameCaches() {\n                vertexCache.clear();\n            }\n\n            // Check if point is inside polygon (with optional padding for hit detection)\n            function pointInPolygon(point, vertices, padding = 0) {\n                // If padding is requested, expand the polygon outward\n                if (padding > 0) {\n                    // Calculate center of polygon\n                    const centerX = vertices.reduce((sum, v) => sum + v.x, 0) / vertices.length;\n                    const centerY = vertices.reduce((sum, v) => sum + v.y, 0) / vertices.length;\n                    \n                    // Expand each vertex away from center\n                    vertices = vertices.map(v => {\n                        const dx = v.x - centerX;\n                        const dy = v.y - centerY;\n                        const dist = Math.sqrt(dx * dx + dy * dy);\n                        if (dist < 0.001) return v; // Avoid division by zero\n                        const scale = (dist + padding) / dist;\n                        return {\n                            x: centerX + dx * scale,\n                            y: centerY + dy * scale\n                        };\n                    });\n                }\n                \n                let inside = false;\n                for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {\n                    const xi = vertices[i].x, yi = vertices[i].y;\n                    const xj = vertices[j].x, yj = vertices[j].y;\n                    \n                    if (((yi > point.y) !== (yj > point.y)) &&\n                        (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi)) {\n                        inside = !inside;\n                    }\n                }\n                return inside;\n            }\n\n            // Get piece at point with generous touch padding\n            function getPieceAtPoint(x, y) {\n                const HIT_PADDING = 20; // Extra pixels around each piece for easier selection\n                \n                // Check from top (last drawn) to bottom\n                for (let i = pieces.length - 1; i >= 0; i--) {\n                    const vertices = getTransformedVertices(pieces[i]);\n                    // First check with padding for easier touch selection\n                    if (pointInPolygon({ x, y }, vertices, HIT_PADDING)) {\n                        return i;\n                    }\n                }\n                return -1;\n            }\n\n            // Draw beveled piece with 3D raised effect\n            function drawPiece(piece, isSelected, isDraggingPiece, isHovered) {\n                const vertices = getTransformedVertices(piece);\n                const color = window.gameConfig.pieceColors[piece.type] || PIECE_TYPES[piece.type].color;\n                const bevelSize = 8; // Size of the bevel edge\n\n                ctx.save();\n\n                // Calculate center of piece for gradients\n                const centerX = vertices.reduce((sum, v) => sum + v.x, 0) / vertices.length;\n                const centerY = vertices.reduce((sum, v) => sum + v.y, 0) / vertices.length;\n\n                // Drop shadow when dragging (larger for 3D effect)\n                if (isDraggingPiece) {\n                    ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';\n                    ctx.shadowBlur = 20;\n                    ctx.shadowOffsetX = 8;\n                    ctx.shadowOffsetY = 8;\n                } else {\n                    // Subtle shadow for raised effect\n                    ctx.shadowColor = 'rgba(0, 0, 0, 0.25)';\n                    ctx.shadowBlur = 6;\n                    ctx.shadowOffsetX = 3;\n                    ctx.shadowOffsetY = 3;\n                }\n\n                // Draw the base/bottom layer (darker, offset slightly)\n                ctx.beginPath();\n                ctx.moveTo(vertices[0].x + 2, vertices[0].y + 2);\n                for (let i = 1; i < vertices.length; i++) {\n                    ctx.lineTo(vertices[i].x + 2, vertices[i].y + 2);\n                }\n                ctx.closePath();\n                ctx.fillStyle = darkenColor(color, 45);\n                ctx.fill();\n\n                // Reset shadow for main shape\n                ctx.shadowColor = 'transparent';\n                ctx.shadowBlur = 0;\n                ctx.shadowOffsetX = 0;\n                ctx.shadowOffsetY = 0;\n\n                // Main shape with gradient fill for subtle 3D effect\n                ctx.beginPath();\n                ctx.moveTo(vertices[0].x, vertices[0].y);\n                for (let i = 1; i < vertices.length; i++) {\n                    ctx.lineTo(vertices[i].x, vertices[i].y);\n                }\n                ctx.closePath();\n\n                // Create radial gradient for polished look (simplified for performance)\n                let gradientRadius = 0;\n                for (let i = 0; i < vertices.length; i++) {\n                    const dist = Math.sqrt((vertices[i].x - centerX) ** 2 + (vertices[i].y - centerY) ** 2);\n                    if (dist > gradientRadius) gradientRadius = dist;\n                }\n                const gradient = ctx.createRadialGradient(\n                    centerX - gradientRadius * 0.3, centerY - gradientRadius * 0.3, 0,\n                    centerX, centerY, gradientRadius * 1.2\n                );\n                gradient.addColorStop(0, lightenColor(color, 20));\n                gradient.addColorStop(0.5, color);\n                gradient.addColorStop(1, darkenColor(color, 15));\n                ctx.fillStyle = gradient;\n                ctx.fill();\n\n                // Draw bevel highlight edges (top-left facing) - thick and bright\n                ctx.lineCap = 'round';\n                ctx.lineJoin = 'round';\n                \n                for (let i = 0; i < vertices.length; i++) {\n                    const next = (i + 1) % vertices.length;\n                    const dx = vertices[next].x - vertices[i].x;\n                    const dy = vertices[next].y - vertices[i].y;\n                    \n                    // Calculate edge normal\n                    const len = Math.sqrt(dx * dx + dy * dy);\n                    const nx = -dy / len;\n                    const ny = dx / len;\n                    \n                    // Check if edge faces top-left (light source direction)\n                    if (nx < 0.1 || ny < 0.1) {\n                        // Draw thick highlight bevel\n                        const highlightGrad = ctx.createLinearGradient(\n                            vertices[i].x, vertices[i].y,\n                            vertices[i].x + nx * bevelSize, vertices[i].y + ny * bevelSize\n                        );\n                        highlightGrad.addColorStop(0, lightenColor(color, 50));\n                        highlightGrad.addColorStop(1, lightenColor(color, 25));\n                        \n                        ctx.strokeStyle = highlightGrad;\n                        ctx.lineWidth = bevelSize;\n                        ctx.beginPath();\n                        ctx.moveTo(vertices[i].x, vertices[i].y);\n                        ctx.lineTo(vertices[next].x, vertices[next].y);\n                        ctx.stroke();\n                    }\n                }\n\n                // Draw bevel shadow edges (bottom-right facing) - thick and dark\n                for (let i = 0; i < vertices.length; i++) {\n                    const next = (i + 1) % vertices.length;\n                    const dx = vertices[next].x - vertices[i].x;\n                    const dy = vertices[next].y - vertices[i].y;\n                    \n                    // Calculate edge normal\n                    const len = Math.sqrt(dx * dx + dy * dy);\n                    const nx = -dy / len;\n                    const ny = dx / len;\n                    \n                    // Check if edge faces bottom-right (shadow direction)\n                    if (nx > -0.1 && ny > -0.1 && (nx > 0.1 || ny > 0.1)) {\n                        // Draw thick shadow bevel\n                        const shadowGrad = ctx.createLinearGradient(\n                            vertices[i].x, vertices[i].y,\n                            vertices[i].x + nx * bevelSize, vertices[i].y + ny * bevelSize\n                        );\n                        shadowGrad.addColorStop(0, darkenColor(color, 35));\n                        shadowGrad.addColorStop(1, darkenColor(color, 20));\n                        \n                        ctx.strokeStyle = shadowGrad;\n                        ctx.lineWidth = bevelSize;\n                        ctx.beginPath();\n                        ctx.moveTo(vertices[i].x, vertices[i].y);\n                        ctx.lineTo(vertices[next].x, vertices[next].y);\n                        ctx.stroke();\n                    }\n                }\n\n                // Inner shine/highlight for polished look\n                ctx.beginPath();\n                ctx.moveTo(vertices[0].x, vertices[0].y);\n                for (let i = 1; i < vertices.length; i++) {\n                    ctx.lineTo(vertices[i].x, vertices[i].y);\n                }\n                ctx.closePath();\n                ctx.clip();\n                \n                // Add glossy highlight in top-left area\n                const shineGrad = ctx.createLinearGradient(\n                    centerX - gradientRadius, centerY - gradientRadius,\n                    centerX + gradientRadius * 0.5, centerY + gradientRadius * 0.5\n                );\n                shineGrad.addColorStop(0, 'rgba(255, 255, 255, 0.3)');\n                shineGrad.addColorStop(0.3, 'rgba(255, 255, 255, 0.1)');\n                shineGrad.addColorStop(0.5, 'rgba(255, 255, 255, 0)');\n                ctx.fillStyle = shineGrad;\n                ctx.fill();\n\n                ctx.restore();\n                ctx.save();\n\n                // Selection glow\n                if (isSelected) {\n                    ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';\n                    ctx.lineWidth = 5;\n                    ctx.shadowColor = 'rgba(255, 255, 255, 0.6)';\n                    ctx.shadowBlur = 10;\n                    ctx.beginPath();\n                    ctx.moveTo(vertices[0].x, vertices[0].y);\n                    for (let i = 1; i < vertices.length; i++) {\n                        ctx.lineTo(vertices[i].x, vertices[i].y);\n                    }\n                    ctx.closePath();\n                    ctx.stroke();\n                } else if (isHovered && !isDraggingPiece) {\n                    // Hover glow\n                    ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';\n                    ctx.lineWidth = 3;\n                    ctx.shadowColor = 'rgba(255, 255, 255, 0.4)';\n                    ctx.shadowBlur = 8;\n                    ctx.beginPath();\n                    ctx.moveTo(vertices[0].x, vertices[0].y);\n                    for (let i = 1; i < vertices.length; i++) {\n                        ctx.lineTo(vertices[i].x, vertices[i].y);\n                    }\n                    ctx.closePath();\n                    ctx.stroke();\n                }\n\n                // Dark outline for definition\n                ctx.shadowColor = 'transparent';\n                ctx.shadowBlur = 0;\n                ctx.strokeStyle = darkenColor(color, 55);\n                ctx.lineWidth = 1.5;\n                ctx.lineJoin = 'round';\n                ctx.beginPath();\n                ctx.moveTo(vertices[0].x, vertices[0].y);\n                for (let i = 1; i < vertices.length; i++) {\n                    ctx.lineTo(vertices[i].x, vertices[i].y);\n                }\n                ctx.closePath();\n                ctx.stroke();\n\n                ctx.restore();\n            }\n            // Color manipulation helpers\n            function lightenColor(hex, percent) {\n                const num = parseInt(hex.slice(1), 16);\n                const r = Math.min(255, (num >> 16) + Math.floor(255 * percent / 100));\n                const g = Math.min(255, ((num >> 8) & 0x00FF) + Math.floor(255 * percent / 100));\n                const b = Math.min(255, (num & 0x0000FF) + Math.floor(255 * percent / 100));\n                return `rgb(${r}, ${g}, ${b})`;\n            }\n\n            function darkenColor(hex, percent) {\n                const num = parseInt(hex.slice(1), 16);\n                const r = Math.max(0, (num >> 16) - Math.floor(255 * percent / 100));\n                const g = Math.max(0, ((num >> 8) & 0x00FF) - Math.floor(255 * percent / 100));\n                const b = Math.max(0, (num & 0x0000FF) - Math.floor(255 * percent / 100));\n                return `rgb(${r}, ${g}, ${b})`;\n            }\n\n            // Draw wooden frame\n            function drawFrame() {\n                const frameX = TARGET_X - FRAME_THICKNESS;\n                const frameY = TARGET_Y - FRAME_THICKNESS;\n                const frameWidth = TARGET_SIZE + FRAME_THICKNESS * 2;\n                const frameHeight = TARGET_SIZE + FRAME_THICKNESS * 2;\n\n                ctx.save();\n\n                // Frame background\n                if (assetCache['wooden_frame_texture']) {\n                    const pattern = ctx.createPattern(assetCache['wooden_frame_texture'], 'repeat');\n                    ctx.fillStyle = pattern;\n                } else {\n                    ctx.fillStyle = '#3E2723';\n                }\n\n                // Draw frame as 4 rectangles\n                // Top\n                ctx.fillRect(frameX, frameY, frameWidth, FRAME_THICKNESS);\n                // Bottom\n                ctx.fillRect(frameX, TARGET_Y + TARGET_SIZE, frameWidth, FRAME_THICKNESS);\n                // Left\n                ctx.fillRect(frameX, TARGET_Y, FRAME_THICKNESS, TARGET_SIZE);\n                // Right\n                ctx.fillRect(TARGET_X + TARGET_SIZE, TARGET_Y, FRAME_THICKNESS, TARGET_SIZE);\n\n                // Inner shadow\n                ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';\n                ctx.lineWidth = 3;\n                ctx.strokeRect(TARGET_X, TARGET_Y, TARGET_SIZE, TARGET_SIZE);\n\n                // Outer highlight\n                ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';\n                ctx.lineWidth = 2;\n                ctx.strokeRect(frameX, frameY, frameWidth, frameHeight);\n\n                // Corner decorations\n                const cornerSize = FRAME_THICKNESS - 4;\n                ctx.fillStyle = '#5D4037';\n                [[frameX + 2, frameY + 2], \n                 [frameX + frameWidth - cornerSize - 2, frameY + 2],\n                 [frameX + 2, frameY + frameHeight - cornerSize - 2],\n                 [frameX + frameWidth - cornerSize - 2, frameY + frameHeight - cornerSize - 2]\n                ].forEach(([x, y]) => {\n                    ctx.fillRect(x, y, cornerSize, cornerSize);\n                });\n\n                ctx.restore();\n            }\n\n            // Update the guide panel in the UI\n            function updateGuidePanel() {\n                const level = LEVELS[currentLevelIndex];\n                const guideAsset = assetCache[level.guideAsset];\n                \n                if (guideAsset && guideAsset.src) {\n                    guideImage.src = guideAsset.src;\n                    guideImage.alt = level.name;\n                }\n            }\n\n            // Draw target area (the play zone)\n            function drawTargetArea() {\n                ctx.save();\n                \n                // Draw a neutral target area - open play area with light border for all levels\n                ctx.fillStyle = 'rgba(200, 200, 200, 0.15)';\n                ctx.fillRect(TARGET_X - 40, TARGET_Y - 40, TARGET_SIZE + 80, TARGET_SIZE + 80);\n                \n                ctx.strokeStyle = 'rgba(93, 64, 55, 0.3)';\n                ctx.lineWidth = 2;\n                ctx.setLineDash([8, 4]);\n                ctx.strokeRect(TARGET_X - 40, TARGET_Y - 40, TARGET_SIZE + 80, TARGET_SIZE + 80);\n                ctx.setLineDash([]);\n\n                ctx.restore();\n            }\n\n            // Polygon area calculation using shoelace formula\n            function polygonArea(vertices) {\n                let area = 0;\n                for (let i = 0; i < vertices.length; i++) {\n                    const j = (i + 1) % vertices.length;\n                    area += vertices[i].x * vertices[j].y;\n                    area -= vertices[j].x * vertices[i].y;\n                }\n                return Math.abs(area / 2);\n            }\n\n            // Check if polygon is inside target area\n            function isPolygonInsideTarget(vertices) {\n                const expandedMargin = 250; // Allow room for free-form shapes\n                \n                // All levels use larger boundary for free-form shapes\n                return vertices.every(v => \n                    v.x >= TARGET_X - expandedMargin && \n                    v.x <= TARGET_X + TARGET_SIZE + expandedMargin &&\n                    v.y >= TARGET_Y - expandedMargin && \n                    v.y <= TARGET_Y + TARGET_SIZE + expandedMargin\n                );\n            }\n            // Dist to segment helper\n            function distToSegmentSquared(p, v, w) {\n                const l2 = (w.x - v.x)*(w.x - v.x) + (w.y - v.y)*(w.y - v.y);\n                if (l2 === 0) return (p.x - v.x)*(p.x - v.x) + (p.y - v.y)*(p.y - v.y);\n                let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;\n                t = Math.max(0, Math.min(1, t));\n                return (p.x - (v.x + t * (w.x - v.x)))*(p.x - (v.x + t * (w.x - v.x))) + \n                       (p.y - (v.y + t * (w.y - v.y)))*(p.y - (v.y + t * (w.y - v.y)));\n            }\n\n            // Check if two pieces are connected\n            function arePiecesConnected(piece1, piece2) {\n                const v1 = getTransformedVertices(piece1);\n                const v2 = getTransformedVertices(piece2);\n                const toleranceSq = 25; // 5px tolerance\n                \n                for (const p1 of v1) {\n                    for (let i = 0; i < v2.length; i++) {\n                        if (distToSegmentSquared(p1, v2[i], v2[(i+1)%v2.length]) <= toleranceSq) return true;\n                    }\n                }\n                for (const p2 of v2) {\n                    for (let i = 0; i < v1.length; i++) {\n                        if (distToSegmentSquared(p2, v1[i], v1[(i+1)%v1.length]) <= toleranceSq) return true;\n                    }\n                }\n                return false;\n            }\n\n            // Simple overlap check using sampling\n            function checkOverlap(piece1, piece2) {\n                const v1 = getTransformedVertices(piece1);\n                const v2 = getTransformedVertices(piece2);\n                \n                // Sample points inside piece1 and check if any are inside piece2\n                const center1 = { \n                    x: v1.reduce((s, v) => s + v.x, 0) / v1.length,\n                    y: v1.reduce((s, v) => s + v.y, 0) / v1.length\n                };\n                \n                // Check center and midpoints (inset slightly to avoid edge false positives)\n                const testPoints = [center1];\n                for (let i = 0; i < v1.length; i++) {\n                    testPoints.push({\n                        x: v1[i].x * 0.90 + center1.x * 0.10,\n                        y: v1[i].y * 0.90 + center1.y * 0.10\n                    });\n                    const midX = (v1[i].x + v1[(i+1)%v1.length].x) / 2;\n                    const midY = (v1[i].y + v1[(i+1)%v1.length].y) / 2;\n                    testPoints.push({\n                        x: midX * 0.90 + center1.x * 0.10,\n                        y: midY * 0.90 + center1.y * 0.10\n                    });\n                }\n\n                for (const point of testPoints) {\n                    if (pointInPolygon(point, v2)) {\n                        return true;\n                    }\n                }\n\n                // Also check piece2 points in piece1\n                const center2 = { \n                    x: v2.reduce((s, v) => s + v.x, 0) / v2.length,\n                    y: v2.reduce((s, v) => s + v.y, 0) / v2.length\n                };\n                \n                if (pointInPolygon(center2, v1)) {\n                    return true;\n                }\n\n                return false;\n            }\n\n            // Win detection\n            function checkWinCondition() {\n                // Check all pieces are inside target\n                for (const piece of pieces) {\n                    const vertices = getTransformedVertices(piece);\n                    if (!isPolygonInsideTarget(vertices)) {\n                        return false;\n                    }\n                }\n\n                // Check no overlaps\n                for (let i = 0; i < pieces.length; i++) {\n                    for (let j = i + 1; j < pieces.length; j++) {\n                        if (checkOverlap(pieces[i], pieces[j])) {\n                            return false;\n                        }\n                    }\n                }\n\n                // Check if all pieces form a single connected shape\n                if (pieces.length > 0) {\n                    const visited = new Set();\n                    const queue = [0];\n                    visited.add(0);\n                    \n                    while (queue.length > 0) {\n                        const curr = queue.shift();\n                        for (let i = 0; i < pieces.length; i++) {\n                            if (!visited.has(i) && arePiecesConnected(pieces[curr], pieces[i])) {\n                                visited.add(i);\n                                queue.push(i);\n                            }\n                        }\n                    }\n                    \n                    if (visited.size !== pieces.length) {\n                        return false;\n                    }\n                }\n\n                const result = analyzePuzzle();\n                lib.log(\"analyze puzzle score: \" + result.score);\n                if (result.score < 65) {\n                    return false;\n                }\n\n                return true;\n            }\n\n            // Snap piece to nearby edges\n            function snapPiece(piece) {\n                const snapDist = window.gameConfig.snapSensitivity || 25;\n                const vertices = getTransformedVertices(piece);\n                \n                let snapX = 0;\n                let snapY = 0;\n                let snapped = false;\n\n                // Snap to target edges\n                for (const v of vertices) {\n                    // Left edge\n                    if (Math.abs(v.x - TARGET_X) < snapDist) {\n                        snapX = TARGET_X - v.x;\n                        snapped = true;\n                    }\n                    // Right edge\n                    if (Math.abs(v.x - (TARGET_X + TARGET_SIZE)) < snapDist) {\n                        snapX = (TARGET_X + TARGET_SIZE) - v.x;\n                        snapped = true;\n                    }\n                    // Top edge\n                    if (Math.abs(v.y - TARGET_Y) < snapDist) {\n                        snapY = TARGET_Y - v.y;\n                        snapped = true;\n                    }\n                    // Bottom edge\n                    if (Math.abs(v.y - (TARGET_Y + TARGET_SIZE)) < snapDist) {\n                        snapY = (TARGET_Y + TARGET_SIZE) - v.y;\n                        snapped = true;\n                    }\n                }\n\n                // Snap to other pieces\n                for (const other of pieces) {\n                    if (other === piece) continue;\n                    const otherVerts = getTransformedVertices(other);\n                    \n                    for (const v of vertices) {\n                        for (const ov of otherVerts) {\n                            const dx = ov.x - v.x;\n                            const dy = ov.y - v.y;\n                            if (Math.abs(dx) < snapDist && Math.abs(dy) < snapDist) {\n                                if (Math.abs(dx) < Math.abs(snapX) || snapX === 0) snapX = dx;\n                                if (Math.abs(dy) < Math.abs(snapY) || snapY === 0) snapY = dy;\n                                snapped = true;\n                            }\n                        }\n                    }\n                }\n\n                if (snapped) {\n                    piece.x += snapX;\n                    piece.y += snapY;\n                    playSound('snap_sound');\n                }\n\n                return snapped;\n            }\n\n            // Update control buttons position\n            function updateControlButtons() {\n                if (selectedPieceIndex < 0 || mode === 'edit') {\n                    controlButtons.style.display = 'none';\n                    return;\n                }\n\n                const piece = pieces[selectedPieceIndex];\n                const vertices = getTransformedVertices(piece);\n                \n                // Find top center of piece\n                let minY = Infinity;\n                let centerX = piece.x;\n                for (const v of vertices) {\n                    if (v.y < minY) minY = v.y;\n                }\n\n                // Position buttons above piece\n                let btnY = minY - 90;\n                if (btnY < 200) btnY = minY + 100; // Move below if too high\n\n                controlButtons.style.display = 'flex';\n                controlButtons.style.left = `${centerX - 114}px`;\n                controlButtons.style.top = `${btnY}px`;\n\n                // Show flip button only for parallelogram\n                flipBtn.style.display = piece.type === 'parallelogram' ? 'flex' : 'none';\n            }\n\n            // Timer functions\n            function startTimer() {\n                timerStartTime = performance.now();\n                timerElapsed = 0;\n                timerRunning = true;\n            }\n            \n            function stopTimer() {\n                if (timerRunning) {\n                    timerElapsed = performance.now() - timerStartTime;\n                    timerRunning = false;\n                }\n            }\n            \n            function resetTimer() {\n                timerStartTime = performance.now();\n                timerElapsed = 0;\n                timerRunning = true;\n            }\n            \n            function updateTimerDisplay() {\n                let elapsed = timerRunning ? (performance.now() - timerStartTime) : timerElapsed;\n                const totalSeconds = Math.floor(elapsed / 1000);\n                const minutes = Math.floor(totalSeconds / 60);\n                const seconds = totalSeconds % 60;\n                const formattedTime = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n                timerDisplay.textContent = `\u23f1\ufe0f ${formattedTime}`;\n            }\n\n            // Create confetti particles (reduced count for performance)\n            function createConfetti() {\n                confettiParticles = [];\n                const colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];\n                const particleCount = 60; // Reduced from 100 for better performance\n                \n                for (let i = 0; i < particleCount; i++) {\n                    confettiParticles.push({\n                        x: Math.random() * CANVAS_WIDTH,\n                        y: -20 - Math.random() * 200,\n                        vx: (Math.random() - 0.5) * 4,\n                        vy: Math.random() * 3 + 2,\n                        rotation: Math.random() * 360,\n                        rotationSpeed: (Math.random() - 0.5) * 10,\n                        size: Math.random() * 10 + 5,\n                        color: colors[Math.floor(Math.random() * colors.length)]\n                    });\n                }\n            }\n\n            // Build level selector UI\n            function buildLevelSelector() {\n                levelSelector.innerHTML = '';\n                \n                LEVELS.forEach((level, index) => {\n                    const btn = document.createElement('button');\n                    btn.className = 'level-btn';\n                    btn.style.position = 'relative';\n                    \n                    const isUnlocked = index === 0 || completedLevels.includes(LEVELS[index - 1].id);\n                    const isCompleted = completedLevels.includes(level.id);\n                    const isActive = index === currentLevelIndex;\n                    \n                    if (!isUnlocked) {\n                        btn.classList.add('locked');\n                        btn.innerHTML = '<span class=\"lock-icon\">\ud83d\udd12</span>';\n                    } else {\n                        btn.textContent = level.icon;\n                        if (isCompleted) {\n                            btn.classList.add('completed');\n                            btn.innerHTML = level.icon + '<span class=\"check-icon\">\u2713</span>';\n                        }\n                    }\n                    \n                    if (isActive) {\n                        btn.classList.add('active');\n                    }\n                    \n                    btn.addEventListener('click', () => {\n                        if (isUnlocked && !gameWon) {\n                            switchLevel(index);\n                        }\n                    });\n                    \n                    levelSelector.appendChild(btn);\n                });\n            }\n\n            // Switch to a different level\n            function switchLevel(levelIndex) {\n                currentLevelIndex = levelIndex;\n                window.gameConfig.currentLevel = levelIndex + 1;\n                \n                const level = LEVELS[currentLevelIndex];\n                levelIndicator.textContent = `Level ${level.id}: ${level.name}`;\n                \n                // Update guide panel\n                updateGuidePanel();\n                \n                // Reset game state\n                gameWon = false;\n                victoryOverlay.style.display = 'none';\n                confettiParticles = [];\n                selectedPieceIndex = -1;\n                \n                // Reset and start timer\n                resetTimer();\n                \n                // Guide is always visible\n                \n                initializePieces();\n                updateControlButtons();\n                buildLevelSelector();\n            }\n\n            // Build collection gallery\n            function buildCollection() {\n                collectionGrid.innerHTML = '';\n                \n                const completedCount = completedLevels.length;\n                collectionSubtitle.textContent = `${completedCount} / ${LEVELS.length} Completed`;\n                \n                LEVELS.forEach((level, index) => {\n                    const isCompleted = completedLevels.includes(level.id);\n                    const isUnlocked = index === 0 || completedLevels.includes(LEVELS[index - 1].id);\n                    \n                    const item = document.createElement('div');\n                    item.className = 'collection-item';\n                    if (isCompleted) {\n                        item.classList.add('completed');\n                    } else if (!isUnlocked) {\n                        item.classList.add('locked');\n                    }\n                    \n                    const imageDiv = document.createElement('div');\n                    imageDiv.className = 'collection-item-image';\n                    \n                    const guideImage = assetCache[level.guideAsset];\n                    if (guideImage && (isCompleted || isUnlocked)) {\n                        const img = document.createElement('img');\n                        img.src = guideImage.src;\n                        img.alt = level.name;\n                        imageDiv.appendChild(img);\n                    } else if (!isUnlocked) {\n                        // Show silhouette placeholder for locked items\n                        imageDiv.style.background = '#E0E0E0';\n                    }\n                    \n                    const infoDiv = document.createElement('div');\n                    infoDiv.className = 'collection-item-info';\n                    \n                    const nameP = document.createElement('p');\n                    nameP.className = 'collection-item-name';\n                    nameP.textContent = `${level.icon} ${level.name}`;\n                    \n                    const statusP = document.createElement('p');\n                    statusP.className = 'collection-item-status';\n                    if (isCompleted) {\n                        statusP.textContent = '\u2713 Completed';\n                    } else if (isUnlocked) {\n                        statusP.textContent = 'In Progress';\n                    } else {\n                        statusP.textContent = 'Locked';\n                    }\n                    \n                    infoDiv.appendChild(nameP);\n                    infoDiv.appendChild(statusP);\n                    \n                    item.appendChild(imageDiv);\n                    item.appendChild(infoDiv);\n                    \n                    // Click to go to that level (if unlocked)\n                    if (isUnlocked) {\n                        item.style.cursor = 'pointer';\n                        item.addEventListener('click', () => {\n                            collectionOverlay.style.display = 'none';\n                            switchLevel(index);\n                        });\n                    }\n                    \n                    collectionGrid.appendChild(item);\n                });\n            }\n\n            // Open collection\n            function openCollection() {\n                buildCollection();\n                collectionOverlay.style.display = 'flex';\n            }\n\n            // Close collection\n            function closeCollection() {\n                collectionOverlay.style.display = 'none';\n            }\n\n            // Mark current level as completed\n            function completeLevel() {\n                const level = LEVELS[currentLevelIndex];\n                if (!completedLevels.includes(level.id)) {\n                    completedLevels.push(level.id);\n                    window.gameConfig.completedLevels = [...completedLevels];\n                }\n                buildLevelSelector();\n            }\n\n            // Update confetti (optimized in-place filtering)\n            function updateConfetti(deltaTime) {\n                let writeIndex = 0;\n                for (let i = 0; i < confettiParticles.length; i++) {\n                    const p = confettiParticles[i];\n                    p.x += p.vx;\n                    p.y += p.vy;\n                    p.rotation += p.rotationSpeed;\n                    p.vy += 0.1; // Gravity\n                    \n                    // Keep particle if still visible\n                    if (p.y < CANVAS_HEIGHT + 50) {\n                        confettiParticles[writeIndex++] = p;\n                    }\n                }\n                confettiParticles.length = writeIndex;\n            }\n\n            // Draw confetti\n            function drawConfetti() {\n                for (const p of confettiParticles) {\n                    ctx.save();\n                    ctx.translate(p.x, p.y);\n                    ctx.rotate(p.rotation * Math.PI / 180);\n                    ctx.fillStyle = p.color;\n                    ctx.fillRect(-p.size/2, -p.size/4, p.size, p.size/2);\n                    ctx.restore();\n                }\n            }\n\n            // Trigger victory\n            function triggerVictory() {\n                gameWon = true;\n                stopTimer(); // Stop the timer on victory\n                \n                // Record level completion time\n                const level = LEVELS[currentLevelIndex];\n                const timeInSeconds = Math.floor(timerElapsed / 1000);\n                levelCompletionTimes[level.id] = timeInSeconds;\n                \n                // Calculate average time and submit to leaderboard\n                submitAverageTimeToLeaderboard();\n                \n                createConfetti();\n                playSound('victory_sound');\n                \n                // Mark level as completed\n                completeLevel();\n                \n                // Update victory message\n                const victoryText = victoryOverlay.querySelector('.victory-text');\n                victoryText.textContent = `${level.name.toUpperCase()} COMPLETED!`;\n                \n                // Check if there's a next level\n                const nextLevelIndex = currentLevelIndex + 1;\n                const hasNextLevel = nextLevelIndex < LEVELS.length;\n                \n                // Update button text\n                playAgainBtn.textContent = hasNextLevel ? 'Next Level' : 'Play Again';\n                playAgainBtn.dataset.action = hasNextLevel ? 'next' : 'replay';\n                \n                setTimeout(() => {\n                    victoryOverlay.style.display = 'flex';\n                }, 1500);\n            }\n\n            // Input handling\n            function getInputPosition(e) {\n                const rect = canvas.getBoundingClientRect();\n                const scaleX = CANVAS_WIDTH / rect.width;\n                const scaleY = CANVAS_HEIGHT / rect.height;\n                \n                if (e.touches) {\n                    return {\n                        x: (e.touches[0].clientX - rect.left) * scaleX,\n                        y: (e.touches[0].clientY - rect.top) * scaleY\n                    };\n                }\n                return {\n                    x: (e.clientX - rect.left) * scaleX,\n                    y: (e.clientY - rect.top) * scaleY\n                };\n            }\n\n            function handlePointerDown(e) {\n                if (gameWon || mode === 'edit') return;\n                e.preventDefault();\n                \n                const pos = getInputPosition(e);\n                const pieceIndex = getPieceAtPoint(pos.x, pos.y);\n                \n                if (pieceIndex >= 0) {\n                    selectedPieceIndex = pieceIndex;\n                    isDragging = true;\n                    canvas.className = 'grabbing';\n                    dragOffset = {\n                        x: pos.x - pieces[pieceIndex].x,\n                        y: pos.y - pieces[pieceIndex].y\n                    };\n                    \n                    // Move to top\n                    const piece = pieces.splice(pieceIndex, 1)[0];\n                    pieces.push(piece);\n                    selectedPieceIndex = pieces.length - 1;\n                    hoveredPieceIndex = selectedPieceIndex;\n                    \n                    playSound('piece_pickup_sound');\n                    updateControlButtons();\n                } else {\n                    selectedPieceIndex = -1;\n                    updateControlButtons();\n                }\n            }\n\n            // Throttle for hover detection\n            let lastHoverCheck = 0;\n            const HOVER_THROTTLE = 50; // ms\n            \n            function handlePointerMove(e) {\n                if (gameWon || mode === 'edit') return;\n                \n                const pos = getInputPosition(e);\n                \n                if (isDragging && selectedPieceIndex >= 0) {\n                    e.preventDefault();\n                    pieces[selectedPieceIndex].x = pos.x - dragOffset.x;\n                    pieces[selectedPieceIndex].y = pos.y - dragOffset.y;\n                    updateControlButtons();\n                    canvas.className = 'grabbing';\n                } else {\n                    // Throttle hover detection for performance\n                    const now = performance.now();\n                    if (now - lastHoverCheck > HOVER_THROTTLE) {\n                        lastHoverCheck = now;\n                        // Update hover state for visual feedback\n                        const newHovered = getPieceAtPoint(pos.x, pos.y);\n                        if (newHovered !== hoveredPieceIndex) {\n                            hoveredPieceIndex = newHovered;\n                            // Update cursor\n                            canvas.className = hoveredPieceIndex >= 0 ? 'can-grab' : '';\n                        }\n                    }\n                }\n            }\n\n            function handlePointerUp(e) {\n                if (!isDragging || gameWon || mode === 'edit') return;\n                \n                isDragging = false;\n                canvas.className = hoveredPieceIndex >= 0 ? 'can-grab' : '';\n                \n                // Snap piece\n                snapPiece(pieces[selectedPieceIndex]);\n                updateControlButtons();\n            }\n\n            // Event listeners\n            canvas.addEventListener('mousedown', handlePointerDown, { passive: false });\n            canvas.addEventListener('mousemove', handlePointerMove, { passive: true });\n            canvas.addEventListener('mouseup', handlePointerUp, { passive: true });\n            canvas.addEventListener('mouseleave', handlePointerUp, { passive: true });\n            \n            canvas.addEventListener('touchstart', handlePointerDown, { passive: false });\n            canvas.addEventListener('touchmove', handlePointerMove, { passive: false });\n            canvas.addEventListener('touchend', handlePointerUp, { passive: true });\n\n            // Rotation buttons\n            rotateLeftBtn.addEventListener('click', () => {\n                if (selectedPieceIndex >= 0 && !gameWon) {\n                    pieces[selectedPieceIndex].rotation = (pieces[selectedPieceIndex].rotation - 45 + 360) % 360;\n                    playSound('rotation_sound');\n                    snapPiece(pieces[selectedPieceIndex]);\n                }\n            });\n\n            rotateRightBtn.addEventListener('click', () => {\n                if (selectedPieceIndex >= 0 && !gameWon) {\n                    pieces[selectedPieceIndex].rotation = (pieces[selectedPieceIndex].rotation + 45) % 360;\n                    playSound('rotation_sound');\n                    snapPiece(pieces[selectedPieceIndex]);\n                }\n            });\n\n            flipBtn.addEventListener('click', () => {\n                if (selectedPieceIndex >= 0 && pieces[selectedPieceIndex].type === 'parallelogram' && !gameWon) {\n                    pieces[selectedPieceIndex].flipped = !pieces[selectedPieceIndex].flipped;\n                    playSound('rotation_sound');\n                    snapPiece(pieces[selectedPieceIndex]);\n                }\n            });\n\n            // Reset button\n            resetBtn.addEventListener('click', () => {\n                gameWon = false;\n                victoryOverlay.style.display = 'none';\n                confettiParticles = [];\n                selectedPieceIndex = -1;\n                initializePieces();\n                updateControlButtons();\n                resetTimer(); // Reset and restart timer\n            });\n\n            // Play again / Next level button\n            playAgainBtn.addEventListener('click', () => {\n                if (playAgainBtn.dataset.action === 'next') {\n                    // Go to next level\n                    const nextLevelIndex = currentLevelIndex + 1;\n                    if (nextLevelIndex < LEVELS.length) {\n                        switchLevel(nextLevelIndex);\n                    }\n                } else {\n                    // Replay current level\n                    gameWon = false;\n                    victoryOverlay.style.display = 'none';\n                    confettiParticles = [];\n                    selectedPieceIndex = -1;\n                    initializePieces();\n                    updateControlButtons();\n                    resetTimer(); // Reset and restart timer\n                }\n            });\n\n            submitBtn.addEventListener('click', () => {\n                if (gameWon) return;\n                \n                feedbackMessage.textContent = 'Analyzing...';\n                feedbackScore.textContent = '';\n                feedbackOverlay.style.display = 'flex';\n                feedbackCloseBtn.style.display = 'none';\n                \n                setTimeout(() => {\n                    const result = analyzePuzzle();\n                    feedbackScore.textContent = `${result.score}%`;\n                    \n                    if (result.score >= 95) {\n                        feedbackScore.className = 'feedback-score perfect';\n                        feedbackMessage.textContent = 'Perfect!';\n                        feedbackCloseBtn.textContent = 'Celebrate!';\n                        feedbackCloseBtn.dataset.action = 'celebrate';\n                    } else if (result.score >= 80) {\n                        feedbackScore.className = 'feedback-score good';\n                        feedbackMessage.textContent = 'Getting close!';\n                        feedbackCloseBtn.textContent = 'Try Again';\n                        feedbackCloseBtn.dataset.action = 'retry';\n                    } else {\n                        feedbackScore.className = 'feedback-score needs-work';\n                        feedbackMessage.textContent = 'Keep trying!';\n                        feedbackCloseBtn.textContent = 'Try Again';\n                        feedbackCloseBtn.dataset.action = 'retry';\n                    }\n                    \n                    feedbackCloseBtn.style.display = 'inline-block';\n                }, 500);\n            });\n\n            // Analyze puzzle against guide image\n            function analyzePuzzle() {\n                const level = LEVELS[currentLevelIndex];\n                const guideImage = assetCache[level.guideAsset];\n                \n                if (!guideImage) {\n                    return { score: 0, message: 'Guide image not loaded' };\n                }\n\n                // Check basic constraints first\n                // 1. All pieces must be inside the target area\n                // 2. No overlapping pieces\n                let overlaps = false;\n                for (let i = 0; i < pieces.length; i++) {\n                    for (let j = i + 1; j < pieces.length; j++) {\n                        if (checkOverlap(pieces[i], pieces[j])) {\n                            overlaps = true;\n                            break;\n                        }\n                    }\n                }\n                \n                if (overlaps) {\n                    return { score: 0, message: 'Pieces cannot overlap' };\n                }\n\n                const compareSize = 200;\n                \n                // Canvas for player's pieces\n                const playerCanvas = document.createElement('canvas');\n                playerCanvas.width = compareSize;\n                playerCanvas.height = compareSize;\n                const playerCtx = playerCanvas.getContext('2d');\n                \n                // Canvas for guide image\n                const guideCanvas = document.createElement('canvas');\n                guideCanvas.width = compareSize;\n                guideCanvas.height = compareSize;\n                const guideCtx = guideCanvas.getContext('2d');\n                \n                // Calculate bounding box of all pieces\n                let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n                for (const piece of pieces) {\n                    const vertices = getTransformedVertices(piece);\n                    for (const v of vertices) {\n                        minX = Math.min(minX, v.x);\n                        minY = Math.min(minY, v.y);\n                        maxX = Math.max(maxX, v.x);\n                        maxY = Math.max(maxY, v.y);\n                    }\n                }\n                \n                const pieceWidth = maxX - minX;\n                const pieceHeight = maxY - minY;\n                const pieceSize = Math.max(pieceWidth, pieceHeight);\n                const padding = 20;\n                const scale = (compareSize - padding * 2) / pieceSize;\n                \n                // Draw player pieces as black silhouette, slightly dilated with stroke\n                playerCtx.fillStyle = 'white';\n                playerCtx.fillRect(0, 0, compareSize, compareSize);\n                playerCtx.fillStyle = 'black';\n                playerCtx.strokeStyle = 'black';\n                playerCtx.lineWidth = 12; // Dilation amount for forgiveness\n                playerCtx.lineJoin = 'round';\n                \n                for (const piece of pieces) {\n                    const vertices = getTransformedVertices(piece);\n                    playerCtx.beginPath();\n                    const startX = (vertices[0].x - minX + (pieceSize - pieceWidth) / 2) * scale + padding;\n                    const startY = (vertices[0].y - minY + (pieceSize - pieceHeight) / 2) * scale + padding;\n                    playerCtx.moveTo(startX, startY);\n                    for (let i = 1; i < vertices.length; i++) {\n                        const x = (vertices[i].x - minX + (pieceSize - pieceWidth) / 2) * scale + padding;\n                        const y = (vertices[i].y - minY + (pieceSize - pieceHeight) / 2) * scale + padding;\n                        playerCtx.lineTo(x, y);\n                    }\n                    playerCtx.closePath();\n                    playerCtx.fill();\n                    playerCtx.stroke();\n                }\n                \n                // Draw guide image preserving aspect ratio to find bounding box\n                guideCtx.fillStyle = 'white';\n                guideCtx.fillRect(0, 0, compareSize, compareSize);\n                \n                const imgAspect = guideImage.width / guideImage.height;\n                let drawW = compareSize, drawH = compareSize;\n                if (imgAspect > 1) {\n                    drawH = compareSize / imgAspect;\n                } else {\n                    drawW = compareSize * imgAspect;\n                }\n                const drawX = (compareSize - drawW) / 2;\n                const drawY = (compareSize - drawH) / 2;\n                \n                guideCtx.drawImage(guideImage, drawX, drawY, drawW, drawH);\n                \n                let tempGuideData = guideCtx.getImageData(0, 0, compareSize, compareSize);\n                let gMinX = compareSize, gMinY = compareSize, gMaxX = 0, gMaxY = 0;\n                \n                for (let y = 0; y < compareSize; y++) {\n                    for (let x = 0; x < compareSize; x++) {\n                        const i = (y * compareSize + x) * 4;\n                        const r = tempGuideData.data[i];\n                        const g = tempGuideData.data[i+1];\n                        const b = tempGuideData.data[i+2];\n                        const a = tempGuideData.data[i+3];\n                        const brightness = (r + g + b) / 3;\n                        if (a > 128 && (r < 245 || g < 245 || b < 245)) {\n                            if (x < gMinX) gMinX = x;\n                            if (y < gMinY) gMinY = y;\n                            if (x > gMaxX) gMaxX = x;\n                            if (y > gMaxY) gMaxY = y;\n                        }\n                    }\n                }\n\n                const tempCanvas = document.createElement('canvas');\n                tempCanvas.width = compareSize;\n                tempCanvas.height = compareSize;\n                tempCanvas.getContext('2d').putImageData(tempGuideData, 0, 0);\n\n                // Clear and redraw guide exactly fitted\n                guideCtx.fillStyle = 'white';\n                guideCtx.fillRect(0, 0, compareSize, compareSize);\n\n                if (gMaxX >= gMinX && gMaxY >= gMinY) {\n                    const gWidth = gMaxX - gMinX;\n                    const gHeight = gMaxY - gMinY;\n                    const gSize = Math.max(gWidth, gHeight);\n                    const gScale = (compareSize - padding * 2) / gSize;\n                    \n                    const destW = gWidth * gScale;\n                    const destH = gHeight * gScale;\n                    const destX = padding + ((compareSize - padding * 2) - destW) / 2;\n                    const destY = padding + ((compareSize - padding * 2) - destH) / 2;\n                    \n                    // Source rect from the first draw coordinates\n                    guideCtx.drawImage(tempCanvas, gMinX, gMinY, gWidth, gHeight, destX, destY, destW, destH);\n                }\n                \n                const playerData = playerCtx.getImageData(0, 0, compareSize, compareSize).data;\n                const guideFinalData = guideCtx.getImageData(0, 0, compareSize, compareSize).data;\n                \n                let matchingPixels = 0;\n                let totalGuidePixels = 0;\n                \n                for (let i = 0; i < playerData.length; i += 4) {\n                    const playerBlack = playerData[i] < 128;\n                    const guideR = guideFinalData[i];\n                    const guideG = guideFinalData[i+1];\n                    const guideB = guideFinalData[i+2];\n                    const guideA = guideFinalData[i+3];\n                    const guideForeground = guideA > 128 && (guideR < 245 || guideG < 245 || guideB < 245);\n                    \n                    if (guideForeground) {\n                        totalGuidePixels++;\n                        if (playerBlack) {\n                            matchingPixels++;\n                        }\n                    }\n                }\n                \n                const score = totalGuidePixels > 0 ? (matchingPixels / totalGuidePixels) * 100 : 0;\n                \n                // If the player's bounding box aspect ratio is very different from the guide's, penalize the score\n                const playerAspect = pieceWidth / pieceHeight;\n                const gWidth = gMaxX - gMinX;\n                const gHeight = gMaxY - gMinY;\n                const guideAspect = gWidth > 0 && gHeight > 0 ? gWidth / gHeight : 1;\n                \n                let finalScore = score;\n                if (Math.abs(playerAspect - guideAspect) > 0.3) {\n                    finalScore -= 15;\n                }\n                \n                if (finalScore >= 80) {\n                    finalScore = 100;\n                }\n                \n                return { score: Math.round(Math.max(0, finalScore)) };\n            }\n\n            // Feedback close button handler\n            feedbackCloseBtn.addEventListener('click', () => {\n                feedbackOverlay.style.display = 'none';\n                \n                if (feedbackCloseBtn.dataset.action === 'celebrate') {\n                    // Trigger victory celebration\n                    triggerVictory();\n                }\n            });\n\n            // Resume audio context on user interaction\n            document.addEventListener('click', () => {\n                if (audioContext && audioContext.state === 'suspended') {\n                    audioContext.resume();\n                }\n            }, { once: true });\n\n            // Collection button handlers\n            collectionBtn.addEventListener('click', openCollection);\n            collectionCloseBtn.addEventListener('click', closeCollection);\n\n            // Submit average time to leaderboard\n            async function submitAverageTimeToLeaderboard() {\n                try {\n                    const completedLevelIds = Object.keys(levelCompletionTimes);\n                    if (completedLevelIds.length === 0) return;\n                    \n                    // Calculate total time and average\n                    const totalTime = completedLevelIds.reduce((sum, id) => sum + levelCompletionTimes[id], 0);\n                    const averageTime = Math.round(totalTime / completedLevelIds.length);\n                    \n                    // Submit to leaderboard (lower is better)\n                    const response = await lib.addPlayerScoreToLeaderboard(averageTime, 10);\n                    \n                    if (response.success) {\n                        lib.log(`Leaderboard updated! Average time: ${averageTime}s, Rank: ${response.userRank || 'N/A'}`);\n                    }\n                } catch (error) {\n                    lib.log(`Failed to submit to leaderboard: ${error.message}`);\n                }\n            }\n\n            // Format seconds as MM:SS\n            function formatTime(seconds) {\n                const mins = Math.floor(seconds / 60);\n                const secs = seconds % 60;\n                return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n            }\n\n            // Open leaderboard\n            async function openLeaderboard() {\n                leaderboardOverlay.style.display = 'flex';\n                leaderboardList.innerHTML = '<div class=\"leaderboard-loading\">Loading...</div>';\n                \n                try {\n                    const response = await lib.getTopNEntriesFromLeaderboard(10);\n                    \n                    if (response.entries && response.entries.length > 0) {\n                        leaderboardList.innerHTML = '';\n                        \n                        response.entries.forEach((entry, index) => {\n                            const entryDiv = document.createElement('div');\n                            entryDiv.className = 'leaderboard-entry';\n                            \n                            // Highlight current user's entry\n                            const isCurrentUser = response.userRank && (index + 1) === response.userRank;\n                            if (isCurrentUser) {\n                                entryDiv.classList.add('current-user');\n                            }\n                            \n                            // Rank\n                            const rankDiv = document.createElement('div');\n                            rankDiv.className = 'leaderboard-rank';\n                            if (index === 0) rankDiv.classList.add('top-1');\n                            else if (index === 1) rankDiv.classList.add('top-2');\n                            else if (index === 2) rankDiv.classList.add('top-3');\n                            rankDiv.textContent = `#${index + 1}`;\n                            \n                            // Profile picture\n                            const profileDiv = document.createElement('div');\n                            profileDiv.className = 'leaderboard-profile';\n                            if (entry.profilePicture) {\n                                const img = document.createElement('img');\n                                img.src = entry.profilePicture;\n                                img.alt = entry.username;\n                                profileDiv.appendChild(img);\n                            } else {\n                                const placeholder = document.createElement('div');\n                                placeholder.className = 'leaderboard-profile-placeholder';\n                                placeholder.textContent = '\ud83d\udc64';\n                                profileDiv.appendChild(placeholder);\n                            }\n                            \n                            // Info\n                            const infoDiv = document.createElement('div');\n                            infoDiv.className = 'leaderboard-info';\n                            const usernameP = document.createElement('p');\n                            usernameP.className = 'leaderboard-username';\n                            usernameP.textContent = entry.username || 'Anonymous';\n                            infoDiv.appendChild(usernameP);\n                            \n                            // Score (formatted time)\n                            const scoreDiv = document.createElement('div');\n                            scoreDiv.className = 'leaderboard-score';\n                            scoreDiv.textContent = formatTime(entry.score);\n                            \n                            entryDiv.appendChild(rankDiv);\n                            entryDiv.appendChild(profileDiv);\n                            entryDiv.appendChild(infoDiv);\n                            entryDiv.appendChild(scoreDiv);\n                            \n                            leaderboardList.appendChild(entryDiv);\n                        });\n                    } else {\n                        leaderboardList.innerHTML = `\n                            <div class=\"leaderboard-empty\">\n                                <div class=\"leaderboard-empty-icon\">\ud83c\udfc5</div>\n                                <div class=\"leaderboard-empty-text\">No entries yet. Be the first to complete a level!</div>\n                            </div>\n                        `;\n                    }\n                } catch (error) {\n                    lib.log(`Failed to load leaderboard: ${error.message}`);\n                    leaderboardList.innerHTML = `\n                        <div class=\"leaderboard-empty\">\n                            <div class=\"leaderboard-empty-icon\">\u26a0\ufe0f</div>\n                            <div class=\"leaderboard-empty-text\">Failed to load leaderboard. Please try again.</div>\n                        </div>\n                    `;\n                }\n            }\n\n            // Close leaderboard\n            function closeLeaderboard() {\n                leaderboardOverlay.style.display = 'none';\n            }\n\n            // Leaderboard button handlers\n            leaderboardBtn.addEventListener('click', openLeaderboard);\n            leaderboardCloseBtn.addEventListener('click', closeLeaderboard);\n\n            // Show game parameters\n            lib.showGameParameters({\n                name: 'Tangram Settings',\n                params: {\n                    'Snap Sensitivity': {\n                        key: 'snapSensitivity',\n                        type: 'slider',\n                        min: 10,\n                        max: 50,\n                        step: 5\n                    },\n                    'Large Triangle 1': {\n                        key: 'pieceColors.largeTriangle1',\n                        type: 'color'\n                    },\n                    'Large Triangle 2': {\n                        key: 'pieceColors.largeTriangle2',\n                        type: 'color'\n                    },\n                    'Medium Triangle': {\n                        key: 'pieceColors.mediumTriangle',\n                        type: 'color'\n                    },\n                    'Small Triangle 1': {\n                        key: 'pieceColors.smallTriangle1',\n                        type: 'color'\n                    },\n                    'Small Triangle 2': {\n                        key: 'pieceColors.smallTriangle2',\n                        type: 'color'\n                    },\n                    'Square': {\n                        key: 'pieceColors.square',\n                        type: 'color'\n                    },\n                    'Parallelogram': {\n                        key: 'pieceColors.parallelogram',\n                        type: 'color'\n                    }\n                }\n            });\n\n            // Start game (hide title screen)\n            function startGame() {\n                titleScreenActive = false;\n                titleScreen.classList.add('hidden');\n                titleScreen.style.display = 'none';\n                startTimer();\n                \n                // Resume audio context\n                if (audioContext && audioContext.state === 'suspended') {\n                    audioContext.resume();\n                }\n            }\n\n            // Play button handler\n            playBtn.addEventListener('click', startGame);\n\n            // Initialize title screen first\n            if (mode === 'play') {\n                // Show loading screen while assets load\n                await preloadTitleAssets();\n                // Hide loading screen after title assets are ready\n                loadingScreen.classList.add('hidden');\n            } else {\n                // In edit mode, skip loading and title screens\n                loadingScreen.style.display = 'none';\n                titleScreen.style.display = 'none';\n                titleScreenActive = false;\n            }\n\n            // Initialize\n            await preloadAssets();\n            initializePieces();\n            buildLevelSelector();\n            \n            // Set initial level indicator and guide\n            const initialLevel = LEVELS[currentLevelIndex];\n            levelIndicator.textContent = `Level ${initialLevel.id}: ${initialLevel.name}`;\n            updateGuidePanel();\n            \n            // Start the timer only if not showing title screen\n            if (!titleScreenActive) {\n                startTimer();\n            }\n\n            // Apply background color\n            function applyBackground() {\n                gameWorld.style.backgroundColor = window.gameConfig.backgroundColor || '#E8E8E8';\n            }\n            applyBackground();\n\n            // Apply button images\n            function applyButtonImages() {\n                const submitAsset = lib.getAsset('submit_button');\n                if (submitAsset && submitAsset.url) {\n                    submitBtn.style.backgroundImage = `url('${submitAsset.url}')`;\n                }\n                \n                const resetAsset = lib.getAsset('reset_button');\n                if (resetAsset && resetAsset.url) {\n                    resetBtn.style.backgroundImage = `url('${resetAsset.url}')`;\n                }\n                \n                const collectionAsset = lib.getAsset('collection_button');\n                if (collectionAsset && collectionAsset.url) {\n                    collectionBtn.style.backgroundImage = `url('${collectionAsset.url}')`;\n                }\n                \n                const leaderboardAsset = lib.getAsset('leaderboard_button');\n                if (leaderboardAsset && leaderboardAsset.url) {\n                    leaderboardBtn.style.backgroundImage = `url('${leaderboardAsset.url}')`;\n                }\n                \n                const playAsset = lib.getAsset('play_button');\n                if (playAsset && playAsset.url) {\n                    playBtn.style.backgroundImage = `url('${playAsset.url}')`;\n                }\n            }\n            applyButtonImages();\n\n            // Edit mode setup\n            if (mode === 'edit') {\n                controlButtons.style.display = 'none';\n                levelSelector.style.display = 'none';\n                collectionBtn.style.display = 'none';\n                leaderboardBtn.style.display = 'none';\n                timerDisplay.style.display = 'none';\n                guidePanel.style.display = 'none';\n                timerRunning = false; // Don't run timer in edit mode\n                // In edit mode, pieces are visible but not interactive\n            }\n\n            // Game loop\n            function gameLoop(timestamp) {\n                const deltaTime = Math.min((timestamp - lastTime) / 1000, 0.11);\n                lastTime = timestamp;\n                \n                // Clear per-frame caches\n                clearFrameCaches();\n\n                // Clear canvas\n                ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n                // Scale to fit\n                const scaleX = canvas.width / CANVAS_WIDTH;\n                const scaleY = canvas.height / CANVAS_HEIGHT;\n                const scale = Math.min(scaleX, scaleY);\n                \n                ctx.save();\n                ctx.scale(scale, scale);\n\n                // Draw background\n                if (assetCache['wood_background']) {\n                    // Draw wood background image to fill canvas\n                    const bgImg = assetCache['wood_background'];\n                    // The image is 1080x1920, canvas is 720x1280\n                    // Scale to cover the entire canvas while maintaining aspect ratio\n                    const imgAspect = bgImg.width / bgImg.height;\n                    const canvasAspect = CANVAS_WIDTH / CANVAS_HEIGHT;\n                    \n                    let drawWidth, drawHeight, drawX, drawY;\n                    if (imgAspect > canvasAspect) {\n                        // Image is wider, fit to height\n                        drawHeight = CANVAS_HEIGHT;\n                        drawWidth = drawHeight * imgAspect;\n                        drawX = (CANVAS_WIDTH - drawWidth) / 2;\n                        drawY = 0;\n                    } else {\n                        // Image is taller, fit to width\n                        drawWidth = CANVAS_WIDTH;\n                        drawHeight = drawWidth / imgAspect;\n                        drawX = 0;\n                        drawY = (CANVAS_HEIGHT - drawHeight) / 2;\n                    }\n                    \n                    ctx.drawImage(bgImg, drawX, drawY, drawWidth, drawHeight);\n                } else {\n                    // Fallback to solid color\n                    ctx.fillStyle = window.gameConfig.backgroundColor || '#E8E8E8';\n                    ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);\n                }\n\n                // Draw target area\n                drawTargetArea();\n\n                // Guide is now in the HTML UI\n\n                // Draw pieces\n                for (let i = 0; i < pieces.length; i++) {\n                    const isSelected = i === selectedPieceIndex;\n                    const isDraggingThis = isSelected && isDragging;\n                    const isHovered = i === hoveredPieceIndex;\n                    drawPiece(pieces[i], isSelected, isDraggingThis, isHovered);\n                }\n\n                // Draw confetti if won\n                if (gameWon) {\n                    updateConfetti(deltaTime);\n                    drawConfetti();\n                }\n\n                ctx.restore();\n                \n                // Update timer display (throttled to every 16 frames ~1 second)\n                frameCount++;\n                if (frameCount >= 16) {\n                    frameCount = 0;\n                    updateTimerDisplay();\n                }\n\n                requestAnimationFrame(gameLoop);\n            }\n\n            requestAnimationFrame(gameLoop);\n        }\n    </script>\n</body>\n</html>", "game_config": "{\n  \"pieceColors\": {\n    \"largeTriangle1\": \"#2D5016\",\n    \"largeTriangle2\": \"#F4C430\",\n    \"mediumTriangle\": \"#6B4C9A\",\n    \"smallTriangle1\": \"#DC143C\",\n    \"smallTriangle2\": \"#4169E1\",\n    \"square\": \"#8B4513\",\n    \"parallelogram\": \"#FF8C00\"\n  },\n  \"backgroundColor\": \"#E8E8E8\",\n  \"frameStyle\": \"classic\",\n  \"snapSensitivity\": 25,\n  \"currentLevel\": 1,\n  \"completedLevels\": []\n}", "game_design_document": "## Core Game Loop and Mechanics\n\n**Moment-to-Moment Gameplay:**\nThe player's core experience revolves around spatial reasoning and precise manipulation of seven geometric pieces (tans). Players will:\n1. Select a piece from the scattered pool at the bottom of the screen\n2. Drag it toward the target outline area\n3. Rotate the piece in 45\u00b0 increments to find the correct orientation\n4. Flip the parallelogram when needed (this piece uniquely requires mirroring)\n5. Position pieces carefully, utilizing snap-to-grid assistance for precise alignment\n6. Observe how pieces fit together, adjusting as needed\n7. Experience satisfaction when pieces lock into place with visual/audio feedback\n\n**Progression System:**\n- **Level 1**: Complete the large square outline (tutorial-level difficulty)\n- **Level 2**: Form the Camel silhouette (moderate difficulty)\n- **Level 3**: Form the Bear silhouette (moderate-hard difficulty)\n- **Level 4**: Form the Bird silhouette (hard difficulty)\n\n**Win Condition:**\nAll seven pieces must be placed within the target outline with:\n- No overlapping pieces\n- No gaps in the outline\n- All pieces used exactly once\n\n**Engagement Drivers:**\n- Spatial puzzle satisfaction from finding the \"aha!\" solution\n- Progressive difficulty that builds mastery\n- Clean, tactile interaction that feels like physical puzzle pieces\n- Visual and audio feedback that rewards correct placement\n- Replayability through multiple silhouette challenges\n\n## Starting Scope\n\n**First Implementation Must Include:**\n1. **Level 1 Only**: The square outline puzzle as the complete, polished experience\n2. **All Seven Pieces**: Correctly proportioned tans with the exact color scheme\n3. **Core Controls**: \n   - Drag and drop for all pieces\n   - Rotation buttons (45\u00b0 increments) that appear when a piece is selected\n   - Flip button for the parallelogram only\n4. **Snap Mechanics**: Pieces snap to align with each other and the outline border when within threshold distance\n5. **Win Detection**: Accurate detection when the square is completely filled\n6. **Victory Screen**: Celebratory animation and \"SQUARE COMPLETED!\" message\n7. **Visual Polish**: \n   - Beveled, raised appearance for pieces\n   - Wooden frame around the target outline\n   - Clean, minimal UI\n   - Smooth animations for dragging and snapping\n\n**Future Iterations Will Add:**\n- Levels 2-4 (animal silhouettes)\n- Level selection menu\n- Hint system\n- Reset/shuffle buttons\n- Sound effects for snapping and victory\n- Particle effects for completion\n- Progress saving\n\n## Game Perspective\n\n**Fixed-screen** perspective with a top-down view of the puzzle pieces and target outline. The entire game fits within the 720\u00d71280 viewport with no scrolling required.\n\n**Layout (Portrait 720\u00d71280):**\n- **Top Section (0-200px)**: Title \"Tangram Master\" and level indicator\n- **Middle Section (200-800px)**: Main play area containing the target outline (wooden-framed square)\n- **Bottom Section (800-1280px)**: Piece pool where all seven tans start in a scattered arrangement\n- **Overlay UI**: Rotation/flip controls appear near selected pieces\n\n## Game World and Level Design\n\n**Level 1: The Square (Starting Scope)**\n\nThe target outline is a large square (approximately 400\u00d7400px) centered in the middle section of the screen. This square has a decorative wooden frame border (similar to image_0.png) that is approximately 20px thick with a beveled, 3D appearance.\n\n**Initial Piece Positions:**\nAll seven pieces start scattered in the bottom section (piece pool area) in random orientations but not overlapping. They should be arranged so all are visible and accessible:\n- 2 large triangles (green, yellow)\n- 1 medium triangle (purple)\n- 2 small triangles (red, blue)\n- 1 square (brown)\n- 1 parallelogram (orange)\n\n**Piece Proportions (Critical):**\nUsing a base unit where the small triangle's legs = 1 unit:\n- Small triangle legs: 1 unit each\n- Small square sides: 1 unit each\n- Parallelogram: base = \u221a2 units, height = 1 unit\n- Medium triangle legs: \u221a2 units each\n- Large triangle legs: 2 units each\n\nWhen all pieces are correctly arranged, they form a perfect square with side length = 2 + \u221a2 units.\n\n**Target Outline Visualization:**\nThe wooden frame shows only the outer boundary of the square. The interior is semi-transparent (light gray with 30% opacity) so players can see pieces underneath as they work.\n\n## Edit Mode Design\n\n**Editable Properties:**\n1. **Piece Colors**: Users can change the color of each individual tan\n   - Color picker for each of the 7 pieces\n   - Default colors: Green, Yellow, Purple, Red, Blue, Brown, Orange\n   - Constraint: Colors must have sufficient contrast with the background\n\n2. **Background Style**: Users can select from preset background options\n   - Light Gray (default)\n   - Cream\n   - Light Blue\n   - Dark Wood Texture\n   - Constraint: Must maintain visibility of all pieces\n\n3. **Frame Style**: Users can toggle between frame styles\n   - Classic Wood (default, from image_0.png)\n   - Modern Minimal (thin black line)\n   - Gold Ornate\n   - No Frame\n\n4. **Snap Sensitivity**: Adjustable snap distance\n   - Range: 10-50 pixels\n   - Default: 25 pixels\n   - Helps players customize difficulty\n\n**Edit Mode UI:**\nA gear icon in the top-right corner opens an overlay panel with these customization options. Changes apply immediately and are stored in window.gameConfig.\n\n## Control Design\n\n**Desktop Controls:**\n- **Mouse Click + Drag**: Select and move pieces\n- **Left Click on Piece**: Select piece (shows rotation/flip controls)\n- **Rotation Buttons**: Click \"\u21b6\" or \"\u21b7\" buttons that appear near selected piece\n- **Flip Button**: Click \"\u21c4\" button (appears only for parallelogram)\n- **Click Empty Space**: Deselect current piece\n\n**Mobile Touch Controls:**\n- **Touch + Drag**: Select and move pieces\n- **Tap on Piece**: Select piece (shows rotation/flip controls)\n- **Tap Rotation Buttons**: Large touch-friendly buttons (96\u00d796px minimum) appear near selected piece\n  - \"\u21b6 45\u00b0\" button (rotate counter-clockwise)\n  - \"\u21b7 45\u00b0\" button (rotate clockwise)\n- **Tap Flip Button**: \"\u21c4 Flip\" button (96\u00d796px, appears only for parallelogram)\n- **Tap Empty Space**: Deselect current piece\n\n**Control Buttons Positioning:**\nWhen a piece is selected, control buttons appear in a semi-circular arc above the piece (if space allows) or below it. Buttons are positioned to avoid overlapping the target outline area and remain within the viewport.\n\n**Piece Movement Behavior:**\n- Pieces can be dragged anywhere on screen\n- Pieces have a subtle drop shadow when being dragged\n- Pieces snap to alignment when within threshold distance of:\n  - Other piece edges\n  - Target outline borders\n  - Grid positions within the target area\n- Snapping provides haptic feedback on mobile (if supported) and visual pulse effect\n- Pieces can overlap during placement but win detection requires no overlaps\n\n**Rotation Behavior:**\n- All pieces rotate around their geometric center\n- Rotation is always in 45\u00b0 increments (8 possible orientations)\n- Rotation is animated smoothly over 150ms\n- Current rotation angle is tracked for each piece\n\n**Flip Behavior (Parallelogram Only):**\n- Flips horizontally (mirror image)\n- Animated over 200ms with a 3D flip effect\n- Can be flipped in any rotation state\n\n## Art Style and Visual Theme\n\n**Overall Style: Polished Digital Wooden Blocks**\n\nThe game should evoke the tactile feel of physical wooden Tangram sets while maintaining a clean, modern digital aesthetic.\n\n**Color Palette:**\n- **Piece Colors** (from image_0.png): \n  - Large Triangle 1: Forest Green (#2D5016)\n  - Large Triangle 2: Golden Yellow (#F4C430)\n  - Medium Triangle: Deep Purple (#6B4C9A)\n  - Small Triangle 1: Crimson Red (#DC143C)\n  - Small Triangle 2: Royal Blue (#4169E1)\n  - Square: Rich Brown (#8B4513)\n  - Parallelogram: Bright Orange (#FF8C00)\n\n- **Background**: Light neutral gray (#E8E8E8)\n- **Frame**: Dark wood brown (#3E2723) with lighter highlights (#5D4037)\n- **UI Elements**: Dark gray (#424242) with white text\n\n**Visual Treatment for Pieces:**\n- **3D Beveled Effect**: Each piece has a subtle raised edge (2-3px bevel) using CSS or canvas gradients\n- **Top Surface**: Base color at full saturation\n- **Bevel Highlight**: Lighter shade (20% lighter) on top-left edges\n- **Bevel Shadow**: Darker shade (30% darker) on bottom-right edges\n- **Drop Shadow**: When dragging, pieces cast a soft shadow (4px blur, 20% opacity black)\n- **Selected State**: Pieces glow with a white outline (3px, 60% opacity) when selected\n\n**Wooden Frame:**\n- Thick border (20px) around the target outline\n- Textured appearance suggesting wood grain (can be achieved with CSS patterns or a generated texture)\n- Inner shadow to create depth\n- Corner decorations (small beveled squares) at each corner\n\n**UI Elements:**\n- **Title Text**: Large, bold, sans-serif font (48px) in dark gray\n- **Level Indicator**: \"Level 1: The Square\" in 24px font below title\n- **Control Buttons**: Rounded rectangles with icon symbols\n  - Background: Semi-transparent white (#FFFFFF 80%)\n  - Border: 2px solid dark gray\n  - Icons: Dark gray symbols (\u21b6, \u21b7, \u21c4)\n  - Size: 96\u00d796px for mobile touch targets\n\n**Animation Style:**\n- **Smooth Transitions**: All movements use ease-out timing (200-300ms)\n- **Snap Animation**: Quick pulse effect (scale 1.0 \u2192 1.05 \u2192 1.0 over 150ms)\n- **Victory Animation**: \n  - Pieces pulse with a golden glow\n  - Confetti particles (simple colored circles) fall from top\n  - \"SQUARE COMPLETED!\" text fades in with scale animation\n\n**Asset Sizes:**\n- Target outline square: 400\u00d7400px\n- Large triangles: ~283\u00d7283px (hypotenuse ~400px)\n- Medium triangle: ~200\u00d7200px (hypotenuse ~283px)\n- Small triangles: ~141\u00d7141px (hypotenuse ~200px)\n- Small square: ~141\u00d7141px\n- Parallelogram: ~200\u00d7141px\n\nAll measurements are approximate and should be calculated precisely based on the mathematical proportions described in the Level Design section.", "persistent_storage_config": {"leaderboard": {"sort_ascending": true}}, "game_metadata": {"game_id": "01KQK4HRVX5G16J0WRRA3KJ33C", "title": "Tangram Master", "description": "Arrange seven classic geometric shapes to perfectly fit various outlines, from squares to intricate animal silhouettes!", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_bfd0f34b-8f49-43af-a158-30bd531582cf.webp", "created_at": 1777685042, "updated_at": 1777772595584.9897, "art_style": null}}