{"asset_map": {"kitchen_background": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/kitchen_background_5b3f654c-0041-4e58-bbb7-0fc4cb8c2e4a.webp", "type": "image", "prompt": "Generate a 16:9 wide background image of a polished, dimensional 3D CGI home kitchen scene. Based on the composition of image 1, render wooden countertops, cream cabinets, and cookware with realistic depth and soft ambient occlusion. Use refined materials with a palette of warm wood, muted rose, and champagne-gold. The scene is bathed in gentle golden morning light from a window. Calm, inviting, and uncluttered professional game environment style, high resolution, soft focus on distant details, no text or people.", "description": "A polished 3D CGI version of the cozy kitchen background with golden morning light and elegant materials.", "subtype": "background", "aspect_ratio": [1264, 848], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "digital_scale": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/digital_scale_7a916ef8-a524-4f42-a452-b95346bf65b6.webp", "type": "image", "prompt": "Generate a premium high-quality 3D CGI game prop of a digital kitchen scale. Maintain the exact silhouette and design of the scale in image 1, featuring a cream-colored platform and a digital display showing '0g'. Use a warm ivory and champagne-gold color palette with realistic dimensional edges. The material should show subtle brushed metal and polished ceramic textures with soft contact shadows. Clean, readable digital font. Isolated asset on a transparent background, professional studio lighting, high resolution, 4k detail.", "description": "A premium 3D CGI digital kitchen scale in warm ivory and champagne gold, matching the existing scale's form.", "subtype": "object", "aspect_ratio": [982, 824], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "mixing_bowl_large": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mixing_bowl_large_84c9da6a-616a-4e8b-b504-d8cf91ba6e3d.webp", "type": "image", "prompt": "Generate a high-end 3D CGI render of a large stainless-steel mixing bowl, viewed from a slight top-down angle. The bowl should feature realistic metal thickness, a smooth curved rim, and sophisticated studio lighting with soft highlights and clean reflections. Professional patisserie quality with subtle ambient occlusion and a gentle grounding shadow. Style: Grand Luxe, crisp and dimensional. Maintain the exact silhouette and perspective of the existing bowl in image 1. Isolated object on a transparent background, no ingredients or extra props.", "description": "A premium 3D CGI stainless steel mixing bowl with professional studio lighting and realistic metallic reflections.", "subtype": "object", "aspect_ratio": [1113, 840], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "electric_mixer": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/electric_mixer_027f4194-757a-4cdd-b228-dff0361e342b.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI game prop of a handheld electric mixer. The mixer should have a cream-colored molded body with refined surfaces, visible bevels, and subtle champagne-gold accents. It features realistic polished metal beaters with soft specular highlights. Maintain the exact silhouette and view of image 1. The object should have a soft contact shadow, isolated on a transparent background, with no hands, no text, and no extra props. Professional 3D render, high resolution, clean studio lighting.", "description": "A polished 3D CGI handheld electric mixer with cream body and metal beaters.", "subtype": "object", "aspect_ratio": [828, 1264], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cake_pan_empty": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cake_pan_empty_10c443e7-55d8-4be9-938b-775152c5b7cc.webp", "type": "image", "prompt": "Generate a premium 3D CGI object of an empty round metal cake baking tin. Use a slight top-down perspective to show the interior and believable wall thickness with a rolled rim. The material should be brushed metal with subtle champagne reflections and an elegant patisserie finish. Ensure professional lighting, clear perspective depth, and a soft grounding shadow. Maintain an isolated composition on a transparent background. Match the perspective and form of image 1 but upgrade to a realistic 3D render style.", "description": "A premium 3D CGI empty round metal cake tin with brushed metal reflections and perspective depth.", "subtype": "object", "aspect_ratio": [1024, 872], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "oven_closed": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/oven_closed_fcd5a5cd-a21c-4411-9fa0-ece40fca79c7.webp", "type": "image", "prompt": "Transform the oven in image 1 into a premium 3D CGI game prop. Maintain the original cream-colored porcelain-enamel design, closed glass door, and control dials. Enhance with distinct dimensional form, crisp bevels, realistic glass reflections, and refined champagne-gold metal accents. Soft ambient shadows, high-quality 3D render style, isolated on a transparent background, professional game asset quality, front-perspective view.", "description": "A premium 3D CGI version of the cream-colored kitchen oven with refined metal details.", "subtype": "object", "aspect_ratio": [788, 799], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "oven_open": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/oven_open_784d40ef-68ac-435f-937e-ee0a5867ce9b.webp", "type": "image", "prompt": "Transform the existing oven in image 1 into a premium 3D CGI game prop. An open modern cream-colored oven with a clean enamel finish, reflective metal racks, and a warm golden interior glow. High-quality 3D render, polished surfaces, realistic lighting and depth, soft highlights on the glass door. Isolated on a transparent background, front-angled view, no food or characters, matching a Grand Luxe patisserie aesthetic.", "description": "Premium 3D CGI open oven with a warm interior glow and metallic racks.", "subtype": "object", "aspect_ratio": [1021, 941], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cooling_rack": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cooling_rack_482f3078-e3ea-44c6-9edd-866d73c879f5.webp", "type": "image", "prompt": "Generate a premium 3D CGI render of a rectangular wire cooling rack, isolated on a transparent background. Each wire is visibly rounded and dimensional with accurate perspective. Use a refined stainless steel or champagne metallic finish with subtle reflections and soft self-shadowing. High-quality bakery prop aesthetic, clean and polished, no extra objects or baked goods.", "description": "A premium 3D CGI stainless steel wire cooling rack with realistic metallic reflections and depth.", "subtype": "object", "aspect_ratio": [1264, 780], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "long_knife": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/long_knife_699e7580-cd03-433a-aef7-8d698518e7d6.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI prop of a long serrated bread knife. The blade is made of polished stainless steel with subtle realistic reflections and clearly visible serrations. The handle is made of warm, tactile wood with a visible grain and slight bevels. Match the shape and side-view angle of image 1. Professional studio lighting with soft shading, isolated on a transparent background, no hands, no food, no text, clean and polished game-ready asset.", "description": "A 3D CGI rendered version of the long serrated bread knife with realistic textures.", "subtype": "object", "aspect_ratio": [806, 1263], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "offset_spatula": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/offset_spatula_7c56543f-0584-4bca-91ca-004c3d9d601a.webp", "type": "image", "prompt": "Generate a professional 3D CGI offset spatula, isolated object on a transparent background. The tool features a thin, flexible brushed metal blade with a distinct offset bend and a refined warm-toned wooden handle. High-end patisserie aesthetic with elegant Grand Luxe finish, subtle metallic reflections, and soft dimensional shadows. Clean studio lighting, sharp focus, 4k resolution, no hands or extra props.", "description": "A polished 3D CGI offset spatula with a metal blade and wooden handle for spreading frosting.", "subtype": "object", "aspect_ratio": [1021, 1011], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "flour_bag": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/flour_bag_40425c8e-53f1-4e35-8591-170adf5bea10.webp", "type": "image", "prompt": "Transform the flour bag from image 1 into a premium 3D CGI baking ingredient prop. Create a high-quality 3D render of an open paper bag with realistic folded tops, visible paper thickness, soft creases, and tactile matte shading. Keep the sharp and legible English 'Flour' label from the original. Use warm ivory paper tones and gentle, soft shadows. Maintain a clean front-view, isolated composition on a transparent background. High resolution, professional studio lighting, 3D animated movie style, no hands, no extra objects.", "description": "Premium 3D CGI version of the flour bag with realistic textures and soft lighting.", "subtype": "object", "aspect_ratio": [901, 1022], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "sugar_bag": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/sugar_bag_de345924-8339-4c1e-83d1-2e8c1230f5d5.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI render of an open paper bag of sugar. The bag should have the English word 'Sugar' printed clearly on the front in a clean font. Use realistic paper textures with subtle folds, depth, and a slight visible sugar texture at the top opening. Match the premium 'Grand Luxe' style with soft studio lighting, realistic shadows, and a cozy warm aesthetic. Isolated on a transparent background, slight top-down perspective, no hands or extra objects.", "description": "A premium 3D CGI rendered open paper bag of sugar with realistic textures and 'Sugar' label.", "subtype": "object", "aspect_ratio": [902, 1021], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "eggs_carton": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/eggs_carton_cf6122d6-8d48-43ee-aab5-af8b5ac88e30.webp", "type": "image", "prompt": "An elegant 3D CGI render of an open cardboard egg carton containing six white eggs, arranged in two rows of three. High-quality 3D model with realistic paper pulp texture on the carton and smooth matte shells on the eggs. Soft contact shadows, warm studio lighting, 3/4 top-down view. Professional 3D asset, clean isolated composition on a transparent background, matching the style of the electric_mixer and kitchen_background_french_realistic.", "description": "A 3D CGI rendered open cardboard egg carton with six white eggs.", "subtype": "object", "aspect_ratio": [842, 843], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "milk_bottle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/milk_bottle_2d206584-0149-4a8a-af76-29ba85f39e93.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI game prop of the milk bottle from image 1. Maintain the exact shape, front view, and 'Milk' English label. Transform it with professional textures: translucent glass or high-quality plastic material, visible dimensional white milk inside, crisp specular reflections, and soft contact shadows. Use a refined ivory and subtle champagne-gold accent color palette. Professional studio lighting, clean edges, isolated object on a transparent background, high resolution, 3D render style.", "description": "A polished 3D CGI milk bottle with a readable 'Milk' label and realistic material properties.", "subtype": "object", "aspect_ratio": [511, 1248], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "butter_block": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/butter_block_1b6d3fed-5120-4890-bddf-8f63103ac22a.webp", "type": "image", "prompt": "A premium 3D CGI rendered block of pale yellow butter, partially unwrapped. The wrapper is realistic with visible thickness and folds, featuring the English text 'Butter' in a clean font. Realistic tactile shading, soft contact shadow on a neutral surface. High-resolution, professional studio lighting, polished game asset quality, maintaining the same orientation and shape as the original butter_block.", "description": "Premium 3D CGI block of butter with an English label, matching the realistic kitchen style.", "subtype": "object", "aspect_ratio": [1177, 841], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "baking_powder_container": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/baking_powder_container_579c104a-ea1e-4dc2-b0eb-a1490bbcc534.webp", "type": "image", "prompt": "Generate a premium 3D CGI prop of a small baking powder container. Based on the style and label of image 1, create a highly detailed version with a dimensional lid, subtle metallic and plastic material reflections, and a crisp readable label. The aesthetic should be warm Grand Luxe patisserie style with gentle highlights and a soft contact shadow. Isolated composition, front view, transparent background, professional quality, high resolution.", "description": "Premium 3D CGI baking powder container with a polished patisserie aesthetic.", "subtype": "object", "aspect_ratio": [783, 1109], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "raw_batter": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/raw_batter_49a42ff0-1a6d-4bc7-8727-6d8cb5557bc8.webp", "type": "image", "prompt": "Generate a dimensional and appetizing 3D CGI surface of smooth, liquid pale yellow cake batter. The texture should have gentle glossy highlights, soft rounded folds, and subtle depth to look creamy and realistic. Maintain a pale vanilla color. Close-up view, high resolution, professional 3D render, no bowl, no utensils, no text, no extra ingredients, clean composition.", "description": "Dimensional and glossy 3D CGI vanilla cake batter surface.", "subtype": "other", "aspect_ratio": [1264, 843], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cake_baking_stage1": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cake_baking_stage1_92b811f7-8c67-4406-90e1-f7f01ccc17a6.webp", "type": "image", "prompt": "Generate a detailed 3D CGI game asset of a round cake in a metal baking pan at the early baking stage. The cake should be pale and just beginning to rise, with a subtle sponge texture visible. Include realistic dimensional reflections on the metal tin, warm oven light casting soft shadows, and an appetizing, polished finish. Maintain a slight top-down angle consistent with the existing game perspective. No props, no text, and do not show it as fully baked.", "description": "A 3D CGI rendered round cake in a pan at the early pale rising stage of baking.", "subtype": "object", "aspect_ratio": [1024, 873], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cake_baking_stage2": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cake_baking_stage2_728b3693-fcf3-4976-bc1f-301596be3422.webp", "type": "image", "prompt": "Create a high-quality 3D CGI rendered image of a round cake in a metal baking pan at the mid-baking stage. The cake should be golden and risen with a realistic sponge texture and soft warm highlights. The pan should have dimensional metallic walls and realistic shadows. Match the composition and perspective of image 1, but transform the style into a polished, professional 3D game asset. No text or extra props.", "description": "A 3D CGI rendered round cake in a pan at the mid-bake stage, showing a golden risen surface.", "subtype": "object", "aspect_ratio": [1024, 1024], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cake_baking_stage3": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cake_baking_stage3_1fa70485-17c0-44d6-a1dc-ba656e29cd82.webp", "type": "image", "prompt": "Generate a premium 3D CGI game asset of a fully baked golden-brown sponge cake in a round metal pan. The cake should have a realistic sponge crumb texture and a perfectly browned top, matching the framing of image 1. Use warm patisserie lighting with soft contact shadows. The style should be high-quality and appetizing, maintaining a slight top-down angle consistent with a modern kitchen game. No decorations, no text, and no other objects in the frame, transparent background.", "description": "A premium 3D CGI fully baked golden-brown sponge cake in a round baking pan.", "subtype": "object", "aspect_ratio": [1024, 872], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cake_layer_bottom": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cake_layer_bottom_23366dc4-444f-4ed9-8692-a105dbad8c21.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI bakery asset of a single bottom layer of a sponge cake. The cake should have a flat cut top surface revealing an airy, porous golden crumb texture. The sides should show natural baked edges. Use soft studio lighting with subtle shading to create dimension. Match the shape and perspective of image 1, but transformed into a polished 3D render. Isolated on a transparent background, no frosting, no text, no extra props.", "description": "A detailed 3D CGI bottom sponge cake layer showing a porous golden crumb and flat cut surface.", "subtype": "object", "aspect_ratio": [1024, 820], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cake_layer_top": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cake_layer_top_1b543192-4b8b-458d-952f-1b4abe503fed.webp", "type": "image", "prompt": "Generate a premium 3D CGI rendered top layer of a sponge cake, featuring a smooth golden-brown rounded top and visible delicate airy crumb texture on the cut bottom. The asset should have dimensional sides, soft warm highlights, and high-quality realistic textures. Side view, isolated on a transparent background, no frosting, no decorations, no text. Match the perspective and shape of image 1 but transform it into a highly detailed realistic style.", "description": "A realistic, premium 3D CGI top layer of a sponge cake with a golden rounded top and detailed crumb texture.", "subtype": "object", "aspect_ratio": [1023, 829], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "whipped_cream_bowl": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/whipped_cream_bowl_7531e505-c754-40af-9c36-940f10447a89.webp", "type": "image", "prompt": "Transform the existing whipped_cream_bowl into a premium 3D CGI version. High-quality 3D render of a cream-colored ceramic bowl with a glossy dimensional glaze and defined rim, filled with fluffy white whipped cream showing realistic airy swirls, soft peaks, and delicate highlights. Use warm patisserie studio lighting to create soft shadows and depth. Isolated composition from a slight top-down angle, high resolution, professional digital art, transparent background.", "description": "A premium 3D CGI cream-colored bowl filled with fluffy white whipped cream.", "subtype": "object", "aspect_ratio": [999, 957], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "strawberry_whole": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/strawberry_whole_766b9c22-c39c-41d0-8879-c7141cfee06f.webp", "type": "image", "prompt": "Generate a polished, appetizing 3D CGI ripe red strawberry with green leaves. Transform the existing design into a high-fidelity food model while preserving its silhouette and vibrant red color. Add realistic seed dimples, glossy fresh skin reflecting soft light, and dimensional green leaves with visible veins. Include a soft grounding shadow beneath it. High resolution, professional food photography style, isolated on a transparent background, no extra objects or text.", "description": "A polished 3D CGI ripe strawberry topping with realistic textures.", "subtype": "object", "aspect_ratio": [843, 1168], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "strawberry_half": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/strawberry_half_67a2ac32-7525-40ee-a230-b4884a8aac66.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI render of a strawberry cut in half, showcasing a juicy dimensional pink interior with realistic seeds. The outer skin should be a vibrant glossy red with fresh green leaf details at the top. Use soft studio lighting with delicate highlights to emphasize the texture. Isolated on a transparent background, top-down perspective, matching the style of the existing kitchen tools.", "description": "A realistic 3D CGI sliced strawberry showing the juicy interior for cake decoration.", "subtype": "object", "aspect_ratio": [1216, 843], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "blueberry": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/blueberry_16c33d75-9b53-402b-8d4b-53e070f1c095.webp", "type": "image", "prompt": "Generate a single, realistic deep-blue blueberry as a premium 3D CGI cake topping. The berry should be round with a natural white bloom, featuring dimensional surface texture, soft realistic highlights, and a gentle shadow. High-resolution, professional quality, isolated on a transparent background, no leaves or props.", "description": "A crisp, premium 3D CGI blueberry topping with realistic texture and soft highlights.", "subtype": "object", "aspect_ratio": [737, 729], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "chocolate_shaving": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/chocolate_shaving_e633d046-647f-4fc7-9315-e406a6509028.webp", "type": "image", "prompt": "Generate a single, high-quality 3D CGI delicate dark chocolate curl. The shaving should have a realistic thickness, a smooth chocolate sheen with subtle highlights, and a soft professional shadow. Match the dark chocolate color and curl shape of image 1. Isolated on a transparent background, professional patisserie quality, 8k resolution, highly detailed textures.", "description": "A finely detailed 3D CGI dark chocolate curl for patisserie decoration.", "subtype": "object", "aspect_ratio": [1024, 918], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "sugar_pearl_pink": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/sugar_pearl_pink_76567c77-826e-43ca-b827-6020b30e5aaa.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI render of a single, small pink edible sugar pearl. The pearl should be perfectly round with a smooth, pearlescent finish and a soft specular highlight. Use a pale pink color palette. The asset should be shown from a slight top-down perspective with a soft contact shadow underneath to give it dimension. Clean, polished, game-ready object on a transparent background.", "description": "A 3D CGI pink edible sugar pearl with a pearlescent shine for cake decoration.", "subtype": "object", "aspect_ratio": [908, 922], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "sugar_pearl_white": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/sugar_pearl_white_54392db5-9c0f-4ef1-be11-0a7cd07877ac.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI render of a single, small white edible sugar pearl. The pearl should have a perfect round shape, a smooth pearlescent surface with a subtle iridescent highlight, dimensional shading, and a soft contact shadow to give it depth. The style should be clean and polished, matching a premium 3D dessert simulation game. Isolated object on a transparent background, no text, no extra props.", "description": "A high-quality 3D CGI white edible sugar pearl with an iridescent shine for cake decoration.", "subtype": "object", "aspect_ratio": [738, 744], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "birthday_candle_unlit": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/birthday_candle_unlit_c5b6d67a-8300-4aeb-bc2f-9b3c86d54ea6.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI model of an unlit birthday candle. The candle features a pink and white spiral-striped pattern with dimensional spiral grooves and a realistic wax texture. Include a small, unlit cotton wick at the top. The lighting should feature soft studio highlights and subtle realistic shadows to give it depth. Side view, isolated object, transparent background, professional 3D render, polished finish, matching the refined style of existing kitchen props.", "description": "Refined 3D CGI pink-and-white spiral-striped unlit birthday candle.", "subtype": "object", "aspect_ratio": [275, 1264], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "birthday_candle_lit": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/birthday_candle_lit_9a7daece-6352-44d9-84fd-0dc072549f6b.webp", "type": "image", "prompt": "A premium 3D CGI game prop of a lit birthday candle, pink and white spiral-striped design. Features detailed dimensional wax grooves, a realistic soft warm flame glow, and subtle highlights. High-quality render with realistic textures, soft shadows, and a clean professional finish. Front view, standing illustration, isolated on a transparent background.", "description": "A premium 3D CGI lit pink-and-white spiral birthday candle with a realistic flame and warm glow.", "subtype": "object", "aspect_ratio": [281, 1220], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "completed_cake_base": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/completed_cake_base_c18edb0c-54c5-4aca-a18b-eb3cc89af679.webp", "type": "image", "prompt": "A high-quality 3D CGI render of a single-layer round sponge cake fully covered in smooth ivory-white cream frosting. The cake features a realistic spatula texture on the sides, soft professional lighting with gentle highlights, and a subtle grounding shadow. The view is a slight top-down perspective, isolated on a transparent background. Match the exact silhouette and proportions of image 1 but transform it into a photorealistic, elegant style suitable for a high-end dessert simulator.", "description": "An elegant, photorealistic 3D CGI single-layer round cake covered in smooth white ivory frosting.", "subtype": "object", "aspect_ratio": [856, 849], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "progress_bar_empty": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/progress_bar_empty_77290d05-67ba-483e-a756-4be195bb7f14.webp", "type": "image", "prompt": "Generate a polished 3D CGI UI graphic of an empty progress bar frame. The asset should feature a refined ivory enamel body with a subtle beveled depth and a very restrained, elegant champagne-gold metallic trim. The interior is a crisp, clean white, currently empty. Match the exact shape and proportions of image 1. Professional studio lighting with soft shadows, high-quality material rendering, game-ready UI element, transparent background.", "description": "A polished 3D CGI empty progress bar frame with ivory enamel and champagne-gold trim.", "subtype": "ui", "aspect_ratio": [1264, 323], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "progress_bar_fill": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/progress_bar_fill_7b121484-4ee5-415b-8bec-c6239b0c4281.webp", "type": "image", "prompt": "Generate a dimensional premium 3D CGI UI element of a progress bar fill. The shape is a horizontal capsule with rounded ends. Use a vibrant pink gradient, adding a subtle glossy bevel and soft dimensional highlights consistent with a luxury game interface. High-quality render, clean edges, professional studio lighting, isolated on a transparent background.", "description": "A premium 3D CGI pink progress bar fill with glossy highlights and bevels.", "subtype": "ui", "aspect_ratio": [1264, 321], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "checkmark_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/checkmark_icon_20953769-0cc6-4efd-9d32-fa1abb4dc16d.webp", "type": "image", "prompt": "Generate a clean dimensional 3D CGI UI icon of a cheerful green checkmark with rounded ends. Maintain the exact shape and silhouette of image 1. Add a subtle raised enamel finish, crisp edges, soft studio highlights, and a delicate shadow for depth. High-quality 3D render, isolated on a transparent background, professional game UI asset.", "description": "A dimensional 3D CGI green checkmark icon for UI feedback.", "subtype": "ui", "aspect_ratio": [1021, 940], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "star_particle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/star_particle_0fd06bea-dda6-4226-b16e-3a332910259e.webp", "type": "image", "prompt": "Generate a polished 3D CGI game effect of a small four-pointed yellow star. The star should have a soft, gentle glow and a delicate luminous highlight to provide dimensional depth while remaining lightweight. Maintain a clean four-point silhouette and vibrant yellow color. Isolated object on a transparent background, high resolution, professional game-ready VFX quality.", "description": "A polished 3D CGI four-pointed yellow star particle with a soft luminous glow.", "subtype": "effect", "aspect_ratio": [1024, 1019], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "heart_particle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/heart_particle_4b9e592d-e055-4281-9d29-2dd43ffb3d13.webp", "type": "image", "prompt": "Generate a small, clean, isolated 3D CGI pink heart with rounded dimensional depth and a soft luminous highlight. The heart should have a gentle outer glow, polished material finish, and a professional game-ready look. Match the pink tone of image 1. High resolution, transparent background, centered composition, no text or extra symbols.", "description": "A polished 3D CGI pink heart particle effect with a soft glow and rounded depth.", "subtype": "effect", "aspect_ratio": [844, 757], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "button_primary": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/button_primary_6cbda616-a7fc-4590-8137-bb0cc8bd07ee.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI rendered UI button, front view. The button should have a soft rounded rectangular shape in a warm welcoming pink color. Add dimensional beveled thickness, subtle glossy surface reflections, and a refined champagne-gold metallic edge detail. Ensure it has a soft drop shadow and a clean, polished appearance consistent with a 'Grand Luxe' edition game. Transparent background, no text, no icons, professional studio lighting. Match the pink color and form of image 1.", "description": "A premium 3D CGI pink primary UI button with champagne-gold edges and a glossy finish.", "subtype": "ui", "aspect_ratio": [1104, 529], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "button_secondary": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/button_secondary_e8543b81-24af-444c-bb18-0a7a8a832041.webp", "type": "image", "prompt": "Generate a premium dimensional 3D CGI UI button asset. The button should have a rounded rectangular form in a soft cream color with an elegant ivory finish. Add subtle beveled depth and a restrained champagne-gold trim around the edges. Professional studio lighting with a soft shadow underneath. No text, no icons, clean and polished game-ready UI element with a transparent background.", "description": "A premium 3D CGI cream-colored secondary UI button with an elegant ivory finish and champagne-gold trim.", "subtype": "ui", "aspect_ratio": [1237, 585], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "menu_panel": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/menu_panel_05dce490-762a-4e4b-aa24-071324cef5b8.webp", "type": "image", "prompt": "Generate a blank, high-quality 3D CGI UI menu panel. The panel is a large cream-colored surface with a luxurious ivory raised edge and a subtle champagne-gold inset border. Design with soft realistic depth, gentle shadows, and professional studio lighting. Keep the center clean and blank for game content. No text, icons, or extra objects. Match the 3D aesthetic of image 1.", "description": "A luxurious 3D CGI cream-colored UI menu panel with gold accents and realistic depth.", "subtype": "ui", "aspect_ratio": [1264, 802], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "click_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/click_sound_568fbf5a-426f-4505-8126-036ea95873ad.mp3", "type": "audio", "prompt": "Short, organic wooden click sound effect, gentle and muffled, soft ceramic tap, minimalist UI feedback, warm acoustic quality, 0.1 seconds.", "description": "A brief, warm wooden click sound for button interactions.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.46, "loop": false, "opus_url": null, "animation": null}, "select_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/select_sound_36b7a9bd-062b-4adb-a653-d7d5d69da68d.mp3", "type": "audio", "prompt": "Soft chime ding, light bell-like quality, high-pitched but gentle, cozy game UI sound, rewarding and clean, subtle resonance tail.", "description": "A gentle bell-like ding for item selection.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 0.98, "loop": false, "opus_url": null, "animation": null}, "step_complete_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/step_complete_sound_0650548a-5c5f-4f96-9138-368d7b0c5acd.mp3", "type": "audio", "prompt": "Ascending musical chime phrase, sparkling glockenspiel and soft bells, light 'ta-da' moment, celebratory and warm, 1 second duration, cozy atmosphere.", "description": "A cheerful ascending chime phrase for completing a baking step.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.8, "loop": false, "opus_url": null, "animation": null}, "pour_liquid_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/pour_liquid_sound_df01925a-82df-4b6d-8e0d-63a939acfa19.mp3", "type": "audio", "prompt": "Thick liquid pouring sound, viscous batter or cream flowing into a bowl, smooth continuous glug, low splashing, realistic culinary sound, satisfying and rich texture.", "description": "Smooth, thick batter pouring sound effect.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.04, "loop": false, "opus_url": null, "animation": null}, "mixer_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/mixer_sound_f9afde65-b98f-453e-b6e6-c0bf874e89d6.mp3", "type": "audio", "prompt": "Electric hand mixer motor hum, whirring beaters, steady mechanical sound, moderate pitch, realistic kitchen appliance, loopable, clean and consistent.", "description": "A steady, loopable electric hand mixer hum.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 4, "loop": true, "opus_url": null, "animation": null}, "oven_door_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/oven_door_sound_b4734047-99f8-4136-a896-c3d009994252.mp3", "type": "audio", "prompt": "Kitchen oven door opening with a slight metallic creak followed by a soft heavy thud closing, realistic domestic sound, gentle and sturdy.", "description": "Realistic sound of an oven door opening and closing.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.17, "loop": false, "opus_url": null, "animation": null}, "knife_cutting_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/knife_cutting_sound_ad48c7d3-e21f-4bb5-b3f1-b4a46912a2a0.mp3", "type": "audio", "prompt": "Soft knife cutting through spongy cake, gentle sawing motion, airy and fluffy texture sound, satisfying culinary slice, non-harsh, realistic.", "description": "Soft sawing sound of a knife cutting through a fluffy cake.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.46, "loop": false, "opus_url": null, "animation": null}, "spreading_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/spreading_sound_8e6a9110-ab5f-44db-9160-2fd30386955a.mp3", "type": "audio", "prompt": "Frosting spreading sound effect, smooth spatula moving over soft cake, creamy texture, rhythmic and gentle, like spreading butter, satisfying kitchen sound.", "description": "Soft sound of spreading cream or frosting with a spatula.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.72, "loop": false, "opus_url": null, "animation": null}, "place_decoration_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/place_decoration_sound_b292723b-a93c-4111-b060-f39869610a4e.mp3", "type": "audio", "prompt": "Playful soft boop sound, light bubble pop, gentle placement sound, organic and cute, brief 0.2s impact, cozy game aesthetic.", "description": "A playful, soft 'boop' for placing fruit or toppings.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 1.99, "loop": false, "opus_url": null, "animation": null}, "background_music": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/background_music_53e84b2d-2bc5-4a58-844a-f9bd3d6805e5.mp3", "type": "audio", "prompt": "Relaxing background music, gentle piano melody, soft legato strings, cozy home kitchen vibe, 70 BPM, C major, peaceful and nostalgic, loopable, high quality instrumental.", "description": "Calm, nostalgic piano and strings for the kitchen atmosphere.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 30, "loop": true, "opus_url": null, "animation": null}, "birthday_song": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/birthday_song_02161e8a-ee40-4ffe-98e9-0df861f9a7cc.mp3", "type": "audio", "prompt": "Instrumental Happy Birthday song, cheerful and light arrangement, piano and glockenspiel lead, acoustic guitar backing, upbeat but cozy, celebratory finale.", "description": "Light, instrumental birthday theme for the final celebration.", "subtype": "music", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 30, "loop": false, "opus_url": null, "animation": null}, "ambient_kitchen_sound": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/ambient_kitchen_sound_8cdf7954-7932-42ab-8f63-4f998f8de6f2.mp3", "type": "audio", "prompt": "Subtle kitchen ambience, distant wall clock ticking, faint hum of a refrigerator, soft air movement, peaceful and homey atmosphere, very low volume, loopable.", "description": "Subtle background room tone with a distant clock tick.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 30, "loop": true, "opus_url": null, "animation": null}, "kitchen_background_japanese": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/kitchen_background_japanese_95171dc9-c737-46e8-9a47-6c72e9115101.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI full-screen background of a minimalist Japanese dessert kitchen. Maintain the layout of image 1, featuring wooden countertops and a view of a peaceful Zen garden. The style should be dimensional with refined tactile materials like polished light wood and cream surfaces. Use soft global illumination and gentle morning light. The color palette should be 'Grand Luxe' with cream, natural wood, muted rose, and subtle champagne-gold accents. No text, no characters, 16:9 wide aspect ratio, professional architectural visualization style.", "description": "A polished 3D CGI version of the Japanese dessert kitchen with a Zen garden view, featuring soft morning light and premium materials.", "subtype": "background", "aspect_ratio": [1264, 848], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "kitchen_background_realistic": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/kitchen_background_realistic_8d012550-da67-431e-9661-a768ef59812b.webp", "type": "image", "prompt": "Transform the composition of image 1 into a high-end 3D-rendered CGI game environment. A sun-drenched modern home kitchen with realistic dimensional cabinetry, wooden countertops, and soft global illumination. The color palette should be elegant: cream, warm wood, muted rose, and subtle champagne-gold accents. Ensure clear depth of field and premium material textures like polished stone and smooth wood. High resolution, professional game cinematic quality, full-screen background, no text, no people, no UI elements. 16:9 aspect ratio.", "description": "A refined 3D-rendered CGI modern kitchen background with warm lighting and premium materials.", "subtype": "background", "aspect_ratio": [1376, 768], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "kitchen_background_japanese_realistic": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/kitchen_background_japanese_realistic_3c3f26ae-fbcb-4b22-a045-244ad43d88a4.webp", "type": "image", "prompt": "Generate a wide 16:9 full-screen game background of a crisp, elegant 3D CGI minimalist Japanese dessert kitchen. Preserve the composition of image 1, featuring a Zen garden view through a large window. Use highly dimensional architectural details with realistic textures for light oak wood, smooth stone, and shoji paper. Implement gentle global illumination and serene soft daylight. Use a premium Grand Luxe color palette of cream, natural wood, and restrained champagne-gold accents. Professional 3D render, high-end architectural visualization, no people, no text.", "description": "An elegant 3D CGI minimalist Japanese kitchen background with a Zen garden view.", "subtype": "background", "aspect_ratio": [1264, 848], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "kitchen_background_french_realistic": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/kitchen_background_french_realistic_066d1524-0a54-44b8-99f4-db8cbff293eb.webp", "type": "image", "prompt": "Transform the existing kitchen background into a polished 3D CGI scene. A professional French patisserie kitchen with high depth and dimension. Features luxurious ivory and cream cabinets, elegant marble worktops with visible grain, and polished copper fixtures with tasteful champagne gold accents. The color palette includes muted wine-rose tones. Lighting is warm, soft, and cinematic, creating a premium Grand Luxe atmosphere. No text, no people, wide aspect ratio for a full-screen game background, highly detailed 3D render style.", "description": "A premium 3D CGI French patisserie kitchen background with marble and copper accents.", "subtype": "background", "aspect_ratio": [1264, 848], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "egg_single": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/egg_single_b2947211-97f0-4c59-bc80-1fbf110f8e5f.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI render of a single white egg. The egg should have a perfect oval shape with a clean, smooth white shell showing subtle realistic texture. Use professional studio lighting to create dimensional soft highlights and a gentle grounding shadow. The asset should be an isolated object, front-facing, with a polished patisserie aesthetic consistent with a premium 3D kitchen game. Transparent background, high resolution, detailed material properties.", "description": "A polished realistic 3D CGI white egg prop for the baking process.", "subtype": "object", "aspect_ratio": [843, 1120], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "egg_yolk_liquid": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/egg_yolk_liquid_e23b86b7-56cf-44ea-bb77-819205ace9aa.webp", "type": "image", "prompt": "Generate a professional 3D CGI rendered asset of a fresh egg yolk with dripping egg white. The yolk should be a rich, dimensional golden-yellow sphere with soft highlights. The egg white should be glossy and translucent, forming natural gravity-shaped drips and a flowing silhouette. Studio lighting, high-quality material shaders, realistic textures, isolated on a transparent background, no shell, no hands, no text.", "description": "A realistic 3D CGI fresh egg yolk with dripping egg white for cooking animations.", "subtype": "effect", "aspect_ratio": [816, 763], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "egg_shell_broken_top": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/egg_shell_broken_top_223dc99a-22f0-4b17-b875-893ec17df639.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI render of the top half of a broken white eggshell, matching the style of existing kitchen assets. Focus on a crisp, porcelain-like texture with subtle highlights and soft shadows. The shell should have a natural jagged rim and visible thin thickness. Isolated object, front view, transparent background, no hands, no egg contents, professional studio lighting.", "description": "A premium 3D CGI top half of a broken white eggshell used for baking animations.", "subtype": "object", "aspect_ratio": [843, 961], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "egg_shell_broken_bottom": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/egg_shell_broken_bottom_400efbc6-7b42-4f61-b736-d2e80c985e16.webp", "type": "image", "prompt": "Generate a detailed 3D CGI render of the bottom half of a broken white eggshell. Match the style and material of the top shell in image 1. The shell should have a natural jagged edge, visible curved thickness, and a smooth matte texture. Use professional studio lighting with gentle highlights and soft shadows to create depth. Isolated object on a transparent background, no hands, no text, high resolution, premium patisserie game asset style.", "description": "A detailed 3D CGI bottom half of a broken white eggshell with a jagged edge.", "subtype": "object", "aspect_ratio": [738, 740], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "flour_stream_texture": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/flour_stream_texture_797ad0d1-06ab-4ec6-9eb2-82232c32dae4.webp", "type": "image", "prompt": "Generate a 9:16 tall image of a continuous, vertical stream of white flour pouring down through the center of the frame. Render with 3D CGI particle depth, featuring fine-grain sparkle and soft, powdery edges. Use realistic studio lighting to create dimension within the white powder. The silhouette should be clean and isolated on a transparent background. No containers, no hands, no text, just the flowing flour stream, matching the premium 3D CGI aesthetic of image 1.", "description": "A 3D rendered continuous stream of white flour with particle depth for pouring animations", "subtype": "effect", "aspect_ratio": [552, 1264], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cream_dollop": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cream_dollop_96959238-d2d7-4985-b63d-79b3d84f8c60.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI photorealistic dollop of white whipped cream. The cream should be in a perfect piped swirl shape with clearly defined dimensional ridges and a soft, airy texture. Include delicate glossy highlights where light hits the peaks and subtle realistic shadows in the folds. Isolated on a transparent background, clean edges, professional food photography lighting, premium dessert topping aesthetic, no extra garnishes.", "description": "A photorealistic 3D CGI swirl of white whipped cream for cake decoration.", "subtype": "object", "aspect_ratio": [1022, 987], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "happy_birthday_topper": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/happy_birthday_topper_62beb3e5-32b3-4a20-b733-469c355804c1.webp", "type": "image", "prompt": "Generate a dimensional 3D CGI cake topper that says 'Happy Birthday'. The design should feature crisp, readable lettering with realistic thickness and a polished, premium plastic or acrylic finish. Match the colorful palette of image 1. Include delicate highlights and soft ambient occlusion shadows to give it depth. The asset should be a standalone decoration with a transparent background, high resolution, professional studio lighting, and maintain the exact spelling and silhouette of the original topper.", "description": "A dimensional 3D CGI 'Happy Birthday' cake topper with a polished premium finish.", "subtype": "object", "aspect_ratio": [1024, 891], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "kuromi_topper": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/kuromi_topper_3cbdc8ff-f67b-4006-800d-980b5862d3ab.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI collectible-style cake topper of Kuromi. Preserve the signature black hood with pink skull, pointed ears, and mischievous expression. The asset should have dimensional sculpted forms, smooth matte plastic material shading, and soft premium highlights. Render as a standalone character decoration with a clear silhouette. Professional studio lighting, 4k resolution, transparent background, front-facing view.", "description": "A polished 3D CGI collectible-style Kuromi character cake topper with premium material shading.", "subtype": "object", "aspect_ratio": [754, 1263], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "little_twin_stars_topper": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/little_twin_stars_topper_262cec4b-2997-4926-b970-8b9da582bda5.webp", "type": "image", "prompt": "Transform the characters in image 1 into a high-quality 3D CGI cake topper. Generate a dimensional, softly sculpted plastic or fondant-like figurine of Kiki and Lala (Little Twin Stars). Maintain their exact identities, pastel hair colors (pink and blue), and original composition. Use premium studio lighting with soft highlights and gentle shadows to create depth. The style should be clean, polished, and charming, matching a premium 3D game aesthetic. Transparent background, front-facing illustration.", "description": "A 3D CGI rendered character cake topper featuring Kiki and Lala in a soft pastel style.", "subtype": "object", "aspect_ratio": [842, 1030], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "mint_leaf": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mint_leaf_db55f487-900a-4adf-8738-7704cb834c52.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI pair of fresh green mint leaves used for cake decoration. Maintain the exact leaf count and shape of image 1, but transform into a realistic dimensional garnish. Add intricate visible veins, natural gentle curvature to the leaves, and soft fresh highlights. Include delicate self-shadowing to enhance 3D depth. Professional food photography quality, isolated on a transparent background, side-view angle.", "description": "A pair of fresh green mint leaves as a realistic 3D CGI cake garnish.", "subtype": "object", "aspect_ratio": [1264, 796], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "orange_slice": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/orange_slice_cd8abae8-16eb-4538-a88d-867fb45a5339.webp", "type": "image", "prompt": "Generate a detailed 3D CGI rendered candied orange half-slice as a cake garnish. The orange slice should have translucent flesh with visible juicy segments, a thick dimensional orange peel, and a delicate glistening sugar glaze. Use professional studio lighting with soft shadows. Ensure it is a single isolated object with a clean, high-quality look that matches the existing 3D premium kitchen assets. High resolution, 8k render, photorealistic but stylized for a game.", "description": "A detailed 3D CGI candied orange half-slice for cake decoration.", "subtype": "object", "aspect_ratio": [1024, 921], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "bear_cookie": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/bear_cookie_d9968d11-ea81-4232-8191-767c8b697ace.webp", "type": "image", "prompt": "Transform the bear cookie in image 1 into a polished 3D CGI bakery decoration. Generate a full-body, front-facing cute bear-shaped butter cookie with a golden-baked crumb texture. Ensure the cookie has a slightly rounded thickness with fine baked details and softly dimensional facial features. Apply gentle warm highlights and soft professional studio lighting to create depth. Maintain the exact silhouette and identity of the original bear while upgrading to a high-quality 3D rendered style. Isolated on a transparent background.", "description": "A polished 3D CGI bear-shaped butter cookie for cake decoration.", "subtype": "object", "aspect_ratio": [842, 1171], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "chocolate_cream": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/chocolate_cream_85d029df-254e-4095-b159-33dc92fc5745.webp", "type": "image", "prompt": "A high-quality 3D CGI render of a single, fluffy swirl of rich chocolate whipped cream frosting. The object should have distinct piped ridges and an appetizing dimensional texture with a gentle chocolate sheen and professional studio highlights. Rendered as a refined patisserie decoration, isolated on a transparent background with soft natural shadows at the base. Maintain the rich dark cocoa color and elegant spiral shape, similar to a high-end pastry topping. Professional lighting, 8k resolution, clean edges.", "description": "Refined 3D CGI swirl of chocolate whipped cream for cake decoration.", "subtype": "object", "aspect_ratio": [1023, 977], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "halloween_cake_base": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/halloween_cake_base_a2b65c8a-e4ee-4ca1-8f12-9f9f9f193d40.webp", "type": "image", "prompt": "Generate a single isolated, premium photorealistic 3D CGI round frosted cake body. Match the lighting, camera angle, and polished realistic rendering of image 1. The cake features smooth deep aubergine/plum-purple frosting on the sides, a neat glossy orange pumpkin-spice frosting top, and elegant orange icing drips running down the sides. Include a subtle antique-gold piped trim at the base. High-resolution, professional studio lighting, three-quarter elevated view. No decorations, no pumpkins, no ghosts, no plates, no background. Transparent background, centered, full silhouette.", "description": "A single-tier Halloween-themed cake base with plum-purple frosting and orange drips, matching the 3D CGI patisserie style.", "subtype": "object", "aspect_ratio": [1024, 1013], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "halloween_kitchen_background": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/halloween_kitchen_background_59948364-d298-4c60-a0b4-c46e0fabdfd1.webp", "type": "image", "prompt": "Transform the existing kitchen background into a cozy Halloween-themed luxury French patisserie. Maintain the 16:9 wide composition, polished 3D CGI style, and warm evening lighting. Add elegant Halloween decor: small carved pumpkins with warm candlelight, purple and orange bunting, subtle hanging bats, and delicate cobwebs in the upper corners. Place autumn leaves and mini pumpkins on the counters while keeping the central workspace clear. The window should show a soft purple-orange twilight sky. The atmosphere must be welcoming and refined, not scary or cluttered. Match the high-end materials and lighting of the original kitchen.", "description": "A luxury Halloween-themed French patisserie kitchen background with warm evening lighting and festive decor.", "subtype": "background", "aspect_ratio": [1264, 848], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "twin_brothers_shop_exterior": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/d84173d0-c051-70c1-42bd-1cff41ddf10f/1793591432789_01M6D62NJNFR5HDBGB7HHT96K7.image.png", "type": "image", "prompt": null, "description": "Twin Brothers Coffee shop exterior photo, intended as a visual reference for the bakery/shop background.", "subtype": null, "aspect_ratio": [1345, 1008], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "twin_brothers_cinnamon_roll_closeup": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/d84173d0-c051-70c1-42bd-1cff41ddf10f/1793591496300_01M6D64KKC6EFGKMRGB6ESKBCR.image.png", "type": "image", "prompt": null, "description": "Close-up reference photo of a Twin Brothers Coffee cinnamon roll, intended as the final baked product reference.", "subtype": null, "aspect_ratio": [2000, 1500], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "twin_brothers_cinnamon_roll_box": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/d84173d0-c051-70c1-42bd-1cff41ddf10f/1793591508210_01M6D64Z7JWWR5QVD1MDCVB4PP.image.png", "type": "image", "prompt": null, "description": "Twin Brothers Coffee cinnamon rolls in branded packaging, intended as a final product and presentation reference.", "subtype": null, "aspect_ratio": [439, 455], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "twin_brothers_cinnamon_roll_coffee": {"url": "https://dzmv1mxivz48k.cloudfront.net/images/d84173d0-c051-70c1-42bd-1cff41ddf10f/1793591877775_01M6D6G84F08X4JAW79H56D35F.image.png", "type": "image", "prompt": null, "description": "Twin Brothers Coffee cinnamon roll served on a plate beside a cup of coffee; use as the finished cinnamon roll showcase image.", "subtype": null, "aspect_ratio": [588, 393], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_kneading_luxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_kneading_luxe_b3c1f33d-31aa-4708-9e32-e8736df295db.webp", "type": "image", "prompt": "Generate a premium, high-resolution, photorealistic 3D CGI game prop. Show a pair of clean baker's hands gently hand-kneading a smooth, elastic golden-cream enriched bread dough inside the 'mixing_bowl_large' brushed stainless-steel bowl. Close three-quarter top-down view. Use warm directional bakery lighting, precise fine dough textures, and a sophisticated espresso-and-cream color grade. Ensure elegant realistic material highlights on the metal and skin. Full-body object illustration with the entire bowl and hands visible, no cropping. Transparent background, professional quality, high detail, no text or watermarks.", "description": "A pair of hands kneading smooth cinnamon roll dough in a stainless steel bowl, premium 3D CGI style.", "subtype": "object", "aspect_ratio": [1115, 842], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_shaping_luxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_shaping_luxe_5f230330-ba55-483f-b3cd-5e05e67629e6.webp", "type": "image", "prompt": "Generate a premium, high-resolution photorealistic 3D CGI game prop of a rolled rectangular enriched dough sheet on a warm walnut pastry board. The dough is thickly covered edge-to-edge with dark cinnamon brown sugar filling and visible finely chopped walnuts. Include neat spiral-cut portions showing the buttery cinnamon layers. A small elegant 7 cm measuring ruler (no text) sits beside the dough. Three-quarter overhead view, golden bakery lighting, crisp realistic textures, warm espresso/cream/caramel color palette. Isolated on transparent background, professional quality, high detail, no cropping.", "description": "A premium 3D CGI prop showing the cinnamon roll dough sheet with filling, walnuts, and spiral cuts on a pastry board.", "subtype": "object", "aspect_ratio": [730, 627], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_tray_luxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_tray_luxe_11c422de-fb98-4211-b372-a845c917c8dd.webp", "type": "image", "prompt": "Generate a high-resolution, photorealistic 3D CGI game prop of six hand-cut thick square cinnamon rolls arranged in a rectangular brushed-metal baking tray. The rolls are nestled closely, with the tray bottom visibly pooled with rich, glossy cinnamon caramel syrup and generous pieces of chopped walnuts. The dough is a soft golden-cream color with visible dark cinnamon spirals. Three-quarter overhead view, warm soft cafe lighting with a sophisticated espresso and gold color palette. Professional studio quality, realistic material highlights on the metal and syrup, isolated on a transparent background, no cropping, no text, no logo.", "description": "A premium 3D CGI tray of six unbaked cinnamon rolls pooled in caramel syrup and walnuts, ready for the oven.", "subtype": "object", "aspect_ratio": [877, 607], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_hero_luxe": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_hero_luxe_916b7ab7-b437-4329-8f51-c377984a886b.webp", "type": "image", "prompt": "Generate a premium, high-resolution photorealistic 3D CGI hero game asset of a single signature cinnamon roll. The pastry is a thick, 7 cm cube-shaped softly rounded hand-made roll with moist tender golden sides and a rich dark cinnamon spiral. Features generous smooth glossy caramel glaze flowing evenly over the top and edges, with carefully scattered chopped walnut pieces. Three-quarter close-up view, luxurious warm directional cafe lighting, elegant espresso, caramel, cream, and brass palette. Subtle realistic steam and appetizing polished textures, crisp sharp focus. Isolated on a transparent background, full body visible with generous margins, no plate, no cup, no text, no logo, no watermark.", "description": "A premium 3D CGI hero image of a signature square cinnamon roll with caramel glaze and walnuts.", "subtype": "object", "aspect_ratio": [749, 644], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "twin_brothers_3d_bakery_interior": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/twin_brothers_3d_bakery_interior_f5f4cb90-53a6-4372-bb92-e071883856e1.webp", "type": "image", "prompt": "A premium 3D CGI game background of a refined, welcoming caf\u00e9 interior for a portrait 9:16 screen. The style should be a high-end realistic 3D render with warm walnut counters, espresso-brown and cream color palette, and subtle brass accents. Soft golden morning light illuminates the scene. Behind the counter, include two friendly adult twin-brother bakers in coordinated cream shirts and dark espresso aprons, rendered as polished 3D CGI characters. Maintain a large, uncluttered central and lower foreground area for gameplay objects. The aesthetic should reference the charcoal stone and glass identity from image 1 and the warm coffee-and-pastry mood from image 2. High-resolution, detailed lighting, atmospheric depth, professional quality, no UI text, no watermark.", "description": "A premium 3D CGI interior background of the Twin Brothers Coffee bakery featuring the two twin bakers.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_turntable": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_turntable_2f12bbf1-b0b6-4b71-a301-afab9a6b8f0f.webp", "type": "image", "prompt": "Generate a single isolated premium 3D CGI bakery turntable prop. Features a round ivory marble rotating top with subtle measurement rings and a sturdy champagne-gold metal pedestal. Three-quarter front view, centered illustration. Use realistic polished materials with soft studio shadows. Consistent with the warm espresso, cream, and brass palette of Twin Brothers Coffee. High-end 3D render, professional studio lighting, transparent background, no text, no pastry, no hands.", "description": "A premium 3D CGI rotating marble cake stand for displaying and decorating cinnamon rolls.", "subtype": "object", "aspect_ratio": [716, 548], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_caramel_bowl": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_caramel_bowl_cf830b6d-75da-4a76-b3f1-68f15f86dfdc.webp", "type": "image", "prompt": "Generate a single isolated, photorealistic premium 3D CGI shallow cream ceramic mixing bowl filled with thick glossy amber caramel sauce. A small polished brass spoon is resting inside the bowl. Focus on delicious soft highlights on the caramel and warm bakery studio lighting. Three-quarter view, full object visible and centered, transparent background. Match the refined aesthetic and ivory/champagne-gold material palette of existing assets like the digital_scale. High resolution, professional quality, game-ready object.", "description": "A premium 3D CGI cream ceramic bowl filled with glossy amber caramel sauce and a brass spoon.", "subtype": "object", "aspect_ratio": [790, 666], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_yeast_bowl": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_yeast_bowl_3e4eb1a5-db6f-44d2-9691-51aa4b26f1ba.webp", "type": "image", "prompt": "Generate a high-end realistic 3D CGI food prop of an isolated small clear glass prep bowl filled with tiny beige instant yeast granules. Use a three-quarter view, centered composition with the full bowl visible. Apply warm studio lighting with realistic glass refractions and grainy yeast texture. Professional patisserie quality, transparent background, no text, no hands, no extra objects. Match the premium 3D aesthetic of the Twin Brothers Coffee game assets.", "description": "An isolated clear glass prep bowl filled with dry yeast granules for the cinnamon roll recipe.", "subtype": "object", "aspect_ratio": [737, 501], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_salt_bowl": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_salt_bowl_f7ef2870-4aa9-4228-b0e6-32d8c74c4ebf.webp", "type": "image", "prompt": "Generate a high-end realistic 3D CGI food prop of a miniature cream ceramic pinch bowl filled with fine white sea salt. Three-quarter view, centered, full object visible. Professional studio lighting with soft shadows, premium materials. Transparent background. Consistent with the warm ivory and champagne gold aesthetic of the Twin Brothers Coffee bakery set.", "description": "An isolated miniature cream ceramic pinch bowl with fine sea salt for the baking process.", "subtype": "object", "aspect_ratio": [924, 635], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_chopped_walnuts": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_chopped_walnuts_60a1c77f-8b86-449e-82ba-27fb73f97a4d.webp", "type": "image", "prompt": "Generate a small handful of irregular chopped walnut pieces as an isolated premium realistic 3D CGI food prop. Rich natural toasted walnut browns with subtle highlights and soft studio shadows. Centered composition, high resolution, detailed texture, professional quality, transparent background, no bowl, no text, no hands, no other ingredients.", "description": "A pile of chopped walnuts for cinnamon roll decoration, premium 3D CGI style.", "subtype": "object", "aspect_ratio": [858, 443], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_coffee_showcase": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_coffee_showcase_a90319ee-9f5d-4028-a174-9b45abebf3a0.webp", "type": "image", "prompt": "Generate a premium photorealistic 3D CGI product showcase of a single generously glazed, square-shaped cinnamon roll. The roll should feature visible soft layered spiral sides, an even glossy amber caramel coating, and a topping of chopped toasted walnuts. It is served on a refined pale ceramic plate placed beside a simple white ceramic cup of coffee. Three-quarter close-up view, high-end warm caf\u00e9 product photography lighting, resting on an espresso walnut tabletop with a subtle cream background. Use appetizing realistic 3D food materials, portrait-friendly composition (9:16 aspect ratio) with the roll as the main focus. High resolution, detailed, professional quality, no readable branding, no text, no hands.", "description": "A premium 3D CGI final product reveal showing a glazed cinnamon roll on a plate next to a cup of coffee.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_caramel_drizzle": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_caramel_drizzle_c628dd15-8178-4886-bda4-692a3bf8e310.webp", "type": "image", "prompt": "Generate a single isolated thick looping ribbon of glossy amber caramel drizzle. High-end realistic 3D CGI food topping with warm golden highlights and slight dimensional depth. The caramel should be arranged in a short flowing zigzag pattern. Realistic liquid texture, appetizing and polished. Front-facing view, centered on a transparent background, no bowl, no spoon, no text, no other objects.", "description": "A thick, glossy amber caramel drizzle ribbon for decorating the cinnamon roll.", "subtype": "effect", "aspect_ratio": [735, 495], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_cinnamon_dust": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_cinnamon_dust_8e150262-9426-486d-90de-2d6b771aee52.webp", "type": "image", "prompt": "Generate a small isolated delicate scattering of warm cinnamon powder with a few tiny soft clumps, rendered as a premium realistic 3D CGI food garnish. High-resolution texture showing fine spice grains, subtle dimensional shading to create depth on the surface, compact centered composition, transparent background, no plate, no text, no other ingredients or objects. Match the warm lighting and premium 3D style of the Twin Brothers Coffee bakery assets.", "description": "A delicate scattering of realistic cinnamon powder garnish for cake decoration.", "subtype": "effect", "aspect_ratio": [599, 332], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "twin_brothers_3d_bakery_empty": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/twin_brothers_3d_bakery_empty_1efb8eb2-e641-4a31-94db-e49b4fc13ef7.webp", "type": "image", "prompt": "Generate a 9:16 tall photorealistic 3D CGI rendered interior of a specialty coffee shop and cinnamon roll bakery. Architectural style inspired by Twin Brothers Coffee featuring warm wood slat ceiling accents, concrete counters, a professional espresso machine, and glass pastry cases filled with cinnamon rolls. Warm cinematic lighting, boutique cafe atmosphere. Absolutely no people, no humans, no figures, completely empty interior. High resolution, 8k, architectural visualization style.", "description": "Empty luxurious 3D CGI interior of a Taipei-style specialty bakery and coffee shop.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_dough_ball": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_dough_ball_4bd83b01-906c-4755-ae5f-f0d5271dfec1.webp", "type": "image", "prompt": "Generate a premium 3D CGI rendered smooth, glossy, and elastic yeast dough ball resting on a dark marble pastry board. Include a rustic French wooden rolling pin and a stainless steel dough scraper placed beside it. Professional studio product lighting, realistic surface texture of fermented brioche dough with a soft dusting of flour. Centered composition, high resolution, detailed 3D render. Strictly NO hands, NO people, NO humans. Transparent background.", "description": "A premium 3D CGI yeast dough ball with a rolling pin and scraper on a marble board.", "subtype": "object", "aspect_ratio": [1414, 930], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_water_glass": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_water_glass_f046c9a4-00f8-41ae-8aae-494b3b127fee.webp", "type": "image", "prompt": "Generate a premium 3D CGI render of a clear glass measuring tumbler filled with 50ml of pure water. The image should feature professional kitchen studio lighting, soft glass refractions, and realistic water meniscus reflections. High-quality material shaders, isolated on a transparent background, centered composition, no people, no hands, realistic kitchen prop style matching the existing 3D baking assets.", "description": "A clean, realistic 3D CGI glass measuring tumbler with water for baking.", "subtype": "object", "aspect_ratio": [620, 792], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_stirring_spoon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_stirring_spoon_818dedd1-965a-491f-bc84-e66253145126.webp", "type": "image", "prompt": "Generate a premium 3D CGI vintage pastry stirring spoon made of polished brass and dark walnut wood. The spoon is dripping with thick, glossy amber caramel glaze. Professional studio lighting with realistic metallic reflections and translucent caramel textures. Isolated on a transparent background, center-aligned, high resolution, photorealistic 3D render. No hands, no people, no figures.", "description": "A premium 3D CGI pastry spoon dripping with caramel, used for mixing and decorating animations.", "subtype": "object", "aspect_ratio": [631, 925], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_rolling_pin": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_rolling_pin_1ac175c2-ebdb-422e-af97-2f6b64723b84.webp", "type": "image", "prompt": "Generate a photorealistic premium 3D CGI product render of a real French tapered wooden rolling pin, made from rich walnut and maple with visible subtle wood grain. Smooth turned ends and soft professional studio reflections. Isolated object, horizontal diagonal composition, transparent background. High-resolution details, convincing depth and realistic material shading. No text, no labels, no hands, no people. Matches the premium patisserie style of existing kitchen assets.", "description": "A premium 3D CGI French tapered wooden rolling pin for the dough preparation stage.", "subtype": "object", "aspect_ratio": [1056, 317], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_filled_dough_sheet": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_filled_dough_sheet_a89ccf21-ad8b-41b0-b2a6-f584d7ac7a05.webp", "type": "image", "prompt": "Generate a high-quality 3D CGI still life of a rectangular brioche dough sheet on a dark walnut pastry board. The dough is spread with melted butter, thick dark brown sugar, cinnamon, and scattered chopped walnuts. A French wooden rolling pin and a stainless steel ruler are placed neatly at the side. 720x1280 portrait composition, top-down three-quarter view, warm natural bakery lighting, realistic textures, professional studio render, no hands, no text.", "description": "A premium 3D CGI dough sheet with cinnamon sugar and walnuts on a pastry board, prepared for rolling.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_baked_tray": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_baked_tray_1722f5eb-470a-44f2-ab69-583ba96de970.webp", "type": "image", "prompt": "Generate a photorealistic premium 3D CGI product scene of six square 7 cm Taipei-style cinnamon rolls baked in a rectangular brushed-metal baking pan. The rolls are evenly golden brown, visibly soft and springy, with deeply glossy amber caramel syrup coating their tops and pooling generously around them; chopped walnut pieces are distributed evenly on top. Warm realistic bakery lighting, soft steam, high detail in pastry layers and baked edges, three-quarter view. Professional studio lighting, high resolution, 3D render style matching image 'cinnamon_roll_tray_luxe'. Transparent background, no hands, no people, no text.", "description": "A tray of six freshly baked Taipei-style cinnamon rolls with glossy caramel and walnuts.", "subtype": "object", "aspect_ratio": [1183, 731], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_caramel_finish": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_caramel_finish_d0a392b6-b6f6-4cc7-82f0-d5a6f05503aa.webp", "type": "image", "prompt": "Photorealistic premium 3D CGI close-up of one signature square cinnamon roll, moist glossy caramel-coated exterior, soft elastic golden crumb, rich cinnamon and brown-sugar spiral, even caramel glaze, neatly scattered chopped walnuts. A small pool of caramel at the base on a simple ivory ceramic plate. Three-quarter view, professional studio bakery lighting, realistic appetizing pastry textures. Isolated composition, high resolution, 8k render, transparent background, no people, no hands, no text.", "description": "A photorealistic 3D CGI hero shot of a finished square cinnamon roll with caramel glaze and walnuts on a plate.", "subtype": "object", "aspect_ratio": [951, 687], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_unbaked_rolls_tray": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_unbaked_rolls_tray_446058ba-033b-4f44-9d0c-e341a3444197.webp", "type": "image", "prompt": "A highly realistic, premium photorealistic 3D CGI food asset of a shallow rectangular brushed-metal baking pan viewed from a three-quarter elevated angle. The pan contains six separate, freshly cut, square-shaped raw cinnamon roll dough portions. The dough must look unmistakably UNBAKED: pale ivory and cream color, soft pillowy texture, visible neat cinnamon-brown spirals on top, with no golden-brown or toasted areas. A shallow layer of glossy amber cinnamon-caramel syrup coats the bottom of the pan with small chopped walnut pieces scattered between the rolls. Professional studio lighting, exquisite tactile dough texture, soft contact shadows, artisan bakery aesthetic. Isolated on a transparent background, no hands, no oven, no text.", "description": "A tray of six raw, unbaked cinnamon rolls in syrup, ready for the oven.", "subtype": "object", "aspect_ratio": [1264, 831], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_raw_dough_tray_realistic": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_raw_dough_tray_realistic_6be5c5fd-bc52-4485-a77d-e3aae12499c4.webp", "type": "image", "prompt": "Generate a premium 3D CGI food asset of a dark rectangular metal baking pan containing exactly six unbaked cinnamon roll dough portions. The dough is pale ivory, soft, and puffy from proofing, showing raw spiral layers with cinnamon filling and chopped walnuts. Include glossy amber caramel syrup pooled at the base. Perspective is a gentle elevated three-quarter view. The dough must look completely raw and pale, not browned or baked. High-resolution food photography lighting with realistic textures and shadows. Transparent background, professional patisserie quality, matching the style of twin_brothers_3d_bakery_interior.", "description": "A dark rectangular pan containing six unbaked, raw cinnamon roll dough portions in caramel syrup for the pre-oven baking stage.", "subtype": "object", "aspect_ratio": [1087, 683], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_shaggy_dough_bowl": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_shaggy_dough_bowl_107b9c34-9d13-478b-af7c-f412f2cc2e9a.webp", "type": "image", "prompt": "Generate a premium photorealistic 3D CGI object of a large stainless-steel mixing bowl filled with raw, pale, sticky, and shaggy bread dough. The dough should look moist and unformed, showing the texture of just-mixed flour and liquids. A small wooden dough scraper lies beside the bowl. Professional product photography style, natural warm bakery lighting, realistic metallic reflections on the bowl, high-detail tactile dough texture. Front-facing view, isolated on a transparent background, no people, no hands.", "description": "A premium 3D CGI stainless steel bowl containing raw, shaggy cinnamon roll dough with a wooden scraper.", "subtype": "object", "aspect_ratio": [1128, 823], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_windowpane_dough": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_windowpane_dough_e0605a76-9ce8-4402-a659-b7b2621fe529.webp", "type": "image", "prompt": "Generate a premium photorealistic 3D CGI food photography asset of a stainless-steel mixing bowl. Inside the bowl, a small portion of pale, raw bread dough is stretched upwards into a thin, translucent windowpane membrane to show gluten development. The dough is creamy pale ivory, moist, and unbaked. Professional studio lighting with warm artisan bakery tones, realistic metallic reflections on the bowl, and tactile dough texture. High resolution, detailed, isolated on a transparent background, center-framed, no hands, no people, no text.", "description": "A 3D CGI demonstration of the windowpane test with raw cinnamon roll dough in a mixing bowl.", "subtype": "object", "aspect_ratio": [1053, 834], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_butter_dough_bowl": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_butter_dough_bowl_8c995213-63b8-44ae-8c85-5b268b775c8d.webp", "type": "image", "prompt": "Premium photorealistic 3D CGI still life of an authentic stainless-steel mixing bowl containing raw pale bread dough with several small cubes of softened yellow butter resting on top. The dough is unbaked, soft, and slightly rough in texture. High-end food photography with warm artisan bakery lighting, realistic metal reflections on the bowl, and tactile textures for the dough and butter. Isolated on a transparent background, center-aligned, filling the frame. No hands, no people, no text, no UI.", "description": "A stainless steel bowl containing raw cinnamon roll dough topped with cubes of softened butter, ready for kneading.", "subtype": "object", "aspect_ratio": [1034, 755], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_smooth_dough_bowl": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_smooth_dough_bowl_b03675c7-86e9-4306-9ed0-93bbdda09b65.webp", "type": "image", "prompt": "Generate a premium photorealistic 3D CGI food still life of a polished stainless-steel mixing bowl containing a single smooth, elastic, pale ivory raw bread dough ball. The dough should show a subtle stretch mark on the surface to indicate soft elasticity and gluten development. It is clearly unbaked, moist bread dough. Use artisan bakery lighting with realistic metal reflections. Isolated on a transparent background, front-view, object-focused composition. High resolution, professional food photography style, no hands, no text.", "description": "A polished stainless-steel mixing bowl containing smooth, elastic, raw bread dough after butter incorporation.", "subtype": "object", "aspect_ratio": [1122, 847], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_first_rise_dough": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_first_rise_dough_6a67a221-ced5-4617-8d77-aa34854a3a70.webp", "type": "image", "prompt": "Premium photorealistic 3D CGI still life of a large stainless steel mixing bowl filled with pale, raw bread dough that has risen to double its volume. The dough has a smooth, soft domed surface with subtle fermentation bubbles. The bowl is loosely covered with transparent food wrap showing realistic clings and wrinkles. Unbaked dough texture, not cake-like. Warm natural bakery lighting, realistic metallic reflections on the bowl, high-end food photography style. Isolated on a transparent background, center-aligned, no hands or people.", "description": "A stainless steel bowl containing raw cinnamon roll dough that has doubled in size during its first rise, covered in plastic wrap.", "subtype": "object", "aspect_ratio": [1124, 840], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_filled_dough_log": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_filled_dough_log_e27f2b2f-9756-4614-86b5-45d9deee9de8.webp", "type": "image", "prompt": "Generate a premium photorealistic 3D CGI food photography asset of a horizontal tightly rolled log of raw pale bread dough. The cut end shows a detailed dark cinnamon-brown-sugar and walnut spiral. The log rests on a rustic wooden pastry board with a wooden rolling pin and a metal baker's bench knife nearby. The dough is raw, pale, and unbaked with no golden crust. Natural warm bakery lighting, high-resolution tactile textures for dough and wood, professional studio quality. Transparent background, object fills the frame, side-view perspective. Match the 3D CGI style of cinnamon_roll_3d_filled_dough_sheet.", "description": "A premium 3D CGI horizontal log of raw cinnamon roll dough with visible filling on the cut end, placed on a pastry board.", "subtype": "object", "aspect_ratio": [1264, 629], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_cut_raw_rolls_tray": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_cut_raw_rolls_tray_0b888ef5-c260-40bf-9c0c-65318e0ddaf6.webp", "type": "image", "prompt": "Generate a premium photorealistic 3D CGI product scene of six evenly cut, approximately 7 cm tall unbaked cinnamon-roll dough portions arranged upright in a dark rectangular metal baking pan. The rolls show exposed pale dough spirals, rich dark cinnamon filling, and visible walnut pieces. A shallow pool of glossy amber caramel syrup coats the bottom of the pan. The dough is raw and pale ivory, not browned or baked. Three-quarter perspective view, professional bakery lighting with soft shadows, realistic metal and dough textures. Transparent background, high resolution, detailed food photography style. No hands, no people, no text.", "description": "Six raw, upright cinnamon roll dough portions with cinnamon spirals and walnuts in a dark metal pan with caramel syrup.", "subtype": "object", "aspect_ratio": [1264, 830], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_unfilled_rolled_sheet": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_unfilled_rolled_sheet_94f9e674-81b9-49d5-a072-690ae79b5502.webp", "type": "image", "prompt": "Generate a premium photorealistic 3D CGI bakery scene. A neat, thick rectangular sheet of pale ivory raw yeast dough is spread across a realistic dark walnut pastry board. Include a faint dusting of flour on the board. A wooden rolling pin and a stainless steel bench scraper are resting at the edge of the board. Three-quarter overhead perspective, elegant warm bakery lighting, realistic physical shadows and textures. High resolution, professional studio quality, no hands, no text, no UI.", "description": "A raw, unfilled rectangular sheet of cinnamon roll dough on a walnut pastry board with a rolling pin and scraper.", "subtype": "object", "aspect_ratio": [1262, 753], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_caramel_soak_rack": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_caramel_soak_rack_c8b18440-422b-4c52-a29b-7c6d22fca2f7.webp", "type": "image", "prompt": "Generate a premium 3D CGI photorealistic single square cinnamon roll, freshly baked with a soft springy crumb, topped with wet glossy amber caramel glaze and chopped walnut pieces. The roll is resting on a professional stainless steel wire cooling rack. Include small, subtle natural wisps of steam/heat rising from the roll. Close-up three-quarter view, warm artisan bakery lighting with realistic physical shadows and textures. High-resolution, professional quality, transparent background. Match the visual style and lighting of cinnamon_roll_3d_caramel_finish.", "description": "A freshly baked square cinnamon roll with caramel glaze and walnuts resting on a cooling rack.", "subtype": "object", "aspect_ratio": [1264, 843], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_warm_roll_turntable": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_warm_roll_turntable_be7fdf1c-a2ea-4dab-90fe-bb1560284c8b.webp", "type": "image", "prompt": "A premium photorealistic 3D CGI bakery product shot of one warm, fully baked square cinnamon roll with moist glossy amber caramel glaze and chopped walnuts. The roll is centered on an elegant cream-marble rotating cake stand. Three-quarter product-photo view with rich natural artisan bakery lighting, realistic physical shadows, and tactile soft crumb textures. Isolated on a transparent background, high-end patisserie aesthetic, no people, no hands, no text, no UI.", "description": "A warm, glazed cinnamon roll presented on a marble turntable, premium 3D CGI style.", "subtype": "object", "aspect_ratio": [1169, 843], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_glazed_roll_turntable": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_glazed_roll_turntable_ab68e1d5-504c-4421-9aa9-9e5381ea7114.webp", "type": "image", "prompt": "Generate a premium photorealistic 3D CGI hero scene for a bakery game: a warm, freshly baked square cinnamon roll centered on a cream-marble turntable. The roll is covered in a beautifully even layer of glossy amber caramel glaze cascading naturally over the sides, topped with scattered walnut pieces. A realistic stainless steel offset spatula rests elegantly beside the roll. Use professional product photography lighting, realistic textures for the soft bread crumb and wet glaze, and a three-quarter view angle. Transparent background, high resolution, detailed 3D render, no hands, no text, no UI elements.", "description": "A premium 3D CGI hero shot of a glazed cinnamon roll with walnuts on a marble turntable for the final decoration stage.", "subtype": "object", "aspect_ratio": [1168, 841], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_caramel_bowl_no_spoon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_caramel_bowl_no_spoon_75a79c85-eced-4a1c-8f0c-be4399234ca0.webp", "type": "image", "prompt": "Generate a premium photorealistic 3D CGI product image of a shallow ivory ceramic pastry bowl filled with thick, smooth, glossy amber caramel sauce. The bowl is shown from a three-quarter front view with a clean rim and no utensils or spoons inside. Feature realistic caramel highlights and soft warm bakery lighting. Match the luxury 3D style of existing assets like 'cinnamon_roll_3d_caramel_bowl'. Isolated on a transparent background, high resolution, professional studio lighting, no text, no hands.", "description": "A premium 3D CGI ivory ceramic bowl filled with glossy amber caramel sauce for the baking process.", "subtype": "object", "aspect_ratio": [942, 658], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_oven_raw_scene": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_oven_raw_scene_7e985159-0aa0-46a0-ac7d-a5ea11fd6c58.webp", "type": "image", "prompt": "Generate a 9:16 tall photorealistic premium 3D CGI portrait bakery scene. A cream enamel professional bakery oven with the door fully open and a warm interior light glowing. A dark rectangular pan containing six pale, raw, unbaked cinnamon roll dough portions with visible walnut-cinnamon spirals sits on the metallic oven rack. The rolls are distinctly unbaked and pale. Three-quarter front view. Match the polished architectural lighting and luxury materials of kitchen_background_french_realistic. Professional studio lighting, realistic stainless steel, enamel, glass, and bread textures. No people, no hands, no text.", "description": "A premium 3D CGI scene showing raw cinnamon rolls inside an open oven, ready for baking.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_oven_baking_scene": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_oven_baking_scene_df237121-450e-4873-bb3a-e03498972ab6.webp", "type": "image", "prompt": "Generate a photorealistic 3D CGI vertical 9:16 scene for a mobile baking game. A premium cream-colored enamel professional oven with the door closed, shown from a three-quarter front angle. Through the clear oven glass, visible under warm amber interior lighting, is a dark rectangular pan containing six cinnamon rolls mid-bake. The dough is puffed and mostly pale with subtle light-golden edges starting to form, indicating they are partially cooked. Realistic glass reflections on the oven door, detailed metal racks, and polished architectural lighting. Match the artisan aesthetic of image 1. No people, no hands, no text, no UI.", "description": "A vertical 3D CGI scene of cinnamon rolls mid-bake inside a closed cream enamel oven with a warm interior glow.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_oven_baked_scene": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_oven_baked_scene_1028e13c-037a-4d95-9945-f5c612a316fb.webp", "type": "image", "prompt": "Generate a 9:16 tall premium photorealistic 3D CGI bakery scene. A cream enamel professional bakery oven door is fully open, revealing a dark rectangular pan on the rack. The pan contains six fully baked, golden-brown cinnamon rolls, glossy with thick amber caramel glaze and scattered walnuts. The rolls show realistic soft, springy bread textures. A warm golden glow emanates from the oven interior. Three-quarter front view, elegant professional boutique bakery lighting, consistent with the warm ivory and espresso palette of the Twin Brothers Coffee aesthetic. No people, no hands, no text, no UI.", "description": "A premium 3D CGI vertical scene of fully baked cinnamon rolls in an open oven.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_pan_raw_proof": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_pan_raw_proof_e0935c22-4eed-41d8-bf31-d2f48abceabf.webp", "type": "image", "prompt": "Photorealistic premium 3D CGI food asset, top three-quarter view of a dark rectangular steel baking pan containing exactly six pale ivory unbaked cinnamon-roll dough portions after second proof. The dough is risen and pillowy with clear cinnamon spirals and walnut pieces. Visible shallow amber caramel syrup at the pan bottom. No browned or baked crust. Realistic dough skin texture, pan metal reflections, warm artisan bakery studio lighting. Transparent background, high resolution, detailed professional quality, no oven, no people, no hands, no text.", "description": "A dark rectangular baking pan with six raw, proofed cinnamon roll dough portions in caramel syrup.", "subtype": "object", "aspect_ratio": [1263, 793], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_step1_prep_scene": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_step1_prep_scene_28bb4bf1-f07e-4859-9618-ea227f62943b.webp", "type": "image", "prompt": "Premium photorealistic 3D CGI scene for a 9:16 vertical mobile game. An artisan bakery prep counter featuring a large polished stainless steel mixing bowl in the center. Arranged neatly around the bowl are seven realistic ingredient vessels: a flour bag, a yeast bowl, a sugar bag, a salt pinch bowl, a milk bottle, a glass measuring tumbler of water, and an egg carton. The background shows a softly defocused warm walnut and cream boutique bakery interior. High-end architectural lighting, realistic materials (metal reflections, paper textures, liquid transparency). No people, no hands, no UI elements. Match the premium 3D style of kitchen_background_realistic and twin_brothers_3d_bakery_interior.", "description": "Artisan bakery prep scene with a mixing bowl and all raw ingredients for the first step of baking.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_pour_flour_scene": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_pour_flour_scene_17d3b555-4f4f-4051-b5d6-b78a679b34e4.webp", "type": "image", "prompt": "Generate a photorealistic premium 3D CGI culinary scene for a vertical mobile game, close three-quarter top view. An open flour vessel is positioned above the center of the large stainless bowl `mixing_bowl_large`, with a natural loose cascade of fine white flour falling down into the bowl. The vessel stays above and outside the bowl. Realistic gravity, powder particles naturally scatter and mound at the bottom of the bowl. Professional kitchen surface with warm boutique bakery lighting, tactile steel reflections and realistic flour textures. Match the premium 3D aesthetic of `cinnamon_roll_3d_step1_prep_scene`. No people, no hands, no text, no UI elements.", "description": "A close-up 3D CGI scene of flour being poured from a container into a stainless steel mixing bowl.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_pour_milk_scene": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_pour_milk_scene_7e44fb5d-0aac-4a58-8aad-dec5dc64d1a6.webp", "type": "image", "prompt": "Photorealistic premium 3D CGI culinary still for a vertical artisan bakery game: a glass measuring jug is tilted above the center of a large stainless steel mixing bowl, pouring a smooth narrow stream of warm milk vertically into the bowl. The jug remains above and outside the bowl, showing realistic graceful gravity and a soft splash at the impact point. Maintain the warm Twin Brothers Coffee bakery lighting and aesthetic. Highly detailed realistic steel, glass, and liquid textures. 9:16 tall image, no people, no hands, no text, no UI, no flat illustration.", "description": "A premium 3D CGI scene showing a glass jug pouring milk into a stainless steel bowl for the dough preparation stage.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_log_cutting_scene": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_log_cutting_scene_db71be7b-e4ad-4925-bb89-210f5c6d404d.webp", "type": "image", "prompt": "A premium photorealistic 3D CGI culinary scene for an artisan baking game. Show a long, unbaked pale ivory cinnamon dough log tightly rolled with a visible dark brown sugar-cinnamon and walnut spiral at the end, resting horizontally on a dark walnut wood pastry board. A polished stainless steel bench scraper knife is positioned vertically across the dough log at a 7cm mark, ready to slice. Show several subtle shallow guide marks along the top of the log, which is still whole. The dough has a realistic raw, slightly sticky texture with visible walnut-cinnamon layers; no golden baked crust yet. Warm bakery lighting, three-quarter close-up view, professional studio photography style, high detail, no people, no hands, no text.", "description": "A 3D CGI scene showing a raw cinnamon roll dough log on a pastry board being measured for cutting.", "subtype": "background", "aspect_ratio": [1264, 843], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_shaggy_dough": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_shaggy_dough_eb6f9ff8-02a7-405a-baef-fbe70736f9c0.webp", "type": "image", "prompt": "Generate a photorealistic premium 3D CGI gameplay scene in 9:16 portrait aspect ratio. First-person point-of-view (POV) looking down at a professional stainless-steel mixing bowl on a warm wooden bakery counter. The baker's own hands are visible from the bottom edges, naturally interacting with the contents. Inside the bowl is raw, pale, moist, and irregular shaggy bread dough being combined. The background shows a soft-focus upscale Taipei bakery interior with warm natural lighting. High-end editorial food photography style, focus on realistic textures of the dough and metal reflections. No UI, no text, no floating objects.", "description": "First-person POV gameplay scene of the baker's hands mixing raw shaggy dough in a stainless steel bowl.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_windowpane_dough": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_windowpane_dough_b4135f1f-4080-4a7a-9e09-769b45b23093.webp", "type": "image", "prompt": "Create a photorealistic, premium 3D CGI gameplay scene for a baking game. 9:16 portrait orientation, immersive first-person POV looking down at a prep counter. Only the baker's own hands and partial forearms are visible from the bottom edges, stretching a small piece of raw, pale, elastic bread dough between fingers to demonstrate the translucent gluten 'windowpane test'. The hands are held over a large stainless-steel mixing bowl containing the rest of the raw dough. The environment is a warmly lit boutique Taipei bakery, softly out of focus in the background. Natural cafe daylight, refined material textures (stainless steel, warm wood), editorial food photography style. No face, no other people, no text, no UI, no logos, no borders, no baked goods. High resolution, 3D CGI rendered style.", "description": "First-person POV gameplay scene showing the baker's hands performing the windowpane test on raw cinnamon roll dough.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_adding_butter": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_adding_butter_e3a1d775-bc38-4a8b-9165-bd9677ae2a67.webp", "type": "image", "prompt": "Create a photorealistic, premium 3D CGI gameplay scene for Step 2 of the Twin Brothers Coffee cinnamon-roll baking game. Generate a 9:16 tall image for an immersive first-person baker's-eye POV looking down over a prep counter. Show only the baker's own hands and partial forearms entering from the bottom edges: one hand gently adding a few small cubes of softened unsalted butter into a bowl of raw pale bread dough while the other hand steadies the large stainless-steel mixing bowl. The dough is unbaked, pale, and moist, clearly in the raw bread stage. Boutique Taipei coffee-bakery setting in the background with soft warm bokeh, realistic stainless steel reflections, and a wood countertop. Use natural lighting and a refined high-end food editorial aesthetic. Ensure credible hand anatomy and natural first-person action. No text, no logos, no UI, no inset images, no borders, and no baked pastries.", "description": "A first-person POV gameplay scene showing the baker adding cubes of butter to raw dough in a stainless steel bowl.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_kneading_smooth_dough": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_kneading_smooth_dough_39a270aa-bbf3-4d18-a36a-b03c7d56854e.webp", "type": "image", "prompt": "Generate a photorealistic, premium 3D CGI gameplay scene in 9:16 portrait aspect ratio. Immersive first-person baker POV looking down at two realistic hands kneading softened butter into a smooth, elastic, pale raw bread dough inside a large stainless-steel bowl. Only the baker's hands and forearms are visible from the bottom/side edges. The dough is raw, smooth, and springy, not baked. Setting is a luxury Taipei bakery with warm wood counters and soft-focus background. High-end food editorial photography style, warm natural lighting, detailed textures, no text, no UI, no borders.", "description": "A first-person POV gameplay scene showing hands kneading smooth, elastic raw dough in a stainless steel bowl.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_first_rise": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_first_rise_03878b20-abda-40fa-b98d-4ca2ed6aba45.webp", "type": "image", "prompt": "Generate a photorealistic, premium 3D CGI gameplay scene in 9:16 portrait aspect ratio. The view is an immersive first-person baker POV looking down at a large stainless-steel mixing bowl on a warm wood bakery counter. Inside the bowl is a single batch of pale raw bread dough that has risen to double its size, looking soft and airy. The baker's own hands and partial forearms are visible from the bottom/sides, gently lifting the edge of a light linen cloth cover to check the dough. No face or other people. The background is a high-end boutique Taipei bakery atmosphere, softly out of focus with warm natural daylight. Focus on tactile realistic dough textures, metallic reflections on the bowl, and premium photorealistic food editorial style. No text, UI, or borders.", "description": "First-person POV of the baker checking the risen dough under a cloth in a 3D CGI kitchen.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_step1_water_pour": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_step1_water_pour_2a643df8-3c11-4d40-9639-d2a03ed7c866.webp", "type": "image", "prompt": "Create a photorealistic, premium 3D CGI 9:16 vertical in-game action scene. First-person baker viewpoint looking down at a professional stainless-steel mixing bowl on a warm walnut bakery counter. Only natural baker hands and forearms are visible, no face or body. A clear glass measuring cup is tilted above the center of the bowl, with a thin continuous stream of clear water falling vertically into the bowl. Realistic gravity, tactile glass and steel materials, soft bakery lighting. The background is a warm, softly out-of-focus coffee shop. High-quality render, cinematic lighting, no UI or text. Match the 3D CGI style of kitchen_background and mixing_bowl_large.", "description": "First-person POV of pouring water into the mixing bowl for the cinnamon roll dough preparation.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_step1_sugar_pour": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_step1_sugar_pour_9733ab19-fd65-4ee6-b4b2-3446e57cdbc9.webp", "type": "image", "prompt": "A photorealistic, premium 3D CGI vertical 9:16 in-game action scene. First-person baker viewpoint looking down at a large stainless-steel mixing bowl on a warm walnut bakery counter. Only natural baker hands and forearms are visible, no face, no body. Show a small cream ceramic ramekin tilted directly above the center of the bowl, with a short cascade of distinct brown sugar crystals falling vertically into the bowl, forming a small granular mound. Authentic gravity, crisp dry crystal texture, NOT a liquid. Soft bakery lighting, tactile realistic ceramic and steel, warm coffee-shop background softly out of focus. No text, no UI, no collage, no frame.", "description": "First-person POV gameplay scene of brown sugar being poured from a ramekin into a stainless steel mixing bowl.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_step1_yeast_pour": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_step1_yeast_pour_6dba0589-9da9-4aa4-824f-40fb434f1bd4.webp", "type": "image", "prompt": "Create a photorealistic premium 3D CGI vertical 9:16 first-person baking action scene. The camera looks downward from the baker's viewpoint at a large stainless-steel mixing bowl centered on a warm walnut counter. Only realistic hands and forearms are visible. A small glass prep bowl is tilted over the center of the bowl, with tiny beige dry yeast granules scattering and tumbling into the bowl as distinct particles. High detail, authentic gravity, warm artisan bakery lighting, soft-focus Twin Brothers Coffee background. No text, no UI, no collage, no labels.", "description": "First-person POV gameplay scene showing yeast being poured into the mixing bowl.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_step1_salt_pour": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_step1_salt_pour_0ea373ef-8c5d-44bc-9d02-46c5dd35952c.webp", "type": "image", "prompt": "Photorealistic premium 3D CGI vertical 9:16 first-person baking action scene. View downward from the baker's eyes toward a large stainless-steel mixing bowl centered on a warm walnut worktop. Show only realistic hands and forearms, no face or person. A tiny ivory pinch bowl is gently tipped close above the bowl center, sprinkling a small pinch of fine white sea salt crystals into the bowl; clearly dry grains falling separately, not a liquid ribbon. Natural believable baking action, elegant warm bakery light, lifelike ceramics and metal textures, softly blurred Twin Brothers Coffee artisan bakery backdrop in the distance. Professional food photography quality, 3D rendered style matching the aesthetic of image 1.", "description": "First-person POV gameplay scene showing the baker's hands sprinkling salt into a stainless steel bowl.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_step1_egg_crack": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_step1_egg_crack_22f73e09-a4f4-445a-b635-f6d8a648a4a3.webp", "type": "image", "prompt": "Photorealistic premium 3D CGI vertical 9:16 first-person POV baking action scene for Twin Brothers Coffee cinnamon-roll dough. Looking down at a large stainless-steel mixing bowl on a warm walnut counter. Only realistic hands and forearms, no face or person. Show one white egg being cracked naturally over the center of the bowl: two separated shell halves in the hands, translucent egg white and intact golden yolk dropping into bowl, authentic gravity and scale. Bowl centered and fully visible, premium warm artisan bakery lighting, crisp realistic egg and steel, softly blurred cafe background. No text, no UI, no photo border, no collage, no unrelated finished pastry.", "description": "First-person POV gameplay scene of the baker cracking an egg into a stainless steel bowl, premium 3D CGI style.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_step1_flour_pour": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_step1_flour_pour_e9a60f5a-9806-4fff-b851-12746653afe1.webp", "type": "image", "prompt": "Create a photorealistic premium 3D CGI vertical 9:16 first-person POV baking action scene. View downward from the baker's eyes at a large stainless-steel mixing bowl centered on a warm walnut bakery counter. Show only realistic hands and forearms, no face or body. A small white ceramic bowl is gently tipped immediately above the center of the stainless mixing bowl; a soft cascade of loose white bread flour granules and powder falls by gravity, spreading and gathering as a small soft mound in the bowl. It must look like dry flour grains and powder, not a continuous liquid stream. Natural believable hand action, correct scale, warm bakery lighting, lifelike steel, ceramic, and flour textures, softly blurred cafe background. No text, labels, UI, borders, cards, collage or unrelated food. Match the lighting and material style of kitchen_background and mixing_bowl_large.", "description": "First-person POV gameplay scene of flour being poured into a stainless steel bowl for cinnamon roll dough preparation.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_pov_step1_milk_pour": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_pov_step1_milk_pour_f7435032-c9e2-4dd9-93ec-6eb0f378a320.webp", "type": "image", "prompt": "Create a photorealistic premium 3D CGI vertical 9:16 first-person POV baking action scene. View downward from baker's eyes toward a large stainless-steel mixing bowl centered on a warm walnut counter. Only realistic hands and forearms are visible, no face or body. Show a small clear glass jug with milk tilted naturally right above the bowl center, pouring a single smooth, narrow, continuous stream of liquid milk straight down into the bowl. Match the style and lighting of image 1. Authentic gravity, believable scale, and natural hand posture. Warm artisan bakery lighting, lifelike glass, milk and stainless steel textures, with a softly blurred specialty coffee shop in the background. No text, labels, UI, borders, or collage.", "description": "First-person POV of pouring milk from a glass jug into a stainless steel bowl for cinnamon roll dough.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_flour_pour_bowl": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_flour_pour_bowl_12b41324-a2ef-44d3-bd6d-15b4403cffac.webp", "type": "image", "prompt": "A photorealistic premium 3D CGI baking prop of a small ivory ceramic ingredient bowl filled with fine white bread flour. Natural three-quarter view, slightly flared rim, elegant handmade ceramic glaze. Professional studio lighting with soft shadows directly beneath. Isolated on a transparent background, no hands, no bags, no text, no labels. Matches the premium patisserie aesthetic of the existing kitchen assets.", "description": "An isolated ivory ceramic bowl filled with white bread flour for the baking preparation stage.", "subtype": "object", "aspect_ratio": [690, 501], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_sugar_pour_bowl": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_sugar_pour_bowl_b9bf1082-e99a-4fed-8ebe-2fb7b9713e03.webp", "type": "image", "prompt": "Create an isolated photorealistic premium 3D CGI baking prop: a small warm cream ceramic prep bowl filled with pale golden granulated sugar crystals. Viewed at a natural three-quarter angle with clearly visible sugar grains. Professional studio lighting, realistic bowl rim material with elegant glaze, soft studio shadow directly beneath. Transparent background, no hands, no bag, no text, no labels, no frame, no background. High resolution, 3D rendered asset for a luxury cooking game.", "description": "Small ivory ceramic prep bowl filled with golden sugar crystals for baking animations.", "subtype": "object", "aspect_ratio": [1157, 841], "compression_suggestion": "high", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_3d_milk_pour_jug": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_3d_milk_pour_jug_c47c40fc-540e-4271-bae8-257e6c5ea72c.webp", "type": "image", "prompt": "Create an isolated, photorealistic premium 3D CGI baking prop on a fully transparent background: a small clear glass measuring jug, partly filled with warm white milk, featuring a neat pouring spout and subtle realistic glass highlights. Natural three-quarter side view with depth, soft studio shadow directly beneath. Professional studio lighting, no hands, no text, no labels, no background, no frame. Match the 3D CGI style of 'milk_bottle' and 'kitchen_background_realistic'.", "description": "A premium 3D CGI clear glass milk jug for the dough preparation stage.", "subtype": "object", "aspect_ratio": [590, 525], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_step3_roll_dough_action": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_step3_roll_dough_action_279d53d3-8f87-4fc9-8588-496a97f871da.webp", "type": "image", "prompt": "A photorealistic premium 3D CGI vertical 9:16 first-person tabletop baking scene. The scene features a pale unbaked rectangular bread dough sheet centered on a dark walnut pastry board. A realistic tapered wooden rolling pin is actively pressing across the dough, with a small metal ruler resting nearby. Top-down oblique first-person camera perspective. Lighting is warm, inspired by a Taipei artisan bakery. Authentic raw dough texture with realistic flour dusting. Elegant espresso and brass color palette. Compose for a mobile screen, no hands, no people, no text, no UI. Match the visual style and lighting of 'twin_brothers_3d_bakery_interior'.", "description": "A 3D CGI first-person scene showing a wooden rolling pin flattening raw cinnamon roll dough on a pastry board.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_step3_spread_filling_action": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_step3_spread_filling_action_687146fc-a13f-4a70-a39b-b60537b56c26.webp", "type": "image", "prompt": "Create a photorealistic premium 3D CGI vertical 9:16 first-person tabletop baking scene. Show only real baking objects, no people, no hands, no text, no UI: an unbaked rectangular bread dough sheet on a dark walnut pastry board, partially coated with a thin even layer of melted butter, cinnamon brown sugar, and chopped walnuts. A realistic metal offset spatula is actively spreading the filling across the dough. Top-down/oblique first-person camera view, warm luxury bakery lighting, natural dough and walnut textures, crisp high detail, elegant espresso-and-brass palette. Large centered composition for a mobile screen. Match the style of kitchen_background and cinnamon_roll_step3_roll_dough_action.", "description": "A 3D CGI first-person view of spreading cinnamon-sugar filling on raw dough for the baking simulation.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_step3_roll_log_action": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_step3_roll_log_action_9c70e4d5-017a-4f08-a724-636710886e01.webp", "type": "image", "prompt": "Create a photorealistic premium 3D CGI vertical 9:16 first-person tabletop baking scene. Show only real baking objects, no people, no hands, no text, no UI: a cinnamon-sugar-and-walnut-filled raw dough sheet being tightly rolled into a short thick log, with a realistic tapered wooden rolling pin gently guiding the fold on a dark walnut pastry board. The spiral filling is visible only at the rolled end; dough remains pale and clearly unbaked. Top-down/oblique first-person camera, warm Twin Brothers Coffee Taipei bakery lighting, lifelike dough folds, natural walnut and cinnamon texture, crisp high-detail, elegant espresso-and-brass palette. Match the style and lighting of kitchen_background_realistic. Compose large centered work surface for a 9:16 aspect ratio. No border, no picture frame, no checkerboard, no collage, no finished baked roll.", "description": "A 3D CGI first-person scene showing a filled cinnamon roll dough sheet being rolled into a log on a pastry board.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_step3_cut_log_action": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_step3_cut_log_action_4e772c02-8e95-403c-9dce-ffd09a033c33.webp", "type": "image", "prompt": "Generate a photorealistic premium 3D CGI 9:16 vertical image of a first-person tabletop baking scene. Show only real baking objects, no people, no hands, no text, no UI. A raw cinnamon-filling and walnut dough log sits on a dark walnut pastry board with a stainless-steel ruler beside it. A sharp stainless-steel chef's knife is captured halfway through making a clean slice into the log, creating a 7cm thick uncooked cinnamon roll portion. The pale dough and dark cinnamon spiral must look moist, raw, and unbaked. Lighting is warm and sophisticated, matching the Twin Brothers Coffee Taipei bakery style. High resolution, professional studio lighting, realistic textures of flour and wood, no borders or collage.", "description": "A first-person 3D CGI scene of a chef's knife slicing a raw cinnamon dough log on a walnut board.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "low", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_step3_pov_roll_dough": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_step3_pov_roll_dough_91d02469-494f-4be0-8352-2454c432fabb.webp", "type": "image", "prompt": "Generate a photorealistic 3D CGI first-person POV shot for a mobile game. A pair of realistic human hands and wrists are visible, pressing and rolling a tapered wooden rolling pin across a rectangular sheet of pale, raw dough. The scene is set on a dark walnut pastry board with a light dusting of flour. Use warm, premium bakery lighting consistent with a Taipei-style coffee shop. The camera angle is at countertop height looking down at the hands. Ensure high-quality textures for the dough and wood, focusing on the physical contact between hands, pin, and dough. 9:16 aspect ratio, no UI, no text, no faces.", "description": "First-person POV of hands rolling out cinnamon roll dough with a wooden rolling pin.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_step3_pov_spread_filling": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_step3_pov_spread_filling_a7da4752-67d9-48bd-8abb-96f1a77d9489.webp", "type": "image", "prompt": "Photorealistic first-person bakery action scene, vertical 9:16 aspect ratio. Only realistic baker's hands and wrists are visible from the baker's POV. One hand actively moves a small metal offset spatula to spread a mixture of melted butter and dense cinnamon-brown sugar evenly over a raw rectangular dough sheet; the other hand gently steadies the walnut pastry board. Visible chopped walnuts, realistic food textures, and actual spatula contact spreading the filling. Lighting is warm and premium, matching the Twin Brothers Coffee Taipei bakery style. No face, no body, no text, no UI, no collage, no card border, absolutely no finished or baked rolls. High resolution 3D CGI style matching image 01M6D6G84F08X4JAW79H56D35F.", "description": "First-person POV of the baker's hands spreading cinnamon filling onto raw dough with a spatula.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_step3_pov_roll_log": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_step3_pov_roll_log_c7783278-1c48-4ff6-bdae-1ecf1009dd1d.webp", "type": "image", "prompt": "A premium 3D CGI photorealistic first-person bakery action shot, vertical 9:16 aspect ratio. Only realistic human hands and wrists are visible from the baker's viewpoint. The hands are carefully lifting the edge of a raw rectangular dough sheet filled with dark cinnamon-brown sugar and chopped walnuts, rolling it tightly into a long log on a walnut pastry board. Show the physical fold actively happening with the exposed spiral texture at the end. Use warm, high-end Taipei specialty coffee shop lighting with soft shadows and realistic food textures. Pale raw dough contrasting with moist filling. No face, no body, no text, no UI, no frames, no baked pastry, professional culinary photography style.", "description": "First-person POV of hands rolling the filled dough into a log for cinnamon rolls.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_step3_pov_cut_rolls": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_step3_pov_cut_rolls_97f5ee15-219a-48bd-a8ce-97aac67eb8c7.webp", "type": "image", "prompt": "Photorealistic first-person baking action, vertical 9:16 composition. Only realistic baker hands and wrists are visible from the baker's perspective. One hand is using a sharp professional chef knife to cut a raw cinnamon-sugar-walnut dough log into thick 7 cm portions on a walnut pastry board, while the other hand safely steadies the log. The knife is shown halfway through a cut, revealing several pale raw dough pieces with distinct, dark cinnamon-sugar spirals inside. Highly detailed textures for flour dust, soft dough, brushed steel knife, and grainy walnuts. Warm premium Taipei specialty coffee shop lighting with soft shadows. No face, no body, no text, no UI, no collage, no card frame, no checkerboard, no baked rolls.", "description": "First-person POV of the baker's hands slicing the raw dough log into individual cinnamon rolls.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "cinnamon_roll_step3_pov_cut_rolls_ready": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/cinnamon_roll_step3_pov_cut_rolls_ready_b30cb58f-f2ad-47b3-8364-9460be363e02.webp", "type": "image", "prompt": "Generate a photorealistic premium 3D CGI first-person bakery action scene in a 9:16 vertical composition. Show only realistic hands and wrists from the baker's viewpoint placing six freshly cut raw cinnamon-roll dough portions into a dark rectangular caramel-lined baking pan. Each portion is pale soft ivory, visibly unbaked with cinnamon-sugar and walnut spiral layers; none golden or baked. Background features a warm Taipei-style bakery counter and softly defocused walnut interior. Hands are actively arranging the pieces into neat rows. Cinematic premium food photography style with realistic dough, caramel syrup, walnut, and metal textures. No face, no body, no text.", "description": "First-person POV gameplay scene of the baker placing raw cinnamon roll portions into a caramel-lined baking pan.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html>\n<head>\n    <title>Twin Brothers Coffee: \u7389\u6842\u6372 | Taipei Cinnamon Roll Bakery</title>\n    <style>\n        /* CSS Reset */\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\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: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n        }\n\n        /* Custom Properties */\n        :root {\n            --transition-fast: 150ms;\n            --transition-normal: 300ms;\n            --transition-slow: 500ms;\n            --ease-out: cubic-bezier(0.0, 0, 0.2, 1);\n            --ease-bounce: cubic-bezier(0.68, -0.6, 0.32, 1.6);\n            --space-xs: 8px;\n            --space-sm: 16px;\n            --space-md: 24px;\n            --space-lg: 32px;\n            --space-xl: 48px;\n            --color-bg-primary: #F5E6D3;\n            --color-bg-secondary: #FFFAF0;\n            --color-accent-pink: #FFB6C1;\n            --color-accent-red: #FF6B6B;\n            --color-accent-blue: #4A90E2;\n            --color-accent-green: #90EE90;\n            --color-text-dark: #333333;\n            --color-text-light: #666666;\n        }\n\n        #game-world {\n            position: relative;\n            width: 100%;\n            height: 100%;\n            background-color: var(--color-bg-primary);\n            overflow: hidden;\n        }\n\n        #game-canvas {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            z-index: 1;\n            /* Prevent the browser from claiming touch drags as page scrolling/zoom gestures. */\n            touch-action: none;\n            -ms-touch-action: none;\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            z-index: 100;\n        }\n\n        .ui-overlay > * {\n            pointer-events: auto;\n        }\n\n        .top-bar {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: auto;\n            padding: var(--space-sm);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: var(--space-sm);\n            pointer-events: none;\n            z-index: 100;\n        }\n\n        .step-info {\n            position: absolute;\n            top: 20px;\n            right: 20px;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n            background: rgba(255, 255, 255, 0.8);\n            padding: 8px 20px;\n            border-radius: 25px;\n            backdrop-filter: blur(4px);\n            pointer-events: auto;\n        }\n\n        .step-title {\n            display: none !important;\n        }\n\n        .step-number {\n            font-size: 24px;\n            font-weight: bold;\n            color: var(--color-text-dark);\n        }\n\n        .progress-container {\n            display: none !important;\n        }\n\n        .bottom-bar {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            padding: 0 var(--space-md);\n            display: flex;\n            flex-direction: column;\n            gap: var(--space-sm);\n            align-items: center;\n            pointer-events: none;\n            z-index: 150;\n        }\n\n        .hint-text {\n            background: rgba(255, 255, 255, 0.98);\n            padding: 12px 28px;\n            border-radius: 0 0 20px 20px;\n            font-size: 22px;\n            font-weight: 600;\n            color: var(--color-text-dark);\n            text-align: center;\n            box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n            border: 3px solid var(--color-accent-pink);\n            border-top: none;\n            max-width: 85%;\n            pointer-events: auto;\n        }\n\n        .action-button {\n            position: absolute;\n            bottom: 20px;\n            left: 50%;\n            transform: translateX(-50%);\n            width: 240px;\n            height: 80px;\n            padding: 0;\n            font-size: 32px;\n            font-weight: 700;\n            color: white;\n            background: linear-gradient(to bottom, var(--color-accent-pink), #FF8C9F);\n            border: 4px solid white;\n            border-radius: 40px;\n            box-shadow: 0 8px 20px rgba(0,0,0,0.2);\n            cursor: pointer;\n            transition: all var(--transition-fast) var(--ease-out);\n            z-index: 160;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            pointer-events: auto;\n        }\n\n        .action-button:hover {\n            transform: translateX(-50%) translateY(-2px);\n            box-shadow: 0 10px 25px rgba(0,0,0,0.3);\n        }\n\n        .action-button:active {\n            transform: translateX(-50%) translateY(0);\n            box-shadow: 0 4px 10px rgba(0,0,0,0.2);\n        }\n\n        .action-button:disabled {\n            opacity: 0.5;\n            cursor: not-allowed;\n        }\n\n        .ingredient-list {\n            display: flex;\n            flex-direction: row;\n            overflow-x: auto;\n            gap: var(--space-sm);\n            padding: var(--space-sm);\n            width: 100%;\n            max-width: 100%;\n            background: rgba(255, 255, 255, 0.6);\n            backdrop-filter: blur(8px);\n            scrollbar-width: none;\n            border-bottom: 2px solid rgba(0,0,0,0.05);\n        }\n\n        .topping-item {\n            flex: 0 0 100px;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 4px;\n            padding: 8px;\n            background: rgba(255,255,255,0.8);\n            border-radius: 12px;\n            border: 3px solid transparent;\n            cursor: pointer;\n            transition: all 0.2s;\n        }\n\n        .topping-item.selected {\n            border-color: var(--color-accent-pink);\n            background: white;\n            transform: scale(1.05);\n            box-shadow: 0 4px 8px rgba(0,0,0,0.1);\n        }\n\n        .topping-icon {\n            width: 60px;\n            height: 60px;\n            object-fit: contain;\n        }\n\n        .topping-name {\n            font-size: 14px;\n            font-weight: 600;\n            color: var(--color-text-dark);\n        }\n\n        .ingredient-item {\n            flex: 0 0 160px; /* Fixed width for horizontal scrolling */\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            gap: 4px;\n            padding: var(--space-xs);\n            background: rgba(255,255,255,0.9);\n            border-radius: 16px;\n            box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n            border: 3px solid transparent;\n            transition: border-color var(--transition-fast);\n        }\n\n        .ingredient-item.correct {\n            border-color: var(--color-accent-green);\n        }\n\n        .ingredient-icon {\n            width: 100px;\n            height: 100px;\n            object-fit: contain;\n            image-rendering: -webkit-optimize-contrast;\n        }\n\n        .ingredient-name {\n            font-size: 20px;\n            font-weight: 600;\n            color: var(--color-text-dark);\n        }\n\n        .ingredient-weight {\n            font-size: 18px;\n            color: var(--color-text-light);\n            margin: 2px 0;\n        }\n\n        .ingredient-weight.correct {\n            color: var(--color-accent-green);\n        }\n\n        .add-button {\n            min-width: 80px;\n            min-height: 80px;\n            font-size: 32px;\n            font-weight: bold;\n            background: var(--color-accent-pink);\n            color: white;\n            border: none;\n            border-radius: 50%;\n            box-shadow: 0 2px 4px rgba(0,0,0,0.2);\n            cursor: pointer;\n            transition: transform var(--transition-fast) var(--ease-out);\n        }\n\n        .add-button:active {\n            transform: scale(0.95);\n        }\n\n        .modal {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            background: white;\n            border-radius: 32px;\n            padding: var(--space-xl);\n            box-shadow: 0 8px 32px rgba(0,0,0,0.3);\n            max-width: 600px;\n            max-height: 80%;\n            overflow-y: auto;\n            z-index: 200;\n            text-align: center;\n        }\n\n        .modal-backdrop {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0,0,0,0.5);\n            z-index: 199;\n        }\n\n        .modal-title {\n            font-size: 40px;\n            font-weight: 600;\n            color: var(--color-text-dark);\n            margin-bottom: var(--space-md);\n        }\n\n        .modal-content {\n            font-size: 24px;\n            color: var(--color-text-dark);\n            line-height: 1.6;\n        }\n\n        .score-display {\n            font-size: 48px;\n            font-weight: bold;\n            color: var(--color-accent-pink);\n            margin: var(--space-md) 0;\n        }\n\n        .rating-display {\n            font-size: 32px;\n            font-weight: 600;\n            color: var(--color-accent-green);\n            margin: var(--space-sm) 0;\n        }\n\n        .modal .action-button {\n            position: relative !important;\n            bottom: auto !important;\n            left: auto !important;\n            transform: none !important;\n            margin: 32px auto 0 !important;\n        }\n\n        .hidden {\n            display: none !important;\n        }\n\n        @media (prefers-reduced-motion: reduce) {\n            * {\n                animation-duration: 0.01ms !important;\n                animation-iteration-count: 1 !important;\n                transition-duration: 0.01ms !important;\n            }\n        }\n        /* Grand Luxe Limited Edition visual treatment */\n        :root {\n            --color-bg-primary: #f3e9dc;\n            --color-bg-secondary: #fffaf2;\n            --color-accent-pink: #b86d79;\n            --color-accent-red: #8b4351;\n            --color-accent-blue: #627c89;\n            --color-accent-green: #849b78;\n            --color-text-dark: #49333a;\n            --color-text-light: #806c68;\n            --luxe-gold: #c9a96a;\n            --luxe-gold-light: #f5e4b7;\n            --luxe-wine: #713b49;\n        }\n        .top-bar {\n            top: 14px !important;\n            left: 0;\n            padding: 0 36px !important;\n            gap: 9px !important;\n        }\n        .brand-name {\n            color: #fff9eb;\n            font: 600 25px/1.05 Georgia, 'Times New Roman', serif;\n            letter-spacing: 4px;\n            text-shadow: 0 2px 8px rgba(46, 26, 26, .75);\n            text-align: center;\n            pointer-events: none;\n        }\n        .brand-name span {\n            display: block;\n            margin-top: 5px;\n            color: #f4dfad;\n            font: 600 12px/1.2 'Segoe UI', sans-serif;\n            letter-spacing: 4px;\n        }\n        .step-info {\n            position: relative !important;\n            top: auto !important;\n            right: auto !important;\n            width: min(620px, 94%);\n            min-height: 55px;\n            justify-content: space-between !important;\n            padding: 10px 23px !important;\n            color: #49333a;\n            background: linear-gradient(135deg, rgba(255,252,245,.96), rgba(246,231,204,.94)) !important;\n            border: 1px solid rgba(201,169,106,.95);\n            border-radius: 17px !important;\n            box-shadow: 0 5px 18px rgba(54,35,28,.2), inset 0 0 0 3px rgba(255,255,255,.55);\n            backdrop-filter: blur(8px);\n        }\n        .step-title {\n            display: inline !important;\n            color: #543843;\n            font: 600 19px/1.2 Georgia, 'Times New Roman', serif;\n            letter-spacing: .3px;\n        }\n        .step-number {\n            color: #855b37 !important;\n            font: 700 18px/1 'Segoe UI', sans-serif !important;\n            letter-spacing: 1px;\n        }\n        .progress-container {\n            display: block !important;\n            width: min(580px, 86%);\n            height: 13px;\n            padding: 2px;\n            overflow: hidden;\n            background: rgba(255,250,240,.78);\n            border: 1px solid rgba(201,169,106,.9);\n            border-radius: 12px;\n            box-shadow: 0 3px 10px rgba(44,28,25,.18);\n        }\n        .progress-bar {\n            height: 100%;\n            min-width: 10px;\n            background: linear-gradient(90deg, #a94d61, #d58b87 55%, #e8c985);\n            border-radius: 10px;\n            box-shadow: 0 0 9px rgba(223,177,119,.65);\n            transition: width .35s ease;\n        }\n        .bottom-bar {\n            display: none !important;\n            top: 180px !important;\n            bottom: auto !important;\n            left: 0;\n            width: 100%;\n            padding: 0 36px !important;\n        }\n        .hint-text {\n            max-width: 88% !important;\n            padding: 11px 25px !important;\n            color: #563c42 !important;\n            background: linear-gradient(135deg, rgba(255,252,244,.96), rgba(246,233,214,.94)) !important;\n            border: 1px solid var(--luxe-gold) !important;\n            border-radius: 15px !important;\n            box-shadow: 0 5px 16px rgba(52,33,28,.18), inset 0 0 0 2px rgba(255,255,255,.6) !important;\n            font: 600 17px/1.35 'Segoe UI', sans-serif !important;\n        }\n        .action-button {\n            bottom: 22px !important;\n            width: 290px !important;\n            height: 82px !important;\n            color: #fffaf0 !important;\n            background: linear-gradient(145deg, #8f4c58, #673341 70%, #542d39) !important;\n            border: 3px solid #e8cf94 !important;\n            border-radius: 25px !important;\n            box-shadow: 0 8px 21px rgba(48,27,31,.34), inset 0 1px 0 rgba(255,255,255,.35), inset 0 0 0 2px rgba(255,255,255,.08) !important;\n            font: 700 24px/1 Georgia, 'Times New Roman', serif !important;\n            letter-spacing: 1.2px;\n        }\n        .action-button:hover {\n            transform: translateX(-50%) translateY(-2px) !important;\n            box-shadow: 0 11px 26px rgba(48,27,31,.4), inset 0 1px 0 rgba(255,255,255,.4) !important;\n        }\n        .action-button:active {\n            transform: translateX(-50%) translateY(1px) !important;\n        }\n        .ingredient-list {\n            background: rgba(255,250,240,.88) !important;\n            border-top: 1px solid rgba(201,169,106,.72);\n            border-bottom: 1px solid rgba(201,169,106,.72) !important;\n            box-shadow: 0 6px 18px rgba(53,34,27,.16);\n        }\n        .ingredient-list[style*=\"top: 140px\"] {\n            top: 238px !important;\n        }\n        .topping-item, .ingredient-item {\n            background: linear-gradient(145deg, rgba(255,255,255,.98), rgba(247,237,220,.96)) !important;\n            border: 1px solid rgba(201,169,106,.58) !important;\n            border-radius: 16px !important;\n            box-shadow: 0 4px 10px rgba(75,51,43,.12) !important;\n        }\n        .topping-item.selected {\n            border: 2px solid #b9874d !important;\n            box-shadow: 0 0 0 3px rgba(239,216,164,.55), 0 6px 14px rgba(75,51,43,.18) !important;\n        }\n        .topping-name, .ingredient-name {\n            color: #573b42 !important;\n            font-family: Georgia, 'Times New Roman', serif !important;\n        }\n        .modal {\n            color: #fff0d4;\n            background: linear-gradient(145deg, #50372a, #261a16 78%) !important;\n            border: 2px solid var(--luxe-gold);\n            box-shadow: 0 14px 40px rgba(12,7,5,.58), inset 0 2px 0 rgba(255,246,224,.2), inset 0 -5px 0 rgba(0,0,0,.28) !important;\n        }\n        .modal-title {\n            color: #f3d69a !important;\n            font-family: Georgia, 'Times New Roman', serif;\n        }\n        .modal-content { color: #fff0d4 !important; }\n        .rating-display, .score-display {\n            color: #e4bd72 !important;\n        }\n        .modal-backdrop {\n            background: rgba(36,22,27,.62) !important;\n            backdrop-filter: blur(4px);\n        }\n        @media (max-width: 600px) {\n            .brand-name { font-size: 21px; letter-spacing: 3px; }\n            .brand-name span { font-size: 10px; letter-spacing: 2.5px; }\n            .top-bar { padding: 0 20px !important; }\n            .step-title { font-size: 16px; }\n            .hint-text { font-size: 15px !important; }\n            .action-button { width: 250px !important; height: 76px !important; }\n        }\n        /* Dimensional Grand Luxe interface: raised enamel surfaces and layered depth */\n        .step-info, .hint-text, .ingredient-list, .topping-item, .ingredient-item, .modal {\n            box-shadow: 0 10px 24px rgba(45, 29, 27, .22),\n                        inset 0 2px 0 rgba(255, 255, 255, .78),\n                        inset 0 -2px 0 rgba(132, 91, 56, .08) !important;\n        }\n        .action-button {\n            position: absolute;\n            background-image: linear-gradient(155deg, #a95e69 0%, #7a3f4c 48%, #542d39 100%) !important;\n            box-shadow: 0 12px 0 #462631, 0 16px 26px rgba(48, 27, 31, .34),\n                        inset 0 3px 0 rgba(255, 255, 255, .32),\n                        inset 0 -4px 0 rgba(40, 20, 24, .2) !important;\n        }\n        .action-button:active {\n            box-shadow: 0 5px 0 #462631, 0 8px 16px rgba(48, 27, 31, .3),\n                        inset 0 2px 0 rgba(255, 255, 255, .25) !important;\n        }\n        .ingredient-item, .topping-item {\n            transform: translateZ(0);\n            transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;\n        }\n        .ingredient-item:hover, .topping-item:hover {\n            transform: translateY(-4px);\n            box-shadow: 0 10px 18px rgba(75, 51, 43, .2),\n                        inset 0 2px 0 rgba(255, 255, 255, .85) !important;\n        }\n        .ingredient-icon, .topping-icon {\n            filter: drop-shadow(0 5px 5px rgba(65, 42, 32, .2));\n        }\n        .modal {\n            border-radius: 30px !important;\n        }\n        .photo-mode-panel {\n            position: absolute;\n            inset: 0;\n            z-index: 300;\n            pointer-events: none;\n            color: #fff9eb;\n            text-align: center;\n        }\n        .photo-mode-panel > * { pointer-events: auto; }\n        .photo-mode-heading {\n            top: 28px !important;\n            left: 24px !important;\n            width: calc(100% - 144px) !important;\n            padding: 15px 18px !important;\n            text-align: left;\n            font-size: 24px !important;\n        }\n        .photo-mode-heading span { font-size: 16px !important; letter-spacing: .2px !important; }\n        .photo-mode-close {\n            position: absolute;\n            top: 28px;\n            right: 20px;\n            min-width: 96px;\n            min-height: 96px;\n            margin: 0;\n            padding: 10px;\n            font-size: 20px;\n            pointer-events: auto;\n        }\n        .photo-viewfinder, .photo-flash { pointer-events: none !important; }\n        .photo-viewfinder {\n            position: absolute;\n            left: 7%;\n            top: 23%;\n            width: 86%;\n            height: 55%;\n            border: 2px solid rgba(255,248,226,.6);\n            border-radius: 18px;\n            box-shadow: 0 0 0 9999px rgba(12,8,6,.08), inset 0 0 28px rgba(255,249,230,.08);\n            pointer-events: none;\n        }\n        .photo-viewfinder i {\n            position: absolute;\n            width: 34px;\n            height: 34px;\n            border-color: rgba(255,239,198,.95);\n            border-style: solid;\n        }\n        .photo-viewfinder i:nth-child(1) { top:-2px;left:-2px;border-width:3px 0 0 3px;border-radius:14px 0 0 0; }\n        .photo-viewfinder i:nth-child(2) { top:-2px;right:-2px;border-width:3px 3px 0 0;border-radius:0 14px 0 0; }\n        .photo-viewfinder i:nth-child(3) { bottom:-2px;left:-2px;border-width:0 0 3px 3px;border-radius:0 0 0 14px; }\n        .photo-viewfinder i:nth-child(4) { bottom:-2px;right:-2px;border-width:0 3px 3px 0;border-radius:0 0 14px 0; }\n        .photo-flash {\n            position: absolute;\n            inset: 0;\n            z-index: 8;\n            opacity: 0;\n            background: #fffdf4;\n            pointer-events: none;\n        }\n        .photo-flash.flash-active { animation: photo-flash-burst 420ms ease-out forwards; }\n        @keyframes photo-flash-burst {\n            0% { opacity: 0; }\n            12% { opacity: .96; }\n            32% { opacity: .72; }\n            100% { opacity: 0; }\n        }\n        .photo-mode-controls {\n            position: absolute;\n            left: 18px;\n            right: 18px;\n            bottom: 20px;\n            display: flex;\n            flex-direction: column;\n            gap: 8px;\n            pointer-events: auto;\n        }\n        .photo-control-row {\n            display: grid;\n            grid-template-columns: 82px 96px minmax(100px, 1fr) 96px;\n            align-items: center;\n            gap: 8px;\n            min-height: 96px;\n            padding: 0 10px;\n            border: 1px solid rgba(215,187,126,.6);\n            border-radius: 18px;\n            background: rgba(38,26,20,.82);\n            box-shadow: 0 6px 18px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.15);\n        }\n        .photo-control-row label {\n            color: #f8e4b5;\n            text-align: left;\n            font: 700 13px/1.25 'Segoe UI', sans-serif;\n            letter-spacing: .6px;\n        }\n        .photo-control-row label span { display: block; margin-top: 5px; color: #fffaf0; font-size: 16px; }\n        .photo-adjust {\n            width: 96px;\n            height: 96px;\n            border: 2px solid #e3c987;\n            border-radius: 16px;\n            color: #fff5dc;\n            background: linear-gradient(145deg,#a96c40,#5d3929);\n            box-shadow: 0 5px 0 #3f291f, inset 0 2px 0 rgba(255,255,255,.28);\n            font: 700 38px/1 Georgia,serif;\n            touch-action: manipulation;\n        }\n        .photo-control-row input[type=\"range\"] { width: 100%; height: 54px; accent-color: #e4bd72; touch-action: pan-x; }\n        .photo-actions { display: flex; align-items: center; justify-content: center; min-height: 112px; gap: 18px; }\n        .photo-feedback {\n            flex: 1;\n            color: #fff0d2;\n            text-align: right;\n            font: 600 16px/1.35 'Segoe UI','Noto Sans TC',sans-serif;\n            text-shadow: 0 2px 5px rgba(0,0,0,.8);\n        }\n        #photo-shutter {\n            flex: 0 0 112px;\n            width: 112px;\n            height: 112px;\n            border: 6px double #fff5dd;\n            border-radius: 50%;\n            color: #fffaf0;\n            background: radial-gradient(circle at 38% 28%, #dca66a, #74452e 72%);\n            box-shadow: 0 6px 0 #3f291f, 0 10px 20px rgba(0,0,0,.4), inset 0 2px 0 rgba(255,255,255,.42);\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            gap: 2px;\n            cursor: pointer;\n            touch-action: manipulation;\n        }\n        #photo-shutter span { font: 42px/1 Arial,sans-serif; }\n        #photo-shutter small { font: 700 11px/1 'Segoe UI',sans-serif; letter-spacing: 1px; }\n        #photo-shutter:active, .photo-adjust:active { transform: translateY(3px); }\n        #game-world.photo-mode .photo-mode-panel { background: linear-gradient(180deg,rgba(16,10,8,.18),rgba(16,10,8,.08) 55%,rgba(16,10,8,.45)); }\n        @media (max-width: 600px) {\n            .photo-mode-heading { top: 18px !important; left: 14px !important; width: calc(100% - 126px) !important; padding: 12px !important; font-size: 18px !important; }\n            .photo-mode-heading span { font-size: 13px !important; }\n            .photo-mode-close { top: 18px; right: 12px; }\n            .photo-mode-controls { left: 8px; right: 8px; bottom: 14px; gap: 6px; }\n            .photo-control-row { grid-template-columns: 68px 88px minmax(70px,1fr) 88px; gap: 5px; padding: 0 6px; }\n            .photo-adjust { width: 88px; height: 88px; }\n            .photo-feedback { font-size: 13px; }\n        }\n        .photo-mode-panel.hidden { display: none !important; }\n        .photo-mode-heading {\n            position: absolute;\n            top: 52px;\n            left: 8%;\n            width: 84%;\n            padding: 18px 20px;\n            color: #fff9eb;\n            background: linear-gradient(135deg, rgba(64,34,39,.78), rgba(113,59,73,.72));\n            border: 2px solid #d8ba7d;\n            border-radius: 20px;\n            box-shadow: 0 8px 24px rgba(36,22,27,.28), inset 0 1px 0 rgba(255,255,255,.3);\n            font: 600 25px/1.3 Georgia, 'Times New Roman', serif;\n            letter-spacing: 1px;\n            text-shadow: 0 2px 6px rgba(0,0,0,.4);\n        }\n        .photo-mode-heading span {\n            display: block;\n            margin-top: 6px;\n            color: #f4dfad;\n            font: 500 16px/1.5 'Segoe UI', 'Noto Sans TC', sans-serif;\n            letter-spacing: .3px;\n        }\n        .photo-mode-footer {\n            position: absolute;\n            left: 7%;\n            bottom: 32px;\n            width: 86%;\n            padding: 16px 20px;\n            pointer-events: auto;\n            color: #543843;\n            background: linear-gradient(135deg, rgba(255,252,244,.97), rgba(246,233,214,.96));\n            border: 2px solid var(--luxe-gold);\n            border-radius: 18px;\n            box-shadow: 0 8px 22px rgba(36,22,27,.3), inset 0 1px 0 rgba(255,255,255,.8);\n            font: 600 16px/1.45 'Segoe UI', 'Noto Sans TC', sans-serif;\n        }\n        .photo-mode-close {\n            display: block;\n            min-width: 220px;\n            min-height: 72px;\n            margin: 12px auto 0;\n            padding: 12px 24px;\n            color: #fffaf0;\n            background: linear-gradient(145deg, #8f4c58, #673341 70%, #542d39);\n            border: 2px solid #d8ba7d;\n            border-radius: 18px;\n            box-shadow: 0 5px 0 #462631, 0 8px 16px rgba(48,27,31,.25);\n            font: 700 18px Georgia, 'Times New Roman', serif;\n            cursor: pointer;\n        }\n        .photo-mode-close:active { transform: translateY(2px); box-shadow: 0 3px 0 #462631; }\n        .photo-studio-button {\n            margin-top: 12px !important;\n            color: #573b42 !important;\n            background: linear-gradient(145deg, #fffdf7, #ead9b8) !important;\n            border-color: #c9a96a !important;\n            box-shadow: 0 5px 0 #a88a55, 0 8px 16px rgba(48,27,31,.2), inset 0 2px 0 rgba(255,255,255,.8) !important;\n            font-size: 20px !important;\n        }\n        #game-world.photo-mode .ui-overlay > :not(#photo-mode-panel) {\n            display: none !important;\n        }\n        @media (max-width: 600px) {\n            .photo-mode-heading { top: 38px; font-size: 20px; }\n            .photo-mode-heading span { font-size: 14px; }\n            .photo-mode-footer { bottom: 22px; font-size: 14px; }\n        }\n        /* Readability pass: larger, sharper UI typography for the fixed 720x1280 game frame. */\n        body, html, button, .ui-overlay {\n            font-family: 'Noto Sans TC', 'Segoe UI', Tahoma, sans-serif;\n            -webkit-font-smoothing: antialiased;\n            text-rendering: optimizeLegibility;\n        }\n        .brand-name { font-size: 32px; line-height: 1.2; }\n        .brand-name span { font-size: 18px; line-height: 1.35; }\n        .step-title { font-size: 26px !important; line-height: 1.3; }\n        .step-number { font-size: 24px !important; }\n        .hint-text { font-size: 24px !important; line-height: 1.45 !important; padding: 16px 28px !important; }\n        .action-button { font-size: 28px !important; }\n        .topping-name { font-size: 20px !important; line-height: 1.3; }\n        .ingredient-name { font-size: 24px !important; line-height: 1.3; }\n        .ingredient-weight { font-size: 22px; }\n        .modal-title { font-size: 46px !important; }\n        .modal-content { font-size: 28px; line-height: 1.55; }\n        .score-display { font-size: 54px; }\n        .rating-display { font-size: 38px; }\n        .photo-mode-heading { font-size: 30px; line-height: 1.4; }\n        .photo-mode-heading span { font-size: 20px; }\n        .photo-mode-footer { font-size: 20px; line-height: 1.6; }\n        #game-canvas { image-rendering: auto; }\n        .ui-overlay { text-shadow: 0 1px 0 rgba(255,255,255,.28); }\n        .step-info, .hint-text, .ingredient-list { backdrop-filter: blur(14px) saturate(1.2); }\n        .ingredient-item, .topping-item { border-radius: 20px !important; }\n        button { -webkit-tap-highlight-color: transparent; }\n        .photo-mode-close { font-size: 23px; }\n        .topping-item { flex-basis: 120px; gap: 8px; padding: 12px; }\n        .topping-icon { width: 72px; height: 72px; }\n        @media (max-width: 600px) {\n            .brand-name { font-size: 27px; }\n            .brand-name span { font-size: 16px; }\n            .step-title { font-size: 22px !important; }\n            .step-number { font-size: 22px !important; }\n            .hint-text { font-size: 21px !important; }\n            .photo-mode-heading { font-size: 24px; }\n            .photo-mode-heading span { font-size: 17px; }\n            .photo-mode-footer { font-size: 18px; }\n        }\n        /* Twin Brothers Coffee palette: warm espresso, cinnamon, cream and brass. */\n        :root {\n            --color-bg-primary: #3a2922;\n            --color-bg-secondary: #fff8ed;\n            --color-accent-pink: #b97845;\n            --color-accent-red: #81482f;\n            --color-accent-blue: #65716b;\n            --color-accent-green: #82936b;\n            --color-text-dark: #382820;\n            --color-text-light: #78685c;\n            --luxe-gold: #c7a66a;\n            --luxe-gold-light: #f1dfb5;\n            --luxe-wine: #49352b;\n        }\n        .brand-name { color: #fff8ec; text-shadow: 0 2px 10px rgba(35,22,15,.88); }\n        .brand-name span { color: #f2d7a0; }\n        .step-info, .hint-text { background: linear-gradient(145deg, rgba(72,48,35,.96), rgba(35,24,20,.94)) !important; border: 2px solid #d1b477 !important; box-shadow: 0 8px 0 rgba(28,18,15,.82), 0 13px 26px rgba(20,12,9,.4), inset 0 2px 0 rgba(255,244,216,.32), inset 0 -3px 0 rgba(0,0,0,.24) !important; }\n        .step-title, .hint-text { color: #fff2d5 !important; }\n        .step-number { color: #f1d69b !important; }\n        .action-button { color: #fff9ee !important; background: linear-gradient(145deg, #bd7d4b, #925632 58%, #65402f) !important; border-color: #e4ca91 !important; box-shadow: 0 12px 0 #483126, 0 16px 26px rgba(35,22,15,.34), inset 0 3px 0 rgba(255,255,255,.35), inset 0 -4px 0 rgba(40,20,12,.2) !important; }\n        .action-button:active { box-shadow: 0 5px 0 #483126, 0 8px 16px rgba(35,22,15,.3) !important; }\n        .topping-item.selected { border-color: #b97845 !important; box-shadow: 0 0 0 3px rgba(185,120,69,.32), 0 6px 14px rgba(45,28,20,.2) !important; }\n        .topping-icon-emoji { display: block; width: 72px; height: 72px; font-size: 54px; line-height: 72px; text-align: center; }\n        .photo-mode-heading { background: linear-gradient(135deg, rgba(53,37,29,.92), rgba(112,73,48,.88)); border-color: #c7a66a; }\n        .modal-title { color: #f3d69a !important; }\n        /* Final tactile 3D treatment shared by every screen and interactive control. */\n        .step-info {\n            background: linear-gradient(145deg, rgba(77,52,37,.97), rgba(37,25,19,.98)) !important;\n            border: 2px solid #d2b477 !important;\n            box-shadow: 0 7px 0 rgba(22,14,10,.82), 0 13px 24px rgba(15,9,6,.38), inset 0 2px 0 rgba(255,246,224,.3), inset 0 -3px 0 rgba(0,0,0,.28) !important;\n        }\n        .step-title, .step-number { color: #fff0cf !important; text-shadow: 0 1px 2px rgba(0,0,0,.65); }\n        .progress-container {\n            background: linear-gradient(180deg, #241914, #5a3b2a) !important;\n            border: 2px solid #d3b678 !important;\n            box-shadow: 0 5px 0 rgba(22,14,10,.72), inset 0 2px 3px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,245,218,.38) !important;\n        }\n        .progress-bar { background: linear-gradient(180deg, #f0d99f 0%, #c89c5d 38%, #8d522f 100%) !important; box-shadow: inset 0 2px 0 rgba(255,250,232,.6), 0 0 10px rgba(226,182,112,.48) !important; }\n        .action-button {\n            background: linear-gradient(155deg, #d49a62 0%, #a9693e 42%, #633d2b 100%) !important;\n            border: 3px solid #ecd49d !important;\n            box-shadow: 0 11px 0 #422b20, 0 18px 28px rgba(18,10,6,.44), inset 0 3px 0 rgba(255,245,220,.4), inset 0 -4px 0 rgba(35,18,10,.42) !important;\n            text-shadow: 0 2px 3px rgba(25,12,7,.65);\n        }\n        .action-button:active { box-shadow: 0 5px 0 #422b20, 0 9px 16px rgba(18,10,6,.38), inset 0 2px 0 rgba(255,245,220,.28) !important; }\n        .photo-mode-heading { background: linear-gradient(145deg, rgba(76,51,35,.97), rgba(35,23,18,.98)) !important; box-shadow: 0 8px 0 rgba(18,12,9,.74), 0 13px 24px rgba(0,0,0,.36), inset 0 2px 0 rgba(255,245,218,.25) !important; }\n        .photo-mode-footer { color: #fff0d2 !important; background: linear-gradient(145deg, rgba(73,48,34,.97), rgba(31,21,17,.98)) !important; box-shadow: 0 8px 0 rgba(18,12,9,.8), 0 14px 26px rgba(20,12,8,.45), inset 0 2px 0 rgba(255,244,215,.24) !important; }\n    </style>\n    <link href=\"https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n</head>\n<body>\n    <div id=\"game-world\">\n        <canvas id=\"game-canvas\"></canvas>\n        <div class=\"ui-overlay\">\n            <div class=\"top-bar\">\n                <div class=\"brand-name\">TWIN BROTHERS COFFEE<span>\u7389\u6842\u6372 \u00b7 TAIPEI CINNAMON ROLL BAKERY</span></div>\n                <div class=\"step-info\">\n                    <span class=\"step-title\" id=\"step-title\">Prepare Ingredients</span>\n                    <span class=\"step-number\" id=\"step-number\">1/9</span>\n                </div>\n                <div class=\"progress-container\">\n                    <div class=\"progress-bar\" id=\"progress-bar\" style=\"width: 10%\"></div>\n                </div>\n            </div>\n            <div class=\"bottom-bar\">\n                <div class=\"hint-text\" id=\"hint-text\">Select and weigh ingredients</div>\n            </div>\n            <button class=\"action-button\" id=\"action-button\">Next</button>\n            <div id=\"step-content\"></div>\n            <div id=\"photo-mode-panel\" class=\"photo-mode-panel hidden\" aria-label=\"Cinnamon roll photo studio\">\n                <div class=\"photo-mode-heading\">PHOTO STUDIO \u00b7 \u7389\u6842\u6372<span>Frame your Twin Brothers Coffee cinnamon roll</span></div>\n                <div class=\"photo-viewfinder\" aria-hidden=\"true\"><i></i><i></i><i></i><i></i></div>\n                <div id=\"photo-flash\" class=\"photo-flash\" aria-hidden=\"true\"></div>\n                <button id=\"photo-mode-close\" class=\"photo-mode-close\" type=\"button\" aria-label=\"Return to finished game\">Back</button>\n                <div class=\"photo-mode-controls\">\n                    <div class=\"photo-control-row\">\n                        <label for=\"photo-zoom\">ZOOM <span id=\"photo-zoom-value\">1.15\u00d7</span></label>\n                        <button class=\"photo-adjust\" type=\"button\" data-photo-adjust=\"zoom-out\" aria-label=\"Zoom out\">\u2212</button>\n                        <input id=\"photo-zoom\" type=\"range\" min=\"1\" max=\"1.8\" step=\"0.01\" value=\"1.15\" aria-label=\"Adjust dessert zoom\">\n                        <button class=\"photo-adjust\" type=\"button\" data-photo-adjust=\"zoom-in\" aria-label=\"Zoom in\">+</button>\n                    </div>\n                    <div class=\"photo-control-row\">\n                        <label for=\"photo-tilt\">TILT <span id=\"photo-tilt-value\">0\u00b0</span></label>\n                        <button class=\"photo-adjust\" type=\"button\" data-photo-adjust=\"tilt-left\" aria-label=\"Tilt left\">\u21b6</button>\n                        <input id=\"photo-tilt\" type=\"range\" min=\"-12\" max=\"12\" step=\"0.5\" value=\"0\" aria-label=\"Adjust photo tilt\">\n                        <button class=\"photo-adjust\" type=\"button\" data-photo-adjust=\"tilt-right\" aria-label=\"Tilt right\">\u21b7</button>\n                    </div>\n                    <div class=\"photo-actions\">\n                        <div id=\"photo-feedback\" class=\"photo-feedback\">In-game photo only \u00b7 not saved to your device</div>\n                        <button id=\"photo-shutter\" type=\"button\" aria-label=\"Take an in-game photo\"><span>\u25ce</span><small>SHUTTER</small></button>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <script>\n        // Twin Brothers Coffee cinnamon-roll bakery game; the final reveal is automatic, with no player decoration step.\n        async function run(mode) {\n            const gameWorld = document.getElementById('game-world');\n            let canvas = document.getElementById('game-canvas');\n            let ctx = canvas.getContext('2d');\n\n            function resizeCanvas() {\n                canvas.width = gameWorld.clientWidth;\n                canvas.height = gameWorld.clientHeight;\n            }\n            resizeCanvas();\n            window.addEventListener('resize', resizeCanvas);\n\n            // Initialize gameConfig\n            if (!window.gameConfig) {\n                window.gameConfig = {\n                    cakeFlavor: 'cinnamon',\n                    cakeSize: 6,\n                    decorationTheme: 'twin_brothers',\n                    showHints: false,\n                    soundVolume: 50,\n                    musicVolume: 30,\n                    language: 'en-US',\n                    kitchenStyle: 'twin_brothers',\n                    timeOfDay: 'morning',\n                    ingredients: {\n                        flour: { target: 300, tolerance: 5, current: 0 },\n                        yeast: { target: 7, tolerance: 2, current: 0 },\n                        sugar: { target: 30, tolerance: 3, current: 0 },\n                        salt: { target: 5, tolerance: 1, current: 0 },\n                        milk: { target: 120, tolerance: 5, current: 0 },\n                        water: { target: 50, tolerance: 5, current: 0 },\n                        eggs: { target: 1, tolerance: 0, current: 0 },\n                        butter: { target: 45, tolerance: 5, current: 0 },\n                        brownSugar: { target: 100, tolerance: 5, current: 0 },\n                        cinnamon: { target: 18, tolerance: 2, current: 0 },\n                        fillingButter: { target: 45, tolerance: 5, current: 0 },\n                        caramel: { target: 120, tolerance: 5, current: 0 },\n                        walnuts: { target: 60, tolerance: 5, current: 0 }\n                    }\n                };\n            }\n\n            // Instruction overlays and tutorial arrows are intentionally disabled to keep ingredients unobstructed.\n            window.gameConfig.showHints = false;\n\n            // Asset cache\n            const assets = {};\n            const audioContext = new (window.AudioContext || window.webkitAudioContext)();\n            const masterGainNode = audioContext.createGain();\n            masterGainNode.connect(audioContext.destination);\n            const audioBuffers = {};\n\n            async function preloadAssets() {\n                const assetIds = [\n                    'twin_brothers_3d_bakery_empty', 'cinnamon_roll_3d_raw_dough_tray_realistic', 'cinnamon_roll_3d_dough_ball', 'cinnamon_roll_3d_shaggy_dough_bowl', 'cinnamon_roll_3d_windowpane_dough', 'cinnamon_roll_3d_butter_dough_bowl', 'cinnamon_roll_3d_smooth_dough_bowl', 'cinnamon_roll_3d_first_rise_dough', 'cinnamon_roll_pov_shaggy_dough', 'cinnamon_roll_pov_windowpane_dough', 'cinnamon_roll_pov_adding_butter', 'cinnamon_roll_pov_kneading_smooth_dough', 'cinnamon_roll_pov_first_rise', 'cinnamon_roll_3d_filled_dough_log', 'cinnamon_roll_3d_cut_raw_rolls_tray', 'cinnamon_roll_3d_unfilled_rolled_sheet', 'cinnamon_roll_3d_caramel_soak_rack', 'cinnamon_roll_3d_warm_roll_turntable', 'cinnamon_roll_3d_glazed_roll_turntable', 'cinnamon_roll_3d_caramel_bowl_no_spoon', 'cinnamon_roll_3d_oven_raw_scene', 'cinnamon_roll_3d_oven_baking_scene', 'cinnamon_roll_3d_oven_baked_scene', 'cinnamon_roll_3d_pan_raw_proof', 'cinnamon_roll_3d_step1_prep_scene', 'cinnamon_roll_3d_pour_flour_scene', 'cinnamon_roll_3d_pour_milk_scene', 'cinnamon_roll_3d_log_cutting_scene', 'cinnamon_roll_step3_pov_roll_dough', 'cinnamon_roll_step3_pov_spread_filling', 'cinnamon_roll_step3_pov_roll_log', 'cinnamon_roll_step3_pov_cut_rolls', 'cinnamon_roll_step3_roll_dough_action', 'cinnamon_roll_step3_spread_filling_action', 'cinnamon_roll_step3_roll_log_action', 'cinnamon_roll_step3_cut_log_action', 'cinnamon_roll_step3_pov_cut_rolls_ready', 'cinnamon_roll_3d_water_glass', 'cinnamon_roll_3d_flour_pour_bowl', 'cinnamon_roll_3d_sugar_pour_bowl', 'cinnamon_roll_3d_milk_pour_jug', 'cinnamon_roll_3d_stirring_spoon', 'cinnamon_roll_3d_rolling_pin', 'cinnamon_roll_3d_filled_dough_sheet', 'cinnamon_roll_3d_baked_tray', 'cinnamon_roll_3d_caramel_finish', 'progress_bar_empty', 'progress_bar_fill',\n                    'kitchen_background_realistic', 'twin_brothers_shop_exterior', 'twin_brothers_cinnamon_roll_closeup', 'twin_brothers_cinnamon_roll_box', 'twin_brothers_cinnamon_roll_coffee', 'cinnamon_roll_shaping_luxe', 'cinnamon_roll_tray_luxe', 'cinnamon_roll_hero_luxe', 'cinnamon_roll_3d_turntable', 'cinnamon_roll_3d_caramel_bowl', 'cinnamon_roll_3d_yeast_bowl', 'cinnamon_roll_3d_salt_bowl', 'cinnamon_roll_3d_chopped_walnuts', 'cinnamon_roll_3d_coffee_showcase', 'cinnamon_roll_3d_caramel_drizzle', 'cinnamon_roll_3d_cinnamon_dust', 'digital_scale', 'mixing_bowl_large', 'electric_mixer',\n                    'cake_pan_empty', 'oven_closed', 'oven_open', 'cooling_rack', 'long_knife',\n                    'offset_spatula', 'flour_bag', 'sugar_bag', 'eggs_carton', 'milk_bottle',\n                    'butter_block', 'baking_powder_container', 'raw_batter', 'cake_baking_stage1',\n                    'cake_baking_stage2', 'cake_baking_stage3', 'cake_layer_bottom', 'cake_layer_top',\n                    'whipped_cream_bowl', 'strawberry_whole', 'strawberry_half', 'blueberry',\n                    'chocolate_shaving', 'sugar_pearl_pink', 'sugar_pearl_white', 'birthday_candle_unlit',\n                    'birthday_candle_lit', 'completed_cake_base', 'checkmark_icon', 'star_particle', 'chocolate_cream',\n                    'heart_particle', 'egg_single', 'egg_yolk_liquid', 'egg_shell_broken_top', 'egg_shell_broken_bottom', 'flour_stream_texture', 'cream_dollop', 'mint_leaf', 'orange_slice', 'bear_cookie'\n                ];\n\n                const audioIds = [\n                    'click_sound', 'select_sound', 'step_complete_sound', 'pour_liquid_sound',\n                    'mixer_sound', 'oven_door_sound', 'knife_cutting_sound', 'spreading_sound',\n                    'place_decoration_sound', 'birthday_song', 'ambient_kitchen_sound'\n                ];\n\n                const imagePromises = assetIds.map(id => {\n                    return new Promise((resolve) => {\n                        const assetInfo = lib.getAsset(id);\n                        if (assetInfo && assetInfo.type === 'image') {\n                            const img = new Image();\n                            img.onload = () => {\n                                assets[id] = { img, info: assetInfo };\n                                resolve();\n                            };\n                            img.onerror = () => resolve();\n                            img.src = assetInfo.url;\n                        } else {\n                            resolve();\n                        }\n                    });\n                });\n\n                const audioPromises = audioIds.map(id => {\n                    return new Promise(async (resolve) => {\n                        const assetInfo = lib.getAsset(id);\n                        if (assetInfo && assetInfo.type === 'audio') {\n                            try {\n                                const response = await fetch(assetInfo.url);\n                                const arrayBuffer = await response.arrayBuffer();\n                                const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n                                audioBuffers[id] = { buffer: audioBuffer, info: assetInfo };\n                            } catch (error) {\n                                lib.log(`Failed to load audio: ${id}`);\n                            }\n                        }\n                        resolve();\n                    });\n                });\n                await Promise.all([...imagePromises, ...audioPromises]);\n            }\n\n            function playSound(id, volume = 1, loop = false) {\n                if (audioBuffers[id]) {\n                    const source = audioContext.createBufferSource();\n                    const gainNode = audioContext.createGain();\n                    \n                    source.buffer = audioBuffers[id].buffer;\n                    source.connect(gainNode);\n                    gainNode.connect(masterGainNode);\n                    \n                    const adjustedVolume = (window.gameConfig.soundVolume / 100) * volume;\n                    gainNode.gain.value = adjustedVolume;\n                    \n                    source.loop = loop;\n                    source.start(0);\n                    \n                    return { source, gainNode };\n                }\n                return null;\n            }\n\n            let currentMusicSource = null;\n            function playMusic(id, volume = 1) {\n                if (currentMusicSource) {\n                    try { currentMusicSource.stop(); } catch(e) {}\n                }\n                if (audioBuffers[id]) {\n                    const source = audioContext.createBufferSource();\n                    const gainNode = audioContext.createGain();\n                    \n                    source.buffer = audioBuffers[id].buffer;\n                    source.connect(gainNode);\n                    gainNode.connect(masterGainNode);\n                    \n                    const adjustedVolume = (window.gameConfig.musicVolume / 100) * volume;\n                    gainNode.gain.value = adjustedVolume;\n                    \n                    source.loop = true;\n                    source.start(0);\n                    \n                    currentMusicSource = source;\n                    return { source, gainNode };\n                }\n                return null;\n            }\n\n            await preloadAssets();\n\n            // Runtime state (not in gameConfig)\n            let currentStep = 0;\n            let stepScores = new Array(9).fill(0);\n            let currentStepData = {};\n            let cakeData = { decorations: [] };\n            let backgroundMusic = null;\n            let photoMode = false;\n            const photoStudioState = { zoom: 1.15, tilt: 0, panX: 0, panY: 0, lastCaptureAt: 0, dragging: false, dragX: 0, dragY: 0 };\n\n            const stepTitle = document.getElementById('step-title');\n            const stepNumber = document.getElementById('step-number');\n            const progressBar = document.getElementById('progress-bar');\n            const hintText = document.getElementById('hint-text');\n            const actionButton = document.getElementById('action-button');\n            const stepContent = document.getElementById('step-content');\n            const photoModePanel = document.getElementById('photo-mode-panel');\n            const photoModeClose = document.getElementById('photo-mode-close');\n            const photoZoom = document.getElementById('photo-zoom');\n            const photoZoomValue = document.getElementById('photo-zoom-value');\n            const photoTilt = document.getElementById('photo-tilt');\n            const photoTiltValue = document.getElementById('photo-tilt-value');\n            const photoShutter = document.getElementById('photo-shutter');\n            const photoFeedback = document.getElementById('photo-feedback');\n            const photoFlash = document.getElementById('photo-flash');\n            const steps = [\n                { name: 'Dough Ingredients \u00b7 \u9eb5\u7cf0\u98df\u6750', hint: 'Add flour, yeast, sugar, salt, warm milk, water and egg first. Butter is added after the dough develops gluten in Step 2.' },\n                { name: 'Hand Knead & First Rise \u00b7 \u624b\u63c9\u8207\u9996\u6b21\u767c\u9175', hint: 'Mix to a shaggy dough, knead until it stretches thin, work in the butter until smooth and elastic, then complete the first rise.' },\n                { name: 'Butter, Fill, Roll & Cut \u00b7 \u5857\u9921\u3001\u6372\u8d77\u8207\u5207\u4ef6', hint: 'Roll the risen dough into a rectangle, spread melted butter, cover with brown sugar, cinnamon and walnuts, roll tightly, then cut six raw rolls to about 7 cm each.' },\n                { name: 'Second Rise & Bake \u00b7 \u4e8c\u6b21\u767c\u9175\u8207\u70d8\u70e4', hint: 'Line the pan with 120 g caramel sauce and walnuts, set the rolls in the syrup, proof again, then bake until golden.' },\n                { name: 'Rest in Cinnamon Caramel \u00b7 \u7cd6\u6db2\u56de\u6f64', hint: 'Let the warm rolls rest as caramel syrup soaks in, keeping the outside moist and the crumb soft and springy.' },\n                { name: 'Lift the Warm Rolls \u00b7 \u53d6\u51fa\u71b1\u9a30\u9a30\u8089\u6842\u6372', hint: 'Lift the rolls from the syrup-lined pan while warm and set them out for the finishing glaze.' },\n                { name: 'Prepare Caramel Glaze \u00b7 \u6e96\u5099\u7126\u7cd6\u91ac', hint: 'Stir the 120 g caramel sauce into a smooth, rich glaze; keep the cinnamon aroma mellow and fragrant, never sharp.' },\n                { name: 'Coat with Caramel \u00b7 \u5747\u52fb\u6dcb\u4e0a\u7126\u7cd6\u91ac', hint: 'Slide horizontally to coat the warm roll with a glossy, even layer of caramel.' },\n                { name: 'Twin Brothers Coffee Reveal \u00b7 \u6210\u54c1\u5c55\u793a', hint: 'Your finished caramel-walnut cinnamon roll is ready to enjoy\u2014no extra decorating needed!' }\n            ];\n\n            lib.showGameParameters({\n                params: {\n                    'Cinnamon Roll Flavor': { key: 'cakeFlavor', type: 'dropdown', options: [\n                        { label: 'Twin Brothers Caramel Walnut Cinnamon', value: 'cinnamon' },\n                        { label: 'Chocolate', value: 'chocolate' }, { label: 'Pumpkin Spice', value: 'pumpkin' },\n                        { label: 'Strawberry', value: 'strawberry' }, { label: 'Matcha', value: 'matcha' }\n                    ]},\n                    'Roll Size': { key: 'cakeSize', type: 'dropdown', options: [\n                        { label: '7 cm Twin Brothers Rolls', value: 7 }, { label: '8 cm Bakery Rolls', value: 8 }, { label: '6 cm Mini Rolls', value: 6 }\n                    ]},\n                    'Kitchen Style': { key: 'kitchenStyle', type: 'dropdown', options: [\n                        { label: 'Twin Brothers Coffee \u00b7 Photorealistic 3D Bakery', value: 'twin_brothers' }\n                    ]},\n                    'Sound Volume': { key: 'soundVolume', type: 'slider', min: 0, max: 100, step: 10 },\n                    'Music Volume': { key: 'musicVolume', type: 'slider', min: 0, max: 100, step: 10 }\n                }\n            });\n\n            function updateUI() {\n                stepTitle.textContent = steps[currentStep].name;\n                stepNumber.textContent = `${currentStep + 1}/9`;\n                progressBar.style.width = `${((currentStep + 1) / 9) * 100}%`;\n                if (window.gameConfig.showHints) {\n                    hintText.textContent = steps[currentStep].hint;\n                    hintText.classList.remove('hidden');\n                } else {\n                    hintText.classList.add('hidden');\n                }\n            }\n\n            function drawImage(id, x, y, w, h) {\n                if (!assets[id]) return;\n                const { img, info } = assets[id];\n                let width = w, height = h;\n                if (info.aspect_ratio && width && !height) {\n                    const [aspectW, aspectH] = info.aspect_ratio;\n                    height = width * (aspectH / aspectW);\n                } else if (info.aspect_ratio && height && !width) {\n                    const [aspectW, aspectH] = info.aspect_ratio;\n                    width = height * (aspectW / aspectH);\n                } else if ((!width || !height) && img.width && img.height) {\n                    const scale = width ? width / img.width : height / img.height;\n                    width = img.width * scale; height = img.height * scale;\n                }\n                ctx.drawImage(img, Math.floor(x), Math.floor(y), Math.floor(width), Math.floor(height));\n            }\n\n            function drawImageFit(id, x, y, w, h) {\n                if (!assets[id]) return false;\n                const img = assets[id].img;\n                if (!img.width || !img.height || w <= 0 || h <= 0) return false;\n                const scale = Math.min(w / img.width, h / img.height);\n                const drawW = img.width * scale, drawH = img.height * scale;\n                ctx.drawImage(img, x + (w - drawW) / 2, y + (h - drawH) / 2, drawW, drawH);\n                return true;\n            }\n\n            function drawStepIngredient(p, x, y, w, h) {\n                // Show each ingredient as a freestanding 3D prop: no square tile, card, or panel.\n                const labelHeight = 48;\n                const iconX = x + 3, iconY = y - 2, iconW = w - 6, iconH = h - labelHeight;\n                ctx.save();\n                ctx.shadowColor = 'rgba(25, 14, 8, 0.42)';\n                ctx.shadowBlur = 16;\n                ctx.shadowOffsetY = 8;\n                const displayAsset = p.asset && assets[p.asset] ? p.asset : p.fallbackAsset;\n                if (!displayAsset || !drawImageFit(displayAsset, iconX, iconY, iconW, iconH)) {\n                    // Small, softly shaded ingredient mound as a clean fallback without a container tile.\n                    const cx = x + w / 2, cy = iconY + iconH * .62;\n                    const r = Math.min(iconW, iconH) * .23;\n                    const objectGrad = ctx.createRadialGradient(cx - r * .35, cy - r * .45, 2, cx, cy, r * 1.4);\n                    objectGrad.addColorStop(0, '#fff8e9');\n                    objectGrad.addColorStop(.48, p.color || '#d9b88a');\n                    objectGrad.addColorStop(1, '#79553b');\n                    ctx.fillStyle = objectGrad;\n                    ctx.beginPath(); ctx.ellipse(cx, cy, r * 1.25, r * .72, 0, 0, Math.PI * 2); ctx.fill();\n                }\n                ctx.restore();\n                const parts = String(p.name || '').split(' \u00b7 ');\n                ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle';\n                ctx.lineJoin = 'round'; ctx.lineWidth = 4;\n                ctx.font = '700 21px \"Noto Sans TC\", sans-serif';\n                ctx.strokeStyle = 'rgba(255,248,235,.98)';\n                ctx.lineWidth = 5;\n                ctx.strokeText(parts[0], x + w / 2, y + h - 31, w - 6);\n                ctx.fillStyle = '#2e2018';\n                ctx.fillText(parts[0], x + w / 2, y + h - 31, w - 6);\n                if (parts[1]) {\n                    ctx.font = '700 18px \"Noto Sans TC\", sans-serif';\n                    ctx.strokeText(parts[1], x + w / 2, y + h - 10, w - 6);\n                    ctx.fillStyle = '#493126';\n                    ctx.fillText(parts[1], x + w / 2, y + h - 10, w - 6);\n                }\n                ctx.restore();\n            }\n\n            // A chosen ingredient is brought briefly above the bowl, tipped, emptied, then returned to its prep station.\n            // Eggs stay in their carton while one egg is cracked; butter is portioned into small pieces rather than dropping the block.\n            function drawPouringIngredient(p, x, y, w, h, key, progress, bowlY, bowlCenterX) {\n                const clamp01 = value => Math.max(0, Math.min(1, value));\n                const ease = value => { const t = clamp01(value); return t * t * (3 - 2 * t); };\n                const iconW = w - 16, iconH = h - 42;\n                const sourceX = x + w / 2, sourceY = y + (h - 42) / 2;\n                const direction = sourceX < bowlCenterX ? 1 : -1;\n                const targetX = bowlCenterX + direction * 72;\n                const targetY = bowlY - 38;\n                const approach = ease(progress / .29);\n                const retreat = ease((progress - .78) / .22);\n                const travel = progress < .78 ? approach : 1 - retreat;\n                const arc = Math.sin(travel * Math.PI) * 18;\n                const cx = sourceX + (targetX - sourceX) * travel;\n                const cy = sourceY + (targetY - sourceY) * travel - arc;\n                const tipIn = ease((progress - .27) / .14);\n                const tipOut = ease((progress - .75) / .14);\n                const tipAmount = progress < .75 ? tipIn : 1 - tipOut;\n                const tilt = direction * .72 * tipAmount;\n\n                if (key === 'eggs' || key === 'butter') {\n                    // Keep the carton and butter block on the counter; animate only the edible portion.\n                    drawStepIngredient(p, x, y, w, h);\n                    if (key === 'eggs') {\n                        const eggTravel = ease(progress / .39);\n                        const eggX = sourceX + (targetX - sourceX) * eggTravel;\n                        const eggY = sourceY - 8 + (targetY + 2 - sourceY + 8) * eggTravel - Math.sin(eggTravel * Math.PI) * 34;\n                        const eggFade = progress > .42 ? Math.max(0, 1 - (progress - .42) / .10) : 1;\n                        ctx.save(); ctx.globalAlpha = eggFade; ctx.translate(eggX, eggY); ctx.rotate(direction * .18 * eggTravel);\n                        if (!drawImageFit('egg_single', -20, -28, 40, 56)) {\n                            const eggGrad = ctx.createLinearGradient(-14, -20, 14, 20);\n                            eggGrad.addColorStop(0, '#fffef5'); eggGrad.addColorStop(.55, '#f6e7c5'); eggGrad.addColorStop(1, '#d1b88c');\n                            ctx.fillStyle = eggGrad; ctx.beginPath(); ctx.ellipse(0, 0, 14, 21, 0, 0, Math.PI * 2); ctx.fill();\n                        }\n                        ctx.restore();\n                        if (progress >= .40 && progress <= .78) {\n                            const shellProgress = ease((progress - .40) / .12);\n                            const shellAlpha = Math.min(1, shellProgress * 2) * Math.min(1, (1 - (progress - .64) / .14));\n                            ctx.save(); ctx.globalAlpha = Math.max(0, shellAlpha);\n                            if (!drawImageFit('egg_shell_broken_bottom', targetX - 34, targetY - 6, 28, 30)) {\n                                ctx.fillStyle = '#fff8e7'; ctx.beginPath(); ctx.ellipse(targetX - 20, targetY + 2, 12, 7, -.45, 0, Math.PI * 2); ctx.fill();\n                            }\n                            if (!drawImageFit('egg_shell_broken_top', targetX + 6, targetY - 14, 28, 30)) {\n                                ctx.fillStyle = '#fff8e7'; ctx.beginPath(); ctx.ellipse(targetX + 20, targetY - 6, 12, 7, .45, Math.PI, Math.PI * 2); ctx.fill();\n                            }\n                            ctx.restore();\n                        }\n                        return { x: targetX, y: targetY + 4, targetX, targetY: bowlY + 98 };\n                    }\n                    return { x: sourceX, y: sourceY, targetX, targetY: bowlY + 98 };\n                }\n\n                ctx.save(); ctx.translate(cx, cy); ctx.rotate(tilt);\n                ctx.shadowColor = 'rgba(25, 14, 8, 0.34)'; ctx.shadowBlur = 16; ctx.shadowOffsetY = 8;\n                if (!p.asset || !drawImageFit(p.asset, -iconW / 2, -iconH / 2, iconW, iconH)) {\n                    const r = Math.min(iconW, iconH) * .23;\n                    const grad = ctx.createRadialGradient(-r * .35, -r * .45, 2, 0, 0, r * 1.4);\n                    grad.addColorStop(0, '#fff8e9'); grad.addColorStop(.48, p.color || '#d9b88a'); grad.addColorStop(1, '#79553b');\n                    ctx.fillStyle = grad; ctx.beginPath(); ctx.ellipse(0, 0, r * 1.25, r * .72, 0, 0, Math.PI * 2); ctx.fill();\n                }\n                ctx.restore();\n                const outletLocalX = direction * iconW * .34, outletLocalY = iconH * .22;\n                const outletX = cx + outletLocalX * Math.cos(tilt) - outletLocalY * Math.sin(tilt);\n                const outletY = cy + outletLocalX * Math.sin(tilt) + outletLocalY * Math.cos(tilt);\n                const label = String(p.name || '').split(' \u00b7 ')[0];\n                ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.lineJoin = 'round'; ctx.lineWidth = 4;\n                ctx.font = '600 16px \"Noto Sans TC\", sans-serif'; ctx.strokeStyle = 'rgba(255,248,235,.92)';\n                ctx.strokeText(label, cx, y + h - 28, w - 8); ctx.fillStyle = '#382820'; ctx.fillText(label, cx, y + h - 28, w - 8);\n                ctx.restore();\n                return { x: outletX, y: outletY, targetX, targetY: bowlY + 98 };\n            }\n\n            function drawIngredientPour(key, p, progress, startX, startY, targetX, targetY) {\n                const clamp01 = value => Math.max(0, Math.min(1, value));\n                const ease = value => { const t = clamp01(value); return t * t * (3 - 2 * t); };\n                const fadeIn = ease((progress - .34) / .08);\n                const fadeOut = 1 - ease((progress - .73) / .10);\n                const visible = fadeIn * fadeOut;\n                if (visible <= 0) return;\n                const reach = ease((progress - .35) / .40);\n                const cpX = (startX + targetX) / 2 + (targetX - startX) * .12;\n                const cpY = (startY + targetY) / 2 - Math.min(24, Math.abs(targetY - startY) * .08);\n                const pointOnStream = t => {\n                    const u = 1 - t;\n                    return { x: u * u * startX + 2 * u * t * cpX + t * t * targetX,\n                             y: u * u * startY + 2 * u * t * cpY + t * t * targetY };\n                };\n                ctx.save(); ctx.globalAlpha = visible;\n                const isLiquid = key === 'milk' || key === 'water' || key === 'eggs';\n                if (isLiquid) {\n                    const end = pointOnStream(Math.max(.04, reach));\n                    ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(startX, startY); ctx.quadraticCurveTo(cpX, cpY, end.x, end.y);\n                    ctx.strokeStyle = key === 'milk' ? 'rgba(255,248,229,.95)' : (key === 'water' ? 'rgba(187,230,242,.94)' : 'rgba(255,249,226,.96)');\n                    ctx.lineWidth = key === 'eggs' ? 10 : 8; ctx.stroke();\n                    ctx.strokeStyle = 'rgba(255,255,255,.78)'; ctx.lineWidth = 2.5; ctx.stroke();\n                    const dripPulse = (Date.now() / 180) % 1;\n                    const drip = pointOnStream(Math.max(.08, Math.min(.96, reach - dripPulse * .12)));\n                    const dripGrad = ctx.createRadialGradient(drip.x - 2, drip.y - 3, 1, drip.x, drip.y, key === 'eggs' ? 8 : 6);\n                    dripGrad.addColorStop(0, '#fffef3'); dripGrad.addColorStop(.7, key === 'milk' ? '#fff2d9' : '#d8f4f8'); dripGrad.addColorStop(1, 'rgba(255,255,255,0)');\n                    ctx.fillStyle = dripGrad; ctx.beginPath(); ctx.ellipse(drip.x, drip.y, 5, 8, .15, 0, Math.PI * 2); ctx.fill();\n                    if (key === 'eggs' && reach > .3) {\n                        const yolk = pointOnStream(Math.min(reach, .96));\n                        const yolkGrad = ctx.createRadialGradient(yolk.x - 3, yolk.y - 4, 1, yolk.x, yolk.y, 12);\n                        yolkGrad.addColorStop(0, '#fff1a6'); yolkGrad.addColorStop(.55, '#f4b93d'); yolkGrad.addColorStop(1, '#c87820');\n                        ctx.fillStyle = yolkGrad; ctx.beginPath(); ctx.ellipse(yolk.x, yolk.y, 9, 11, .25, 0, Math.PI * 2); ctx.fill();\n                    }\n                } else if (key === 'butter') {\n                    // Small cut portions travel in a soft arc; the full butter block remains on the counter.\n                    const now = Date.now();\n                    for (let i = 0; i < 6; i++) {\n                        const t = ((now / 560 + i * .17) % 1) * Math.max(.12, reach);\n                        const pt = pointOnStream(t), size = 4 + (i % 3);\n                        ctx.save(); ctx.translate(pt.x + Math.sin(now / 120 + i) * 2, pt.y); ctx.rotate(.25 + Math.sin(now / 170 + i) * .18);\n                        const butterGrad = ctx.createLinearGradient(-size, -size, size, size);\n                        butterGrad.addColorStop(0, '#fff2ad'); butterGrad.addColorStop(.55, '#f2cf70'); butterGrad.addColorStop(1, '#c99942');\n                        ctx.fillStyle = butterGrad; ctx.strokeStyle = 'rgba(152,105,43,.5)'; ctx.lineWidth = 1.2;\n                        ctx.beginPath(); ctx.roundRect(-size, -size * .72, size * 2, size * 1.45, 2); ctx.fill(); ctx.stroke(); ctx.restore();\n                    }\n                } else {\n                    const powderColor = key === 'salt' ? '#fffdf3' : (key === 'yeast' ? '#c89e67' : (key === 'sugar' ? '#fff0ca' : '#f6ead1'));\n                    const now = Date.now();\n                    for (let i = 0; i < 30; i++) {\n                        const t = ((now / 500 + i * .071) % 1) * Math.max(.12, reach);\n                        const pt = pointOnStream(t), spread = 1.2 + t * 9;\n                        const jitterX = Math.sin(i * 17.13 + now / 125) * spread;\n                        const jitterY = Math.cos(i * 9.71 + now / 150) * spread * .45;\n                        ctx.globalAlpha = visible * (.48 + .42 * Math.abs(Math.sin(i * 3.7)));\n                        ctx.fillStyle = powderColor; ctx.beginPath();\n                        ctx.arc(pt.x + jitterX, pt.y + jitterY, 1.4 + (i % 3) * .55, 0, Math.PI * 2); ctx.fill();\n                    }\n                }\n                // A soft, brief contact ripple makes the falling stream feel seated inside the bowl.\n                const impact = pointOnStream(Math.max(.05, reach));\n                ctx.globalAlpha = visible * .32; ctx.strokeStyle = key === 'milk' || key === 'water' ? 'rgba(255,255,255,.8)' : 'rgba(244,205,145,.75)';\n                ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(impact.x, targetY - 3, 9 + (Date.now() % 300) / 30, 3 + (Date.now() % 200) / 100, 0, 0, Math.PI * 2); ctx.stroke();\n                ctx.restore();\n            }\n\n            function draw3DRollingPin(x, y, angle, isPressing) {\n                if (assets.cinnamon_roll_3d_rolling_pin) {\n                    const img = assets.cinnamon_roll_3d_rolling_pin.img;\n                    const w = 260, h = w * img.height / img.width;\n                    ctx.save();\n                    ctx.translate(x, y);\n                    ctx.rotate(angle);\n                    ctx.shadowColor = 'rgba(30, 16, 10, 0.42)';\n                    ctx.shadowBlur = isPressing ? 9 : 16;\n                    ctx.shadowOffsetY = isPressing ? 5 : 10;\n                    ctx.drawImage(img, -w / 2, -h / 2, w, h);\n                    ctx.restore();\n                    return;\n                }\n                ctx.save();\n                ctx.translate(x, y);\n                ctx.rotate(angle);\n                const pinLen = 220;\n                const pinRadius = isPressing ? 20 : 23;\n                \n                // 3D Drop shadow onto dough / board\n                ctx.save();\n                ctx.shadowColor = 'rgba(30, 16, 10, 0.45)';\n                ctx.shadowBlur = isPressing ? 8 : 18;\n                ctx.shadowOffsetY = isPressing ? 6 : 14;\n                ctx.fillStyle = 'rgba(0,0,0,0.2)';\n                ctx.beginPath();\n                ctx.ellipse(0, 10, pinLen / 2 + 10, pinRadius * 0.5, 0, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.restore();\n\n                // Tapered French wooden rolling pin body\n                const woodGrad = ctx.createLinearGradient(0, -pinRadius, 0, pinRadius);\n                woodGrad.addColorStop(0, '#e8be89');\n                woodGrad.addColorStop(0.2, '#fff1d6');\n                woodGrad.addColorStop(0.5, '#c99256');\n                woodGrad.addColorStop(0.85, '#8c5828');\n                woodGrad.addColorStop(1, '#533114');\n\n                ctx.fillStyle = woodGrad;\n                ctx.beginPath();\n                ctx.moveTo(-pinLen / 2, -pinRadius * 0.45);\n                ctx.quadraticCurveTo(-pinLen * 0.25, -pinRadius, 0, -pinRadius);\n                ctx.quadraticCurveTo(pinLen * 0.25, -pinRadius, pinLen / 2, -pinRadius * 0.45);\n                ctx.lineTo(pinLen / 2, pinRadius * 0.45);\n                ctx.quadraticCurveTo(pinLen * 0.25, pinRadius, 0, pinRadius);\n                ctx.quadraticCurveTo(-pinLen * 0.25, pinRadius, -pinLen / 2, pinRadius * 0.45);\n                ctx.closePath();\n                ctx.fill();\n\n                // Turned wood grain rings\n                ctx.strokeStyle = 'rgba(92, 53, 22, 0.25)';\n                ctx.lineWidth = 1.5;\n                [-pinLen * 0.35, -pinLen * 0.15, pinLen * 0.15, pinLen * 0.35].forEach(gx => {\n                    ctx.beginPath();\n                    ctx.ellipse(gx, 0, 4, pinRadius * 0.85, 0, 0, Math.PI * 2);\n                    ctx.stroke();\n                });\n\n                // End caps with 3D turned bevels\n                [-pinLen / 2, pinLen / 2].forEach(ex => {\n                    ctx.fillStyle = '#6d3e19';\n                    ctx.beginPath();\n                    ctx.arc(ex, 0, pinRadius * 0.45, 0, Math.PI * 2);\n                    ctx.fill();\n                });\n\n                ctx.restore();\n            }\n\n            function draw3DStirringSpoon(x, y, angle) {\n                if (assets.cinnamon_roll_3d_stirring_spoon) {\n                    const img = assets.cinnamon_roll_3d_stirring_spoon.img;\n                    const w = 180, h = w * img.height / img.width;\n                    ctx.save();\n                    ctx.translate(x, y);\n                    ctx.rotate(angle);\n                    ctx.shadowColor = 'rgba(25, 12, 6, 0.4)';\n                    ctx.shadowBlur = 15;\n                    ctx.shadowOffsetY = 10;\n                    ctx.drawImage(img, -w * 0.3, -h * 0.8, w, h);\n                    ctx.restore();\n                    return;\n                }\n                // Procedural 3D brass and dark walnut pastry spoon\n                ctx.save();\n                ctx.translate(x, y);\n                ctx.rotate(angle);\n                ctx.shadowColor = 'rgba(25, 12, 6, 0.4)';\n                ctx.shadowBlur = 14;\n                ctx.shadowOffsetY = 8;\n                \n                // Wooden handle\n                const handleGrad = ctx.createLinearGradient(-8, 0, 8, 0);\n                handleGrad.addColorStop(0, '#533118');\n                handleGrad.addColorStop(0.35, '#a46f40');\n                handleGrad.addColorStop(0.6, '#e0b284');\n                handleGrad.addColorStop(1, '#42240f');\n                ctx.fillStyle = handleGrad;\n                ctx.beginPath();\n                ctx.roundRect(-7, -180, 14, 150, 7);\n                ctx.fill();\n\n                // Brass collar\n                const brassGrad = ctx.createLinearGradient(-9, 0, 9, 0);\n                brassGrad.addColorStop(0, '#8c6b23');\n                brassGrad.addColorStop(0.4, '#fff1a8');\n                brassGrad.addColorStop(1, '#61460d');\n                ctx.fillStyle = brassGrad;\n                ctx.fillRect(-8, -32, 16, 12);\n\n                // Brass spoon bowl\n                ctx.fillStyle = brassGrad;\n                ctx.beginPath();\n                ctx.ellipse(0, 0, 26, 36, 0, 0, Math.PI * 2);\n                ctx.fill();\n\n                // Glossy caramel coat\n                const caramelGrad = ctx.createRadialGradient(0, -5, 2, 0, 0, 24);\n                caramelGrad.addColorStop(0, '#ffcf7b');\n                caramelGrad.addColorStop(0.5, '#bd6d2b');\n                caramelGrad.addColorStop(1, '#693510');\n                ctx.fillStyle = caramelGrad;\n                ctx.beginPath();\n                ctx.ellipse(0, 2, 20, 28, 0, 0, Math.PI * 2);\n                ctx.fill();\n\n                // Caramel drip\n                ctx.fillStyle = '#b76828';\n                ctx.beginPath();\n                ctx.moveTo(-5, 26);\n                ctx.quadraticCurveTo(0, 48, 2, 54);\n                ctx.quadraticCurveTo(4, 48, 5, 26);\n                ctx.closePath();\n                ctx.fill();\n\n                ctx.restore();\n            }\n\n            function draw3DOffsetSpatula(x, y, angle) {\n                if (assets.offset_spatula) {\n                    const img = assets.offset_spatula.img;\n                    const w = 210, h = w * img.height / img.width;\n                    ctx.save();\n                    ctx.translate(x, y);\n                    ctx.rotate(angle);\n                    ctx.shadowColor = 'rgba(25, 12, 6, 0.4)';\n                    ctx.shadowBlur = 12;\n                    ctx.shadowOffsetY = 8;\n                    ctx.drawImage(img, -w * 0.2, -h * 0.7, w, h);\n                    ctx.restore();\n                }\n            }\n\n            function drawPhotoCover(id, x, y, w, h, focusX = .5, focusY = .5) {\n                if (!assets[id]) return false;\n                const img = assets[id].img, targetRatio = w / h, sourceRatio = img.width / img.height;\n                let sx = 0, sy = 0, sw = img.width, sh = img.height;\n                if (sourceRatio > targetRatio) { sw = img.height * targetRatio; sx = (img.width - sw) * Math.max(0, Math.min(1, focusX)); }\n                else { sh = img.width / targetRatio; sy = (img.height - sh) * Math.max(0, Math.min(1, focusY)); }\n                ctx.drawImage(img, sx, sy, sw, sh, x, y, w, h);\n                return true;\n            }\n\n            function drawPhotoContain(id, x, y, w, h) {\n                if (!assets[id]) return false;\n                const img = assets[id].img, scale = Math.min(w / img.width, h / img.height);\n                const dw = img.width * scale, dh = img.height * scale;\n                ctx.drawImage(img, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh);\n                return true;\n            }\n\n            function drawCinnamonRoll(x, y, w, h) {\n                if (assets.cinnamon_roll_3d_caramel_finish) {\n                    const img = assets.cinnamon_roll_3d_caramel_finish.img;\n                    // Preserve the whole plate and pastry; cropping the lower part made the serving plate look broken.\n                    const scale = Math.min(w / img.width, h / img.height);\n                    const dw = img.width * scale, dh = img.height * scale;\n                    ctx.save(); ctx.shadowColor = 'rgba(28,18,12,.38)'; ctx.shadowBlur = 18; ctx.shadowOffsetY = 8;\n                    ctx.drawImage(img, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh); ctx.restore();\n                    return;\n                }\n                if (assets.cinnamon_roll_hero_luxe) {\n                    const img = assets.cinnamon_roll_hero_luxe.img;\n                    const scale = Math.min(w / img.width, h / img.height);\n                    const dw = img.width * scale, dh = img.height * scale;\n                    ctx.save(); ctx.shadowColor = 'rgba(28,18,12,.38)'; ctx.shadowBlur = 18; ctx.shadowOffsetY = 8;\n                    ctx.drawImage(img, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh); ctx.restore();\n                    return;\n                }\n                if (assets.cinnamon_roll_3d_coffee_showcase) {\n                    const img = assets.cinnamon_roll_3d_coffee_showcase.img;\n                    const scale = Math.min(w / img.width, h / img.height);\n                    const dw = img.width * scale, dh = img.height * scale;\n                    ctx.save(); ctx.shadowColor = 'rgba(28,18,12,.38)'; ctx.shadowBlur = 18; ctx.shadowOffsetY = 8;\n                    ctx.drawImage(img, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh); ctx.restore();\n                    return;\n                }\n                ctx.save();\n                ctx.shadowColor = 'rgba(28,18,12,.38)'; ctx.shadowBlur = 18; ctx.shadowOffsetY = 8;\n                ctx.fillStyle = '#e7b878'; ctx.beginPath();\n                ctx.ellipse(x + w / 2, y + h * .57, w * .47, h * .45, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore();\n                ctx.save();\n                ctx.beginPath(); ctx.ellipse(x + w / 2, y + h * .5, w * .47, h * .46, 0, 0, Math.PI * 2); ctx.clip();\n                if (!drawPhotoCover('twin_brothers_cinnamon_roll_closeup', x, y, w, h, .5, .58)) {\n                    ctx.fillStyle = '#9b5529'; ctx.fillRect(x, y, w, h);\n                    ctx.strokeStyle = '#e0b477'; ctx.lineWidth = 10;\n                    for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.ellipse(x + w / 2, y + h / 2, w * (.12 + i * .1), h * (.12 + i * .1), 0, 0, Math.PI * 2); ctx.stroke(); }\n                }\n                ctx.restore();\n                ctx.save(); ctx.strokeStyle = 'rgba(191,112,48,.96)'; ctx.lineWidth = Math.max(7, w * .035); ctx.lineCap = 'round';\n                ctx.beginPath(); ctx.moveTo(x+w*.30,y+h*.44); ctx.bezierCurveTo(x+w*.42,y+h*.35,x+w*.48,y+h*.65,x+w*.58,y+h*.48); ctx.bezierCurveTo(x+w*.67,y+h*.35,x+w*.69,y+h*.5,x+w*.76,y+h*.42); ctx.stroke(); ctx.restore();\n            }\n\n            function drawPanRolls(centerX, centerY, progress = 1, scale = 1, baked = false) {\n                // Never show baked-pastry art before the oven stage has actually finished.\n                if (baked && assets.cinnamon_roll_3d_baked_tray) {\n                    const img = assets.cinnamon_roll_3d_baked_tray.img;\n                    const maxW = 455 * scale, maxH = 240 * scale;\n                    const fit = Math.min(maxW / img.width, maxH / img.height);\n                    const dw = img.width * fit, dh = img.height * fit;\n                    ctx.save(); ctx.globalAlpha = Math.max(.12, Math.min(1, progress));\n                    ctx.shadowColor = 'rgba(28,18,12,.4)'; ctx.shadowBlur = 18 * scale; ctx.shadowOffsetY = 8 * scale;\n                    ctx.drawImage(img, centerX - dw / 2, centerY - dh / 2, dw, dh); ctx.restore();\n                    return;\n                }\n                // Use only the newly specified pale raw-dough tray before baking; legacy 'unbaked' art was actually a baked finished roll.\n                if (!baked && assets.cinnamon_roll_3d_raw_dough_tray_realistic) {\n                    const img = assets.cinnamon_roll_3d_raw_dough_tray_realistic.img;\n                    const maxW = 455 * scale, maxH = 250 * scale;\n                    const fit = Math.min(maxW / img.width, maxH / img.height);\n                    const dw = img.width * fit, dh = img.height * fit;\n                    ctx.save();\n                    ctx.globalAlpha = Math.max(.35, Math.min(1, progress));\n                    ctx.shadowColor = 'rgba(28,18,12,.4)'; ctx.shadowBlur = 16 * scale; ctx.shadowOffsetY = 7 * scale;\n                    ctx.drawImage(img, centerX - dw / 2, centerY - dh / 2, dw, dh);\n                    ctx.restore();\n                    return;\n                }\n                const count = Math.max(0, Math.ceil(Math.max(0, Math.min(1, progress)) * 6));\n                ctx.save();\n                const panGrad = ctx.createLinearGradient(centerX, centerY - 45 * scale, centerX, centerY + 45 * scale);\n                panGrad.addColorStop(0, '#a65c2d'); panGrad.addColorStop(.6, '#69371f'); panGrad.addColorStop(1, '#4b291a');\n                ctx.fillStyle = panGrad; ctx.beginPath(); ctx.ellipse(centerX, centerY, 205 * scale, 62 * scale, 0, 0, Math.PI * 2); ctx.fill();\n                for (let i = 0; i < count; i++) {\n                    const col = i % 3, row = Math.floor(i / 3);\n                    const x = centerX + (col - 1) * 112 * scale + (row ? 18 : -12) * scale;\n                    const y = centerY + (row ? 22 : -18) * scale;\n                    ctx.save(); ctx.shadowColor = 'rgba(30,15,8,.45)'; ctx.shadowBlur = 8 * scale; ctx.shadowOffsetY = 4 * scale;\n                    const rollGrad = ctx.createLinearGradient(x, y - 34 * scale, x, y + 34 * scale);\n                    // Pale, soft raw dough with cinnamon filling; browning appears only after baking.\n                    rollGrad.addColorStop(0, '#f3dfbd'); rollGrad.addColorStop(.55, '#e5c99e'); rollGrad.addColorStop(1, '#c5a77d');\n                    ctx.fillStyle = rollGrad; ctx.beginPath(); ctx.ellipse(x, y, 48 * scale, 31 * scale, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore();\n                    ctx.save(); ctx.strokeStyle = '#8a4c2a'; ctx.lineWidth = 5 * scale; ctx.beginPath();\n                    ctx.ellipse(x, y, 33 * scale, 21 * scale, 0, .15, Math.PI * 1.8); ctx.ellipse(x, y, 18 * scale, 12 * scale, 0, .4, Math.PI * 1.9); ctx.stroke();\n                    ctx.strokeStyle = 'rgba(200,116,48,.88)'; ctx.lineWidth = 3 * scale; ctx.beginPath(); ctx.moveTo(x - 38 * scale, y + 5 * scale); ctx.quadraticCurveTo(x, y - 6 * scale, x + 38 * scale, y + 7 * scale); ctx.stroke();\n                    ctx.fillStyle = '#e0b276'; [[-15,-5],[10,4],[25,-10]].forEach(([dx,dy]) => { ctx.beginPath(); ctx.ellipse(x + dx * scale, y + dy * scale, 5 * scale, 3 * scale, -.4, 0, Math.PI * 2); ctx.fill(); });\n                    ctx.restore();\n                }\n                ctx.restore();\n            }\n\n\n            function drawCinnamonDecoration(type, x, y, size) {\n                if (type === 'roll_pecans' || type === 'roll_icing' || type === 'roll_cinnamon') drawRollTopping(type, x, y, size);\n                else drawImage(type, x - size / 2, y - size / 2, size, size);\n            }\n\n            function drawRollTopping(type, x, y, size) {\n                ctx.save(); ctx.translate(x, y); ctx.scale(size / 100, size / 100);\n                if (type === 'roll_pecans') {\n                    ctx.fillStyle = '#9a5b2e'; ctx.strokeStyle = '#5e351d'; ctx.lineWidth = 4;\n                    ctx.beginPath(); ctx.ellipse(0, 0, 27, 19, -.3, 0, Math.PI * 2); ctx.fill(); ctx.stroke();\n                    ctx.strokeStyle = '#d9a66b'; ctx.lineWidth = 3;\n                    for (let i=-2;i<=2;i++){ctx.beginPath();ctx.moveTo(i*8,-12);ctx.quadraticCurveTo(i*6-5,0,i*8,12);ctx.stroke();}\n                } else if (type === 'roll_icing') {\n                    ctx.strokeStyle = '#c87932'; ctx.lineWidth = 11; ctx.beginPath(); ctx.moveTo(-34,-12); ctx.bezierCurveTo(-14,-32,-8,24,8,5); ctx.bezierCurveTo(20,-10,23,-22,35,-17); ctx.stroke();\n                } else if (type === 'roll_cinnamon') {\n                    ctx.fillStyle = '#75401f'; [[-15,-9,7],[4,-16,6],[17,2,7],[-7,13,6],[0,0,8]].forEach(([dx,dy,r])=>{ctx.beginPath();ctx.arc(dx,dy,r,0,Math.PI*2);ctx.fill();});\n                }\n                ctx.restore();\n            }\n\n            function drawBackground() {\n                // Keep the Twin Brothers Coffee bakery scene as the game background; fall back to a warm gradient if unavailable.\n                if (assets.twin_brothers_3d_bakery_empty) {\n                    drawPhotoCover('twin_brothers_3d_bakery_empty', 0, 0, canvas.width, canvas.height, .5, .5);\n                    const shade = ctx.createLinearGradient(0, 0, 0, canvas.height);\n                    shade.addColorStop(0, 'rgba(24,16,12,.36)');\n                    shade.addColorStop(.42, 'rgba(24,16,12,.12)');\n                    shade.addColorStop(1, 'rgba(24,16,12,.3)');\n                    ctx.fillStyle = shade; ctx.fillRect(0, 0, canvas.width, canvas.height);\n                    return;\n                }\n                const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);\n                gradient.addColorStop(0, '#eee4d5');\n                gradient.addColorStop(.52, '#e3d3bf');\n                gradient.addColorStop(1, '#d2bda5');\n                ctx.fillStyle = gradient;\n                ctx.fillRect(0, 0, canvas.width, canvas.height);\n                const vignette = ctx.createRadialGradient(canvas.width / 2, canvas.height * .42, 80, canvas.width / 2, canvas.height * .48, canvas.height * .72);\n                vignette.addColorStop(0, 'rgba(255,250,239,.16)');\n                vignette.addColorStop(1, 'rgba(62,39,27,.12)');\n                ctx.fillStyle = vignette;\n                ctx.fillRect(0, 0, canvas.width, canvas.height);\n            }\n            function drawLuxuryFrame() {\n                const inset = 13, w = canvas.width, h = canvas.height;\n                ctx.save();\n                ctx.lineWidth = 2;\n                ctx.strokeStyle = 'rgba(213, 183, 119, 0.78)';\n                ctx.strokeRect(inset, inset, w - inset * 2, h - inset * 2);\n                ctx.lineWidth = 1;\n                ctx.strokeStyle = 'rgba(255, 248, 224, 0.62)';\n                ctx.strokeRect(inset + 6, inset + 6, w - (inset + 6) * 2, h - (inset + 6) * 2);\n                ctx.lineWidth = 3;\n                ctx.strokeStyle = 'rgba(225, 197, 137, 0.88)';\n                const corners = [[inset + 2, inset + 2, 1, 1], [w - inset - 2, inset + 2, -1, 1], [inset + 2, h - inset - 2, 1, -1], [w - inset - 2, h - inset - 2, -1, -1]];\n                corners.forEach(([x, y, sx, sy]) => {\n                    ctx.beginPath(); ctx.moveTo(x, y + sy * 34); ctx.lineTo(x, y); ctx.lineTo(x + sx * 34, y); ctx.stroke();\n                    ctx.fillStyle = 'rgba(245, 224, 178, 0.9)'; ctx.beginPath();\n                    ctx.moveTo(x + sx * 8, y); ctx.lineTo(x, y + sy * 8); ctx.lineTo(x - sx * 8, y); ctx.lineTo(x, y - sy * 8); ctx.closePath(); ctx.fill();\n                });\n                ctx.restore();\n            }\n\n\n            function drawHalloweenFrosting(x, y, w, h) {\n                // Preserve the cake image underneath while giving its frosting a clear Halloween palette.\n                ctx.save();\n                ctx.globalAlpha = 0.76;\n                ctx.fillStyle = '#482858';\n                const left = x + w * 0.1, top = y + h * 0.27, bodyW = w * 0.8, bodyH = h * 0.55;\n                const r = Math.min(22, bodyW * 0.08);\n                ctx.beginPath();\n                ctx.moveTo(left + r, top);\n                ctx.lineTo(left + bodyW - r, top);\n                ctx.quadraticCurveTo(left + bodyW, top, left + bodyW, top + r);\n                ctx.lineTo(left + bodyW, top + bodyH - r);\n                ctx.quadraticCurveTo(left + bodyW, top + bodyH, left + bodyW - r, top + bodyH);\n                ctx.lineTo(left + r, top + bodyH);\n                ctx.quadraticCurveTo(left, top + bodyH, left, top + bodyH - r);\n                ctx.lineTo(left, top + r);\n                ctx.quadraticCurveTo(left, top, left + r, top);\n                ctx.closePath();\n                ctx.fill();\n                ctx.globalAlpha = 0.94;\n                ctx.fillStyle = '#75478a';\n                ctx.strokeStyle = '#ee9a3b';\n                ctx.lineWidth = Math.max(3, w * 0.012);\n                ctx.beginPath();\n                ctx.ellipse(x + w / 2, y + h * 0.28, w * 0.41, h * 0.16, 0, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.stroke();\n                ctx.fillStyle = '#f2a344';\n                for (let i = 0; i < 9; i++) {\n                    const a = Math.PI + (i / 8) * Math.PI;\n                    const dx = x + w / 2 + Math.cos(a) * w * 0.36;\n                    const dy = y + h * 0.28 + Math.sin(a) * h * 0.13;\n                    ctx.beginPath();\n                    ctx.arc(dx, dy + h * 0.035, Math.max(3, w * 0.012), 0, Math.PI * 2);\n                    ctx.fill();\n                }\n                ctx.restore();\n            }\n\n            function drawHalloweenDecoration(type, x, y, size) {\n                ctx.save();\n                ctx.translate(x, y);\n                ctx.scale(size / 100, size / 100);\n                ctx.lineJoin = 'round';\n                ctx.lineCap = 'round';\n                if (type === 'halloween_pumpkin') {\n                    ctx.fillStyle = '#f28b2f'; ctx.strokeStyle = '#a94d22'; ctx.lineWidth = 3;\n                    [-19, 0, 19].forEach((offset, i) => { ctx.beginPath(); ctx.ellipse(offset, 5, i === 1 ? 19 : 16, 27, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); });\n                    ctx.fillStyle = '#477348'; ctx.fillRect(-5, -29, 11, 16);\n                    ctx.fillStyle = '#492947';\n                    ctx.beginPath(); ctx.moveTo(-18, -2); ctx.lineTo(-9, 7); ctx.lineTo(-20, 10); ctx.closePath(); ctx.fill();\n                    ctx.beginPath(); ctx.moveTo(18, -2); ctx.lineTo(9, 7); ctx.lineTo(20, 10); ctx.closePath(); ctx.fill();\n                    ctx.beginPath(); ctx.moveTo(-12, 17); ctx.quadraticCurveTo(0, 29, 12, 17); ctx.lineTo(8, 12); ctx.lineTo(3, 20); ctx.lineTo(0, 12); ctx.lineTo(-5, 20); ctx.closePath(); ctx.fill();\n                } else if (type === 'halloween_ghost') {\n                    ctx.fillStyle = '#fff7e8'; ctx.strokeStyle = '#b9a7c6'; ctx.lineWidth = 3;\n                    ctx.beginPath(); ctx.moveTo(-28, 30); ctx.lineTo(-28, -4); ctx.quadraticCurveTo(-26, -32, 0, -34); ctx.quadraticCurveTo(27, -31, 28, -3); ctx.lineTo(28, 30); ctx.lineTo(14, 21); ctx.lineTo(2, 31); ctx.lineTo(-10, 21); ctx.closePath(); ctx.fill(); ctx.stroke();\n                    ctx.fillStyle = '#38233f'; ctx.beginPath(); ctx.ellipse(-10, -5, 4, 7, 0, 0, Math.PI * 2); ctx.ellipse(10, -5, 4, 7, 0, 0, Math.PI * 2); ctx.fill();\n                    ctx.beginPath(); ctx.arc(0, 9, 5, 0, Math.PI * 2); ctx.fill();\n                } else if (type === 'halloween_bat') {\n                    ctx.fillStyle = '#39213e'; ctx.strokeStyle = '#23162a'; ctx.lineWidth = 3;\n                    ctx.beginPath(); ctx.moveTo(-8, -8); ctx.lineTo(-18, -27); ctx.lineTo(-19, -12); ctx.quadraticCurveTo(-35, -27, -44, -18); ctx.lineTo(-38, 1); ctx.lineTo(-48, 14); ctx.quadraticCurveTo(-28, 12, -17, 28); ctx.lineTo(-8, 18); ctx.lineTo(0, 28); ctx.lineTo(17, 12); ctx.quadraticCurveTo(31, 12, 48, 14); ctx.lineTo(38, 1); ctx.lineTo(44, -18); ctx.quadraticCurveTo(28, -27, 18, -12); ctx.lineTo(18, -27); ctx.lineTo(8, -8); ctx.closePath(); ctx.fill(); ctx.stroke();\n                    ctx.fillStyle = '#ffd15f'; ctx.beginPath(); ctx.arc(-5, 0, 2.5, 0, Math.PI * 2); ctx.arc(5, 0, 2.5, 0, Math.PI * 2); ctx.fill();\n                } else if (type === 'halloween_candy_corn') {\n                    ctx.fillStyle = '#fff7e8'; ctx.strokeStyle = '#c97932'; ctx.lineWidth = 4;\n                    ctx.beginPath(); ctx.moveTo(0, -37); ctx.quadraticCurveTo(5, -29, 37, 32); ctx.quadraticCurveTo(40, 41, 29, 41); ctx.lineTo(-29, 41); ctx.quadraticCurveTo(-40, 41, -37, 32); ctx.quadraticCurveTo(-5, -29, 0, -37); ctx.closePath(); ctx.fill(); ctx.stroke();\n                    ctx.save(); ctx.beginPath(); ctx.moveTo(0, -37); ctx.quadraticCurveTo(5, -29, 37, 32); ctx.quadraticCurveTo(40, 41, 29, 41); ctx.lineTo(-29, 41); ctx.quadraticCurveTo(-40, 41, -37, 32); ctx.quadraticCurveTo(-5, -29, 0, -37); ctx.clip();\n                    ctx.fillStyle = '#f28b2f'; ctx.fillRect(-50, 3, 100, 22); ctx.fillStyle = '#ffd45a'; ctx.fillRect(-50, 25, 100, 25); ctx.restore();\n                } else if (type === 'halloween_spiderweb') {\n                    ctx.strokeStyle = '#fff0d3'; ctx.lineWidth = 3;\n                    for (let i = 0; i < 5; i++) { const a = (i / 4) * Math.PI / 2; ctx.beginPath(); ctx.moveTo(-35, -35); ctx.lineTo(-35 + Math.cos(a) * 75, -35 + Math.sin(a) * 75); ctx.stroke(); }\n                    for (let r = 16; r <= 70; r += 18) { ctx.beginPath(); ctx.arc(-35, -35, r, 0, Math.PI / 2); ctx.stroke(); }\n                } else if (type === 'halloween_boo') {\n                    ctx.font = 'bold 31px Georgia, serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';\n                    ctx.lineWidth = 5; ctx.strokeStyle = '#49264f'; ctx.strokeText('BOO!', 0, 0); ctx.fillStyle = '#ffb54c'; ctx.fillText('BOO!', 0, 0);\n                }\n                ctx.restore();\n            }\n\n            function drawTutorialArrow(x, y, angle = 0) {\n                if (!window.gameConfig.showHints) return;\n                ctx.save();\n                ctx.translate(x, y);\n                ctx.rotate(angle);\n                const bounce = Math.sin(Date.now() / 200) * 15;\n                ctx.translate(0, -20 - bounce);\n                \n                ctx.fillStyle = '#e98b3a';\n                ctx.strokeStyle = '#fff2d8';\n                ctx.lineWidth = 4;\n                ctx.beginPath();\n                ctx.moveTo(0, 0); // Tip\n                ctx.lineTo(-20, -30);\n                ctx.lineTo(-10, -30);\n                ctx.lineTo(-10, -60);\n                ctx.lineTo(10, -60);\n                ctx.lineTo(10, -30);\n                ctx.lineTo(20, -30);\n                ctx.closePath();\n                ctx.fill();\n                ctx.stroke();\n                ctx.restore();\n            }\n            function draw3DPan(x, y, width, depth, height, fillProgress, isBakingStage = 0) {\n                const bottomScale = 0.88; \n                const bottomWidth = width * bottomScale;\n                const bottomDepth = depth * bottomScale;\n                const centerX = x;\n                const centerY = y;\n\n                // 1. Bottom outer shadow\n                ctx.save();\n                ctx.fillStyle = 'rgba(0,0,0,0.15)';\n                ctx.beginPath();\n                ctx.ellipse(centerX, centerY + height + 3, bottomWidth / 2 + 5, bottomDepth / 2 + 5, 0, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.restore();\n\n                // 2. Inner back wall (stainless steel texture)\n                ctx.save();\n                const innerGradient = ctx.createLinearGradient(centerX - width / 2, centerY, centerX + width / 2, centerY);\n                innerGradient.addColorStop(0, '#555');\n                innerGradient.addColorStop(0.5, '#888');\n                innerGradient.addColorStop(1, '#555');\n                ctx.fillStyle = innerGradient;\n                ctx.beginPath();\n                ctx.ellipse(centerX, centerY, width / 2, depth / 2, 0, Math.PI, 0, false);\n                ctx.lineTo(centerX + bottomWidth / 2, centerY + height);\n                ctx.ellipse(centerX, centerY + height, bottomWidth / 2, bottomDepth / 2, 0, 0, Math.PI, true);\n                ctx.lineTo(centerX - width / 2, centerY);\n                ctx.fill();\n                ctx.restore();\n\n                // 3. Pan bottom (solves crack issues)\n                ctx.save();\n                ctx.fillStyle = '#777';\n                ctx.beginPath();\n                ctx.ellipse(centerX, centerY + height, bottomWidth / 2, bottomDepth / 2, 0, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.restore();\n\n                // 4. Batter fill (Procedural filling)\n                if (fillProgress > 0) {\n                    const fillHeight = fillProgress * height * 0.95;\n                    const fillY = centerY + height - fillHeight;\n                    const currentScale = bottomScale + (1 - bottomScale) * fillProgress;\n                    const fillW = width * currentScale;\n                    const fillD = depth * currentScale;\n\n                    ctx.save();\n                    let batterColor = '#F5DEB3'; \n                    if (isBakingStage === 1) batterColor = '#EACD9E';\n                    else if (isBakingStage === 2) batterColor = '#D2B48C';\n                    else if (isBakingStage === 3) batterColor = '#B8860B';\n\n                    ctx.fillStyle = batterColor;\n                    ctx.beginPath();\n                    ctx.ellipse(centerX, fillY, fillW / 2 - 2, fillD / 2 - 2, 0, 0, Math.PI * 2);\n                    ctx.fill();\n\n                    if (isBakingStage < 2) {\n                        ctx.globalAlpha = 0.3;\n                        ctx.strokeStyle = '#EACD9E';\n                        ctx.lineWidth = 2;\n                        for (let i = 1; i <= 2; i++) {\n                            ctx.beginPath();\n                            const rippleScale = 0.3 * i + Math.sin(Date.now() / 200 + i) * 0.05;\n                            ctx.ellipse(centerX, fillY, (fillW / 2) * rippleScale, (fillD / 2) * rippleScale, 0, 0, Math.PI * 2);\n                            ctx.stroke();\n                        }\n                    }\n                    ctx.restore();\n                }\n\n                // 5. Outer front wall (polished stainless steel)\n                ctx.save();\n                const outerGradient = ctx.createLinearGradient(centerX - width / 2, centerY, centerX + width / 2, centerY);\n                outerGradient.addColorStop(0, '#666'); \n                outerGradient.addColorStop(0.2, '#DDD'); \n                outerGradient.addColorStop(0.4, '#888'); \n                outerGradient.addColorStop(0.6, '#FFF'); \n                outerGradient.addColorStop(0.8, '#AAA'); \n                outerGradient.addColorStop(1, '#666'); \n                ctx.fillStyle = outerGradient;\n                ctx.beginPath();\n                ctx.ellipse(centerX, centerY, width / 2, depth / 2, 0, 0, Math.PI, false);\n                ctx.lineTo(centerX - bottomWidth / 2, centerY + height);\n                ctx.ellipse(centerX, centerY + height, bottomWidth / 2, bottomDepth / 2, 0, Math.PI, 0, true);\n                ctx.lineTo(centerX + width / 2, centerY);\n                ctx.fill();\n                // Removed stroke for smoother edges\n                \n                // Top edge reflection\n                ctx.beginPath();\n                ctx.ellipse(centerX, centerY, width / 2, depth / 2, 0, 0, Math.PI * 2);\n                ctx.strokeStyle = 'rgba(255,255,255,0.4)';\n                ctx.lineWidth = 1.5;\n                ctx.stroke();\n                ctx.restore();\n            }\n\n            function drawTurntable(x, y, rotation = 0, scale = 1.0) {\n                const width = 440 * scale;\n                const depth = 110 * scale;\n                const height = 25 * scale;\n                const baseWidth = 220 * scale;\n                const baseHeight = 50 * scale;\n                \n                // 1. Bottom outer shadow\n                ctx.save();\n                ctx.shadowBlur = 20 * scale;\n                ctx.shadowColor = 'rgba(0,0,0,0.2)';\n                ctx.fillStyle = 'rgba(0,0,0,0.1)';\n                ctx.beginPath();\n                ctx.ellipse(x, y + baseHeight + 20 * scale, width / 2 + 15 * scale, depth / 2 + 15 * scale, 0, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.restore();\n\n                // 2. Base (Sturdy professional base)\n                ctx.save();\n                const baseGrad = ctx.createLinearGradient(x - baseWidth/2, y, x + baseWidth/2, y);\n                baseGrad.addColorStop(0, '#9e7543');\n                baseGrad.addColorStop(0.3, '#e9d5a5');\n                baseGrad.addColorStop(0.5, '#fff5d9');\n                baseGrad.addColorStop(0.7, '#dfc58e');\n                baseGrad.addColorStop(1, '#9e7543');\n                \n                // Base bottom plate\n                ctx.fillStyle = baseGrad;\n                ctx.beginPath();\n                ctx.ellipse(x, y + baseHeight + 10 * scale, baseWidth / 2, 30 * scale, 0, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.strokeStyle = '#888';\n                ctx.lineWidth = 1 * scale;\n                ctx.stroke();\n\n                // Column support\n                ctx.beginPath();\n                ctx.moveTo(x - 40 * scale, y + height);\n                ctx.lineTo(x + 40 * scale, y + height);\n                ctx.lineTo(x + 50 * scale, y + baseHeight + 10 * scale);\n                ctx.lineTo(x - 50 * scale, y + baseHeight + 10 * scale);\n                ctx.fill();\n                ctx.stroke();\n                ctx.restore();\n\n                // 3. Turntable top\n                ctx.save();\n                // Plate side thickness\n                ctx.fillStyle = '#b18b50';\n                ctx.beginPath();\n                ctx.ellipse(x, y + height/2, width / 2, depth / 2, 0, 0, Math.PI, false);\n                ctx.lineTo(x - width / 2, y);\n                ctx.ellipse(x, y, width / 2, depth / 2, 0, Math.PI, 0, true);\n                ctx.fill();\n\n                // Plate top surface\n                const plateGrad = ctx.createRadialGradient(x, y, 0, x, y, width/2);\n                plateGrad.addColorStop(0, '#fffdf5');\n                plateGrad.addColorStop(0.8, '#f2e4c5');\n                plateGrad.addColorStop(1, '#d6bd85');\n                ctx.fillStyle = plateGrad;\n                ctx.beginPath();\n                ctx.ellipse(x, y, width / 2, depth / 2, 0, 0, Math.PI * 2);\n                ctx.fill();\n                \n                // Rotation scale lines/decorations\n                ctx.save();\n                ctx.translate(x, y);\n                ctx.rotate(rotation);\n                ctx.strokeStyle = 'rgba(165,125,61,0.22)';\n                ctx.lineWidth = 2 * scale;\n                for(let i = 0; i < 12; i++) {\n                    ctx.beginPath();\n                    const angle = (i / 12) * Math.PI * 2;\n                    ctx.moveTo(Math.cos(angle) * (width/2 - 15 * scale), Math.sin(angle) * (depth/2 - 4 * scale));\n                    ctx.lineTo(Math.cos(angle) * (width/2 - 4 * scale), Math.sin(angle) * (depth/2 - 2 * scale));\n                    ctx.stroke();\n                }\n                \n                // Ring decorations\n                for(let i = 1; i <= 2; i++) {\n                    ctx.beginPath();\n                    ctx.ellipse(0, 0, (width / 2) * (i / 3), (depth / 2) * (i / 3), 0, 0, Math.PI * 2);\n                    ctx.stroke();\n                }\n                ctx.restore();\n                \n                // Plate highlight\n                ctx.globalAlpha = 0.3;\n                ctx.fillStyle = '#FFF';\n                ctx.beginPath();\n                ctx.ellipse(x - 15 * scale, y - 4 * scale, width / 3, depth / 4, 0.4, 0, Math.PI * 2);\n                ctx.fill();\n                ctx.restore();\n            }\n\n            // Step 1: Prepare Ingredients\n            function initStep1() {\n                currentStepData = {\n                    added: {},\n                    animating: null,\n                    particles: [],\n                    positions: {\n                        // Separate, balanced 3D ingredient props surround one clear mixing bowl; no composite prep photo is used.\n                        flour: { x: 28, y: 338, w: 142, h: 185, asset: 'cinnamon_roll_3d_flour_pour_bowl', fallbackAsset: 'flour_bag', color: '#FFFFFF', name: 'Bread flour \u00b7 300 g' },\n                        sugar: { x: 178, y: 344, w: 124, h: 154, asset: 'cinnamon_roll_3d_sugar_pour_bowl', fallbackAsset: 'sugar_bag', color: '#E6C785', name: 'Sugar \u00b7 30 g' },\n                        water: { x: 306, y: 350, w: 104, h: 142, asset: 'cinnamon_roll_3d_water_glass', color: '#B8DCE8', name: 'Water \u00b7 50 ml' },\n                        salt: { x: 414, y: 350, w: 116, h: 142, asset: 'cinnamon_roll_3d_salt_bowl', color: '#F2EEE5', name: 'Salt \u00b7 5 g' },\n                        milk: { x: 542, y: 338, w: 146, h: 190, asset: 'cinnamon_roll_3d_milk_pour_jug', fallbackAsset: 'milk_bottle', color: '#F0F8FF', name: 'Warm milk \u00b7 120 ml' },\n                        yeast: { x: 42, y: 602, w: 142, h: 148, asset: 'cinnamon_roll_3d_yeast_bowl', color: '#D9B88A', name: 'Yeast \u00b7 7 g' },\n                        eggs: { x: 538, y: 638, w: 154, h: 152, asset: 'eggs_carton', color: '#FFD700', name: 'Egg \u00b7 1' }\n                    }\n                };\n                stepContent.innerHTML = '';\n                \n                const handleClick = (e) => {\n                    if (currentStepData.animating) return;\n                    const pos = getInputPosition(e);\n                    Object.keys(currentStepData.positions).forEach(key => {\n                        if (currentStepData.added[key]) return;\n                        const p = currentStepData.positions[key];\n                        if (pos.x >= p.x && pos.x <= p.x + p.w && pos.y >= p.y && pos.y <= p.y + p.h) {\n                            currentStepData.animating = { key, startTime: Date.now() };\n                            playSound('select_sound');\n                        }\n                    });\n                };\n\n                canvas.addEventListener('click', handleClick);\n                canvas.addEventListener('touchstart', (e) => {\n                    e.preventDefault();\n                    handleClick(e);\n                }, { passive: false });\n                \n                actionButton.textContent = 'Next';\n                actionButton.disabled = true;\n                actionButton.onclick = () => {\n                    if (checkStep1Complete()) {\n                        playSound('step_complete_sound');\n                        nextStep();\n                    }\n                };\n            }\n\n            function checkStep1Complete() {\n                if (!currentStepData.added) return false;\n                const required = ['flour', 'yeast', 'sugar', 'salt', 'milk', 'water', 'eggs'];\n                const allAdded = required.every(key => currentStepData.added[key]);\n                if (allAdded) stepScores[0] = 10;\n                return allAdded;\n            }\n            // Natural, staged transfer animation for Step 1: lift the ingredient vessel, bring it over the bowl, tip and pour, then return it upright. The vessel never falls into the bowl.\n            const pourClamp = value => Math.max(0, Math.min(1, value));\n            const pourEase = value => { const t = pourClamp(value); return t * t * (3 - 2 * t); };\n            const pourPoint = (startX, startY, controlX, controlY, endX, endY, t) => {\n                const u = 1 - t;\n                return { x: u * u * startX + 2 * u * t * controlX + t * t * endX,\n                         y: u * u * startY + 2 * u * t * controlY + t * t * endY };\n            };\n            const drawPourLabel = (p, x, y, w, h) => {\n                const parts = String(p.name || '').split(' \u00b7 ');\n                ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.lineJoin = 'round';\n                ctx.font = '700 19px \"Noto Sans TC\", sans-serif'; ctx.lineWidth = 5;\n                ctx.strokeStyle = 'rgba(255,248,235,.98)'; ctx.strokeText(parts[0], x + w / 2, y + h - 29, w - 6);\n                ctx.fillStyle = '#2e2018'; ctx.fillText(parts[0], x + w / 2, y + h - 29, w - 6);\n                if (parts[1]) {\n                    ctx.font = '700 17px \"Noto Sans TC\", sans-serif';\n                    ctx.strokeText(parts[1], x + w / 2, y + h - 9, w - 6);\n                    ctx.fillStyle = '#493126'; ctx.fillText(parts[1], x + w / 2, y + h - 9, w - 6);\n                }\n                ctx.restore();\n            };\n\n            drawPouringIngredient = function(p, x, y, w, h, key, progress, bowlY, bowlCenterX) {\n                const homeX = x + w / 2, homeY = y + (h - 42) / 2;\n                const direction = homeX < bowlCenterX ? 1 : -1;\n                const vesselW = w - 18, vesselH = h - 48;\n                const preferredVessel = key === 'flour' ? 'cinnamon_roll_3d_flour_pour_bowl'\n                    : (key === 'sugar' ? 'cinnamon_roll_3d_sugar_pour_bowl'\n                    : (key === 'milk' ? 'cinnamon_roll_3d_milk_pour_jug' : p.asset));\n                const vesselAsset = assets[preferredVessel] ? preferredVessel : (p.fallbackAsset || p.asset);\n                // Carry the vessel directly above the bowl, not to its far side. The pour opening then sits over the bowl's centre.\n                const targetX = bowlCenterX - direction * 26;\n                const targetY = bowlY - 70;\n                const travel = progress < .83 ? pourEase(progress / .31) : 1 - pourEase((progress - .83) / .17);\n                const controlX = (homeX + targetX) / 2;\n                const controlY = Math.min(homeY, targetY) - 24;\n                const pos = pourPoint(homeX, homeY, controlX, controlY, targetX, targetY, travel);\n                const moveLift = Math.sin(travel * Math.PI) * 5;\n                const approach = pourEase((progress - .26) / .12);\n                const straighten = pourEase((progress - .75) / .12);\n                const tip = progress < .75 ? approach : 1 - straighten;\n                const maxTilt = key === 'milk' || key === 'water' || key === 'flour' || key === 'sugar' ? .66 : .48;\n                const tilt = direction * maxTilt * tip;\n                const flowFade = pourEase((progress - .35) / .09) * (1 - pourEase((progress - .75) / .09));\n\n                if (key === 'eggs' || key === 'butter') {\n                    // The carton/block stays on the counter. Carry one intact egg to the rim,\n                    // crack it open there, then let only the raw white and yolk fall into the bowl.\n                    drawStepIngredient(p, x, y, w, h);\n                    if (key === 'eggs') {\n                        const eggTravel = pourEase(progress / .30);\n                        const eggX = homeX + (targetX - homeX) * eggTravel;\n                        const eggY = homeY - 4 + (targetY - 18 - (homeY - 4)) * eggTravel - Math.sin(eggTravel * Math.PI) * 12;\n                        const crackProgress = pourEase((progress - .29) / .12);\n                        if (progress < .47) {\n                            ctx.save();\n                            ctx.globalAlpha = 1 - pourEase((progress - .40) / .07);\n                            ctx.shadowColor = 'rgba(28,18,12,.24)'; ctx.shadowBlur = 10; ctx.shadowOffsetY = 5;\n                            if (!drawImageFit('egg_single', eggX - 23, eggY - 31, 46, 62)) {\n                                const eggGrad = ctx.createLinearGradient(eggX - 18, eggY - 24, eggX + 15, eggY + 25);\n                                eggGrad.addColorStop(0, '#fffef5'); eggGrad.addColorStop(.56, '#f5e7ca'); eggGrad.addColorStop(1, '#cbb48c');\n                                ctx.fillStyle = eggGrad; ctx.beginPath(); ctx.ellipse(eggX, eggY, 17, 24, -.12, 0, Math.PI * 2); ctx.fill();\n                            }\n                            if (crackProgress > .12) {\n                                ctx.shadowColor = 'transparent'; ctx.strokeStyle = 'rgba(137,99,56,.78)'; ctx.lineWidth = 2.2;\n                                ctx.beginPath(); ctx.moveTo(eggX - 8, eggY - 9); ctx.lineTo(eggX - 2, eggY - 3); ctx.lineTo(eggX - 6, eggY + 2); ctx.lineTo(eggX + 1, eggY + 5); ctx.lineTo(eggX + 7, eggY + 12); ctx.stroke();\n                            }\n                            ctx.restore();\n                        } else if (progress < .83) {\n                            const shellProgress = pourEase((progress - .43) / .22);\n                            const shellAlpha = Math.max(0, 1 - pourEase((progress - .74) / .09));\n                            ctx.save(); ctx.globalAlpha = shellAlpha;\n                            drawImageFit('egg_shell_broken_bottom', targetX - 25 - shellProgress * 16, targetY - 8 - shellProgress * 5, 31, 32);\n                            drawImageFit('egg_shell_broken_top', targetX + 2 + shellProgress * 16, targetY - 16 - shellProgress * 5, 31, 32);\n                            ctx.restore();\n                        }\n                        return { x: targetX, y: targetY + 5, targetX: bowlCenterX, targetY: bowlY + 62 };\n                    }\n                    drawPourLabel(p, x, y, w, h);\n                    return { x: bowlCenterX, y: bowlY - 18, targetX: bowlCenterX, targetY: bowlY + 62 };\n                }\n\n                // The bowl-like prep dishes and bottles are carried upright, tipped only over the mixing bowl, then set back down.\n                ctx.save(); ctx.translate(pos.x, pos.y - moveLift); ctx.rotate(tilt);\n                ctx.shadowColor = 'rgba(25,14,8,.32)'; ctx.shadowBlur = 13; ctx.shadowOffsetY = 7;\n                if (!vesselAsset || !drawImageFit(vesselAsset, -vesselW / 2, -vesselH / 2, vesselW, vesselH)) {\n                    ctx.fillStyle = p.color || '#d9b88a'; ctx.beginPath(); ctx.ellipse(0, 0, vesselW * .32, vesselH * .3, 0, 0, Math.PI * 2); ctx.fill();\n                }\n                ctx.restore();\n                // Keep the name attached to the moving vessel, rather than leaving a detached label at its source.\n                drawPourLabel(p, pos.x - w / 2, pos.y - vesselH / 2 - 6, w, vesselH + 22);\n\n                // Calculate the actual low lip of the tilted vessel so the stream starts at its opening, not its centre.\n                const lipLocalX = direction * vesselW * .34, lipLocalY = vesselH * .22;\n                const streamX = pos.x + lipLocalX * Math.cos(tilt) - lipLocalY * Math.sin(tilt);\n                const streamY = pos.y - moveLift + lipLocalX * Math.sin(tilt) + lipLocalY * Math.cos(tilt);\n                return { x: streamX, y: streamY, targetX: bowlCenterX, targetY: bowlY + 62, flowFade };\n            };\n\n            drawIngredientPour = function(key, p, progress, startX, startY, targetX, targetY) {\n                const releaseStart = key === 'eggs' ? .47 : .35;\n                const active = pourEase((progress - releaseStart) / .08) * (1 - pourEase((progress - .84) / .10));\n                if (active <= .005) return;\n                const reach = pourEase((progress - releaseStart) / .37);\n                const dx = targetX - startX, dy = targetY - startY;\n                const controlX = startX + dx * .16;\n                const controlY = startY + dy * .58;\n                const end = pourPoint(startX, startY, controlX, controlY, targetX, targetY, reach);\n                const isLiquid = key === 'milk' || key === 'water' || key === 'eggs';\n\n                ctx.save(); ctx.globalAlpha = active;\n                if (key === 'eggs') {\n                    // A raw egg falls as translucent white with one soft yolk, never as a cooked-looking sunny-side-up disc.\n                    const streamStartX = startX, streamStartY = startY + 5;\n                    const controlX = streamStartX + (targetX - streamStartX) * .12;\n                    const controlY = streamStartY + (targetY - streamStartY) * .60;\n                    const end = pourPoint(streamStartX, streamStartY, controlX, controlY, targetX, targetY, reach);\n                    ctx.lineCap = 'round'; ctx.lineJoin = 'round';\n                    ctx.beginPath(); ctx.moveTo(streamStartX, streamStartY); ctx.quadraticCurveTo(controlX, controlY, end.x, end.y);\n                    ctx.strokeStyle = 'rgba(255,250,231,.62)'; ctx.lineWidth = 9; ctx.stroke();\n                    ctx.beginPath(); ctx.moveTo(streamStartX, streamStartY); ctx.quadraticCurveTo(controlX - 1, controlY, end.x - 1, end.y);\n                    ctx.strokeStyle = 'rgba(255,255,255,.42)'; ctx.lineWidth = 2; ctx.stroke();\n                    const yolkTravel = pourEase((progress - .51) / .27);\n                    if (progress > .51 && progress < .82) {\n                        const yolkPos = pourPoint(streamStartX, streamStartY, controlX, controlY, targetX, targetY, Math.min(.98, yolkTravel));\n                        const yolkGrad = ctx.createRadialGradient(yolkPos.x - 4, yolkPos.y - 5, 1, yolkPos.x, yolkPos.y, 12);\n                        yolkGrad.addColorStop(0, '#f8d781'); yolkGrad.addColorStop(.62, '#d99a32'); yolkGrad.addColorStop(1, '#a96322');\n                        ctx.fillStyle = yolkGrad; ctx.beginPath(); ctx.ellipse(yolkPos.x, yolkPos.y, 8, 10, .18, 0, Math.PI * 2); ctx.fill();\n                        ctx.fillStyle = 'rgba(255,239,188,.48)'; ctx.beginPath(); ctx.ellipse(yolkPos.x - 2, yolkPos.y - 4, 2.4, 1.5, -.3, 0, Math.PI * 2); ctx.fill();\n                    }\n                } else if (key === 'milk' || key === 'water') {\n                    // Milk and water pour as a stable, narrow gravity-fed stream from the vessel lip.\n                    const streamStartX = startX, streamStartY = startY;\n                    const nearVerticalControlX = streamStartX + (targetX - streamStartX) * .16;\n                    const gravityControlY = streamStartY + (targetY - streamStartY) * .58;\n                    const end = pourPoint(streamStartX, streamStartY, nearVerticalControlX, gravityControlY, targetX, targetY, reach);\n                    ctx.lineCap = 'round'; ctx.lineJoin = 'round';\n                    ctx.beginPath(); ctx.moveTo(streamStartX, streamStartY); ctx.quadraticCurveTo(nearVerticalControlX, gravityControlY, end.x, end.y);\n                    ctx.strokeStyle = key === 'water' ? 'rgba(190,231,239,.94)' : 'rgba(255,244,220,.98)';\n                    ctx.lineWidth = key === 'water' ? 4.5 : 6; ctx.stroke();\n                    ctx.beginPath(); ctx.moveTo(streamStartX - 1, streamStartY); ctx.quadraticCurveTo(nearVerticalControlX - 1, gravityControlY, end.x - 1, end.y);\n                    ctx.strokeStyle = 'rgba(255,255,255,.78)'; ctx.lineWidth = 1.4; ctx.stroke();\n                    ctx.globalAlpha = active * .40; ctx.strokeStyle = 'rgba(255,255,255,.86)'; ctx.lineWidth = 1.7;\n                    ctx.beginPath(); ctx.ellipse(targetX, targetY - 2, 9 + (progress * 24) % 6, 2.5, 0, 0, Math.PI * 2); ctx.stroke();\n                } else if (key === 'butter') {\n                    // Small cut butter pieces fall from the stationary butter block, one after another.\n                    for (let i = 0; i < 7; i++) {\n                        const t = pourClamp((progress - .34 - i * .045) / .34);\n                        if (t <= 0 || t >= 1) continue;\n                        const pt = pourPoint(startX + i * 2, startY, controlX, controlY, targetX, targetY, t);\n                        const size = 6 + (i % 3) * 1.5;\n                        ctx.save(); ctx.translate(pt.x + Math.sin(i * 2.1) * 4, pt.y); ctx.rotate(t * 1.5 + i * .4);\n                        const butterGrad = ctx.createLinearGradient(-size, -size, size, size);\n                        butterGrad.addColorStop(0, '#fff4bf'); butterGrad.addColorStop(.55, '#f2d47e'); butterGrad.addColorStop(1, '#c99a43');\n                        ctx.fillStyle = butterGrad; ctx.strokeStyle = 'rgba(152,105,43,.45)'; ctx.lineWidth = 1.2;\n                        ctx.beginPath(); ctx.roundRect(-size, -size * .72, size * 2, size * 1.45, 2); ctx.fill(); ctx.stroke(); ctx.restore();\n                    }\n                } else {\n                    // Dry flour, sugar, yeast and salt tumble as separate granules, never as a solid liquid-like ribbon.\n                    const tone = key === 'salt' ? '#fffdf4' : key === 'yeast' ? '#c89e67' : key === 'sugar' ? '#fff0ca' : '#f5ead6';\n                    const fallHeight = Math.max(1, targetY - startY);\n                    const horizontalTravel = targetX - startX;\n                    const releaseStart = .39, releaseSpan = .20, particleLife = .26;\n                    const particleCount = key === 'flour' ? 68 : 52;\n                    for (let i = 0; i < particleCount; i++) {\n                        const release = releaseStart + (i / particleCount) * releaseSpan;\n                        const age = (progress - release) / particleLife;\n                        if (age < 0 || age > 1) continue;\n                        const fall = age * age;\n                        const spread = 1.5 + age * (key === 'flour' ? 14 : 9);\n                        const seedX = Math.sin(i * 127.1 + key.length * 31.7);\n                        const seedY = Math.cos(i * 91.7 + key.length * 13.3);\n                        const drift = Math.sin(progress * 20 + i * 2.4) * (1.2 + age * 1.8);\n                        // Released dry particles travel from the tipped vessel lip toward the center of the bowl as they fall.\n                        const px = startX + horizontalTravel * age + seedX * spread + drift;\n                        const py = startY + fallHeight * fall;\n                        const size = key === 'flour' ? 1.1 + (i % 3) * .48 : (key === 'yeast' ? 2.0 + (i % 3) * .65 : 1.7 + (i % 4) * .55);\n                        ctx.globalAlpha = active * (1 - age * .12) * (.62 + .30 * ((i % 5) / 4));\n                        ctx.fillStyle = tone;\n                        ctx.beginPath(); ctx.ellipse(px, py, size, size * .78, seedX * .6, 0, Math.PI * 2); ctx.fill();\n                    }\n                    // A soft little mound gathers where the dry ingredient lands in the bowl.\n                    const pileProgress = pourEase((progress - .48) / .34);\n                    if (pileProgress > 0) {\n                        const pileColor = key === 'flour' ? '#fff4df' : (key === 'salt' ? '#fffdf4' : (key === 'yeast' ? '#c99b63' : '#f2dfb7'));\n                        ctx.globalAlpha = active * .82;\n                        ctx.fillStyle = pileColor;\n                        ctx.beginPath();\n                        ctx.ellipse(targetX, targetY - 3, 7 + 18 * pileProgress, 2.5 + 6 * pileProgress, 0, Math.PI, Math.PI * 2);\n                        ctx.lineTo(targetX - (7 + 18 * pileProgress), targetY - 3);\n                        ctx.fill();\n                        ctx.globalAlpha = active * .72;\n                        for (let i = 0; i < 16; i++) {\n                            const sx = Math.sin(i * 19.3 + key.length) * (5 + 15 * pileProgress);\n                            const sy = Math.cos(i * 7.1 + key.length) * (2 + 3 * pileProgress);\n                            ctx.beginPath(); ctx.arc(targetX + sx, targetY - 4 + sy, 1.2 + (i % 3) * .55, 0, Math.PI * 2); ctx.fill();\n                        }\n                    }\n                }\n                ctx.restore();\n            };\n\n            // Ingredients visibly accumulate inside the real mixing bowl after each pour, instead of the bowl resetting to empty.\n            function drawStep1BowlContents(centerX, bowlY) {\n                const innerX = centerX;\n                // Align the contents with the visible opening at the top of the bowl, not its front wall.\n                const innerY = bowlY + 58;\n                const added = currentStepData.added || {};\n                const active = currentStepData.animating;\n                const activeKey = active && active.key;\n                const duration = activeKey === 'eggs' ? 950 : 800;\n                const activeProgress = active ? Math.max(0, Math.min(1, (Date.now() - active.startTime) / duration)) : 0;\n                const dryKeys = ['flour', 'sugar', 'yeast', 'salt'];\n                const dryCount = dryKeys.filter(key => added[key] || (activeKey === key && activeProgress > .58)).length;\n                const liquidCount = ['milk', 'water'].filter(key => added[key] || (activeKey === key && activeProgress > .58)).length;\n                const eggInBowl = added.eggs || (activeKey === 'eggs' && activeProgress > .56);\n                if (!dryCount && !liquidCount && !eggInBowl) return;\n                const ingredientAmount = key => added[key] ? 1 : (activeKey === key ? Math.max(0, Math.min(1, (activeProgress - .42) / .42)) : 0);\n\n                ctx.save();\n                ctx.beginPath();\n                ctx.ellipse(innerX, innerY, 103, 27, 0, 0, Math.PI * 2);\n                ctx.clip();\n\n                // Soft flour bed, settling naturally at the bottom of the bowl.\n                if (ingredientAmount('flour') > 0) {\n                    const flourAmount = ingredientAmount('flour');\n                    const flour = ctx.createRadialGradient(innerX - 8, innerY - 8, 3, innerX, innerY, 94);\n                    flour.addColorStop(0, '#fff8e9'); flour.addColorStop(.68, '#eadcc2'); flour.addColorStop(1, '#bda98b');\n                    ctx.fillStyle = flour;\n                    ctx.beginPath(); ctx.ellipse(innerX, innerY + 7, 83 * flourAmount, (7 + 12 * flourAmount), -.02, 0, Math.PI * 2); ctx.fill();\n                    ctx.globalAlpha = .34;\n                    for (let i = 0; i < 46; i++) {\n                        const px = innerX + Math.sin(i * 17.1) * (18 + (i % 7) * 8);\n                        const py = innerY + 5 + Math.cos(i * 9.7) * (5 + (i % 4) * 4);\n                        ctx.fillStyle = i % 3 ? '#fffdf5' : '#cbb99b';\n                        ctx.beginPath(); ctx.ellipse(px, py, 1.2 + (i % 3) * .45, .7 + (i % 2) * .35, 0, 0, Math.PI * 2); ctx.fill();\n                    }\n                    ctx.globalAlpha = 1;\n                }\n\n                // Distinct dry crystals and yeast granules settle on the flour rather than forming a liquid-like stripe.\n                const dryIngredients = [\n                    ['sugar', '#e7d3a9'], ['yeast', '#b78a55'], ['salt', '#fffdf0']\n                ];\n                dryIngredients.forEach(([key, color], layer) => {\n                    const amount = ingredientAmount(key);\n                    if (amount <= 0) return;\n                    ctx.fillStyle = color;\n                    for (let i = 0; i < Math.ceil(27 * amount); i++) {\n                        const px = innerX + Math.sin(i * 12.3 + layer * 2.1) * (15 + (i % 6) * 10);\n                        const py = innerY + 3 + Math.cos(i * 7.4 + layer) * (4 + (i % 4) * 5);\n                        const r = key === 'yeast' ? 1.7 + (i % 3) * .55 : 1.1 + (i % 3) * .4;\n                        ctx.globalAlpha = .62 + (i % 4) * .08;\n                        ctx.beginPath(); ctx.ellipse(px, py, r * 1.35, r, (i % 5) * .3, 0, Math.PI * 2); ctx.fill();\n                    }\n                });\n                ctx.globalAlpha = 1;\n\n                // Milk and water pool together as a glossy shallow liquid at the bottom of the bowl.\n                if (liquidCount) {\n                    const liquidAmount = Math.max(ingredientAmount('milk'), ingredientAmount('water'));\n                    const liquid = ctx.createLinearGradient(innerX, innerY - 18, innerX, innerY + 21);\n                    liquid.addColorStop(0, 'rgba(255,252,237,.86)'); liquid.addColorStop(.45, 'rgba(236,223,194,.72)'); liquid.addColorStop(1, 'rgba(177,145,103,.68)');\n                    ctx.fillStyle = liquid;\n                    ctx.beginPath(); ctx.ellipse(innerX, innerY + 13, (26 + liquidCount * 17) * liquidAmount, (5 + liquidCount * 3) * liquidAmount, 0, 0, Math.PI * 2); ctx.fill();\n                    ctx.strokeStyle = 'rgba(255,255,255,.64)'; ctx.lineWidth = 2;\n                    ctx.beginPath(); ctx.ellipse(innerX - 4, innerY + 11, (20 + liquidCount * 13) * liquidAmount, (4 + liquidCount * 2) * liquidAmount, -.03, Math.PI * 1.05, Math.PI * 1.9); ctx.stroke();\n                }\n\n                // Raw egg white rests as a loose translucent pool mixed into the ingredients; its small yolk is softly submerged,\n                // with no crisp circular white edge or flat sunny-side-up appearance.\n                if (eggInBowl) {\n                    const eggAmount = ingredientAmount('eggs');\n                    ctx.save(); ctx.globalAlpha = .72 * eggAmount;\n                    ctx.fillStyle = 'rgba(255,249,226,.54)';\n                    ctx.beginPath();\n                    ctx.moveTo(innerX - 50, innerY + 8);\n                    ctx.bezierCurveTo(innerX - 42, innerY - 3, innerX - 28, innerY + 1, innerX - 20, innerY - 4);\n                    ctx.bezierCurveTo(innerX - 8, innerY - 11, innerX + 1, innerY - 2, innerX + 12, innerY - 5);\n                    ctx.bezierCurveTo(innerX + 29, innerY - 8, innerX + 46, innerY - 1, innerX + 49, innerY + 8);\n                    ctx.bezierCurveTo(innerX + 43, innerY + 15, innerX + 26, innerY + 13, innerX + 13, innerY + 17);\n                    ctx.bezierCurveTo(innerX - 2, innerY + 20, innerX - 12, innerY + 12, innerX - 25, innerY + 16);\n                    ctx.bezierCurveTo(innerX - 38, innerY + 19, innerX - 47, innerY + 15, innerX - 50, innerY + 8);\n                    ctx.closePath(); ctx.fill();\n                    ctx.globalAlpha = .88 * eggAmount;\n                    const yolk = ctx.createRadialGradient(innerX + 1, innerY - 6, 1, innerX + 4, innerY + 4, 13);\n                    yolk.addColorStop(0, '#f5d37d'); yolk.addColorStop(.58, '#d99a32'); yolk.addColorStop(1, '#9d5b24');\n                    ctx.fillStyle = yolk; ctx.beginPath(); ctx.ellipse(innerX + 5, innerY + 4, 9, 7, -.12, 0, Math.PI * 2); ctx.fill();\n                    ctx.globalAlpha = .28 * eggAmount; ctx.fillStyle = '#fff1bf';\n                    ctx.beginPath(); ctx.ellipse(innerX + 2, innerY + 1, 2.2, 1.1, -.25, 0, Math.PI * 2); ctx.fill();\n                    ctx.restore();\n                }\n                ctx.restore();\n            }\n\n            function renderStep1() {\n                // Keep one continuous interactive work surface. Ingredients are physically carried,\n                // tilted, released into the same bowl, and shown accumulating instead of cutting to a still photo.\n                drawBackground();\n                // Use individual realistic props in the interactive scene; composite photos looked like a random picture card.\n                const bowlWidth = 300;\n                const bowlX = canvas.width / 2 - bowlWidth / 2;\n                const bowlY = 530;\n                const bowlCenterX = canvas.width / 2;\n                const bowlCenterY = bowlY + 120;\n\n                if (currentStepData.particles) {\n                    for (let i = currentStepData.particles.length - 1; i >= 0; i--) {\n                        const p = currentStepData.particles[i];\n                        p.x += p.vx; p.y += p.vy; p.vy += 0.5; p.life -= 0.02;\n                        ctx.save(); ctx.globalAlpha = Math.max(0, p.life); ctx.fillStyle = p.color;\n                        ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); ctx.restore();\n                        if (p.life <= 0 || p.y > bowlCenterY + 50) currentStepData.particles.splice(i, 1);\n                    }\n                }\n                drawImageFit('mixing_bowl_large', bowlX, bowlY, bowlWidth, 240);\n                drawStep1BowlContents(bowlCenterX, bowlY);\n                Object.keys(currentStepData.positions).forEach(key => {\n                    const p = currentStepData.positions[key];\n                    if (currentStepData.added[key]) return;\n                    const isAnimating = currentStepData.animating && currentStepData.animating.key === key;\n                    if (isAnimating) {\n                        const elapsed = Date.now() - currentStepData.animating.startTime;\n                        const duration = key === 'eggs' ? 950 : 800;\n                        const progress = Math.min(1, elapsed / duration);\n                        const pourOrigin = drawPouringIngredient(p, p.x, p.y, p.w, p.h, key, progress, bowlY, bowlCenterX);\n                        if (pourOrigin) drawIngredientPour(key, p, progress, pourOrigin.x, pourOrigin.y, pourOrigin.targetX, pourOrigin.targetY);\n                        if (progress >= 1) {\n                            currentStepData.added[key] = true; currentStepData.animating = null;\n                            const ingredient = window.gameConfig.ingredients[key];\n                            if (ingredient) ingredient.current = ingredient.target;\n                        }\n                    } else {\n                        drawStepIngredient(p, p.x, p.y, p.w, p.h);\n                    }\n                });\n                if (!checkStep1Complete() && !currentStepData.animating) {\n                    const nextKey = Object.keys(currentStepData.positions).find(key => !currentStepData.added[key]);\n                    if (nextKey) { const p = currentStepData.positions[nextKey]; drawTutorialArrow(p.x + p.w / 2, p.y + p.h / 4, 0); }\n                } else if (checkStep1Complete()) drawTutorialArrow(canvas.width / 2, canvas.height - 150, 0);\n            }\n\n            function updateStep1() {\n                actionButton.disabled = !(checkStep1Complete() && !currentStepData.animating);\n                if (!actionButton.disabled) actionButton.style.opacity = \"1\";\n            }\n\n\n            // Step 2: Mix first, develop gluten, add butter, knead smooth, then complete bulk fermentation.\n            function initStep2() {\n                currentStepData = {\n                    phase: 0, progress: 0, dragging: false, lastX: 0, lastY: 0,\n                    pointerX: canvas.width / 2, pointerY: canvas.height / 2,\n                    butterAdded: false, butterAnimation: 0, butterReady: false,\n                    riseProgress: 0, riseComplete: false\n                };\n                stepContent.innerHTML = '';\n                actionButton.classList.remove('hidden');\n                actionButton.textContent = 'Knead the dough \u00b7 \u63c9\u9eb5';\n                actionButton.disabled = true;\n\n                const bowlCenter = () => ({ x: canvas.width / 2, y: canvas.height * .54 });\n                const addButter = () => {\n                    if (currentStepData.phase !== 2) return;\n                    currentStepData.phase = 3;\n                    currentStepData.progress = 0;\n                    currentStepData.butterAdded = true;\n                    currentStepData.butterAnimation = 0;\n                    currentStepData.butterReady = false;\n                    actionButton.textContent = 'Knead in butter \u00b7 \u63c9\u5165\u725b\u6cb9';\n                    actionButton.disabled = true;\n                    playSound('select_sound');\n                };\n                const finishKneadPhase = () => {\n                    currentStepData.dragging = false;\n                    currentStepData.progress = 0;\n                    if (currentStepData.phase === 0) {\n                        currentStepData.phase = 1;\n                        actionButton.textContent = 'Develop the gluten \u00b7 \u63c9\u81f3\u5c55\u5ef6';\n                        playSound('step_complete_sound', .6);\n                    } else if (currentStepData.phase === 1) {\n                        currentStepData.phase = 2;\n                        actionButton.textContent = 'Add butter \u00b7 \u52a0\u5165\u725b\u6cb9';\n                        actionButton.disabled = false;\n                        actionButton.onclick = addButter;\n                        playSound('step_complete_sound', .7);\n                    } else if (currentStepData.phase === 3) {\n                        currentStepData.phase = 4;\n                        currentStepData.riseProgress = 0;\n                        actionButton.textContent = 'First rise in progress \u00b7 \u9996\u6b21\u767c\u9175\u4e2d';\n                        actionButton.disabled = true;\n                        actionButton.onclick = null;\n                        playSound('step_complete_sound', .7);\n                    }\n                };\n\n                const handlePointerDown = e => {\n                    e.preventDefault();\n                    const pos = getInputPosition(e);\n                    currentStepData.pointerX = pos.x;\n                    currentStepData.pointerY = pos.y;\n                    if (currentStepData.phase === 2) {\n                        const c = bowlCenter();\n                        if (pos.x >= c.x + 125 && pos.x <= c.x + 315 && pos.y >= c.y - 190 && pos.y <= c.y - 45) addButter();\n                        return;\n                    }\n                    if (![0, 1, 3].includes(currentStepData.phase) || (currentStepData.phase === 3 && !currentStepData.butterReady)) return;\n                    const c = bowlCenter();\n                    const nx = (pos.x - c.x) / 195, ny = (pos.y - c.y) / 125;\n                    if (nx * nx + ny * ny > 1.25) return;\n                    currentStepData.dragging = true;\n                    currentStepData.lastX = pos.x;\n                    currentStepData.lastY = pos.y;\n                };\n                const handlePointerMove = e => {\n                    const pos = getInputPosition(e);\n                    currentStepData.pointerX = pos.x;\n                    currentStepData.pointerY = pos.y;\n                    if (!currentStepData.dragging) return;\n                    e.preventDefault();\n                    const distance = Math.hypot(pos.x - currentStepData.lastX, pos.y - currentStepData.lastY);\n                    if (distance > 2) {\n                        const thresholds = { 0: 620, 1: 1050, 3: 900 };\n                        currentStepData.progress = Math.min(100, currentStepData.progress + Math.min(distance, 65) * 100 / thresholds[currentStepData.phase]);\n                        currentStepData.lastX = pos.x;\n                        currentStepData.lastY = pos.y;\n                        if (currentStepData.progress >= 100) finishKneadPhase();\n                    }\n                };\n                const handlePointerUp = () => { currentStepData.dragging = false; };\n                canvas.addEventListener('pointerdown', handlePointerDown);\n                canvas.addEventListener('pointermove', handlePointerMove);\n                canvas.addEventListener('pointerup', handlePointerUp);\n                canvas.addEventListener('pointercancel', handlePointerUp);\n\n                actionButton.onclick = () => {\n                    if (currentStepData.phase === 2) addButter();\n                    else if (currentStepData.riseComplete) {\n                        stepScores[1] = 10;\n                        playSound('step_complete_sound');\n                        nextStep();\n                    }\n                };\n            }\n\n            function updateStep2(deltaTime = 0) {\n                if (currentStepData.phase === 3 && !currentStepData.butterReady) {\n                    currentStepData.butterAnimation = Math.min(1, currentStepData.butterAnimation + deltaTime / 1.25);\n                    if (currentStepData.butterAnimation >= 1) currentStepData.butterReady = true;\n                }\n                if (currentStepData.phase === 4 && !currentStepData.riseComplete) {\n                    currentStepData.riseProgress = Math.min(100, currentStepData.riseProgress + deltaTime * 12.5);\n                    if (currentStepData.riseProgress >= 100) {\n                        currentStepData.riseComplete = true;\n                        actionButton.textContent = 'Next \u00b7 \u64c0\u958b\u9eb5\u7cf0';\n                        actionButton.disabled = false;\n                        actionButton.onclick = () => {\n                            stepScores[1] = 10;\n                            playSound('step_complete_sound');\n                            nextStep();\n                        };\n                    }\n                }\n            }\n\n            function renderStep2() {\n                const centerX = canvas.width / 2;\n                const data = currentStepData;\n                const phaseTitles = ['\u6df7\u5408\u6750\u6599 \u00b7 \u6210\u70ba\u7c97\u7cd9\u9eb5\u7cf0', '\u63c9\u81f3\u5c55\u5ef6 \u00b7 \u8584\u819c\u6e2c\u8a66', '\u9eb5\u7b4b\u5f62\u6210 \u00b7 \u52a0\u5165\u7121\u9e7d\u725b\u6cb9', '\u63c9\u5165\u725b\u6cb9 \u00b7 \u5149\u6ed1\u6709\u5f48\u6027', '\u7b2c\u4e00\u6b21\u57fa\u672c\u767c\u9175'];\n                const instructions = [\n                    '\u4ee5\u7b2c\u4e00\u8eab\u8996\u89d2\u5728\u7897\u4e2d\u756b\u5708\u62d6\u52d5\uff0c\u5c07\u6750\u6599\u63c9\u6210\u5718\u3002',\n                    '\u62c9\u958b\u9eb5\u7cf0\u6e2c\u8a66\u8584\u819c\uff0c\u7e7c\u7e8c\u63c9\u81f3\u9eb5\u7b4b\u5f62\u6210\u3002',\n                    '\u628a\u8edf\u5316\u725b\u6cb9\u653e\u9032\u9eb5\u7cf0\uff0c\u518d\u7e7c\u7e8c\u624b\u63c9\u3002',\n                    data.butterReady ? '\u725b\u6cb9\u5df2\u5438\u6536\uff0c\u7e7c\u7e8c\u63c9\u81f3\u9eb5\u7cf0\u5149\u6ed1\u6709\u5f48\u6027\u3002' : '\u725b\u6cb9\u6b63\u9010\u6b65\u63c9\u5165\u9eb5\u7cf0\uff0c\u7a0d\u5019\u518d\u7e7c\u7e8c\u63c9\u3002',\n                    data.riseComplete ? '\u9eb5\u7cf0\u5df2\u5b8c\u6210\u9996\u6b21\u767c\u9175\u3002' : '\u8b93\u9eb5\u7cf0\u975c\u7f6e\uff0c\u5b8c\u6210\u7b2c\u4e00\u6b21\u57fa\u672c\u767c\u9175\u3002'\n                ];\n                const imageByPhase = [\n                    'cinnamon_roll_pov_shaggy_dough',\n                    'cinnamon_roll_pov_windowpane_dough',\n                    'cinnamon_roll_pov_adding_butter',\n                    data.butterReady ? 'cinnamon_roll_pov_kneading_smooth_dough' : 'cinnamon_roll_pov_adding_butter',\n                    'cinnamon_roll_pov_first_rise'\n                ];\n                const legacyFallback = ['cinnamon_roll_3d_shaggy_dough_bowl', 'cinnamon_roll_3d_windowpane_dough', 'cinnamon_roll_3d_butter_dough_bowl', 'cinnamon_roll_3d_smooth_dough_bowl', 'cinnamon_roll_3d_first_rise_dough'][data.phase];\n                const mainAsset = assets[imageByPhase[data.phase]] ? imageByPhase[data.phase] : (assets[legacyFallback] ? legacyFallback : null);\n\n                // Full-bleed baker-eye POV: the hands, bowl, dough, counter and bakery belong to one scene,\n                // never a small picture floating over the shop background.\n                if (mainAsset && assets[mainAsset] && imageByPhase[data.phase] === mainAsset) {\n                    const zoom = data.dragging ? 1.018 + Math.sin(Date.now() / 95) * .003 : 1.006;\n                    const drawW = canvas.width * zoom, drawH = canvas.height * zoom;\n                    drawPhotoCover(mainAsset, (canvas.width - drawW) / 2, (canvas.height - drawH) / 2, drawW, drawH, .5, .53);\n                } else {\n                    drawBackground();\n                    if (mainAsset) drawPhotoContain(mainAsset, 42, 365, canvas.width - 84, 535);\n                    else drawImageFit('mixing_bowl_large', 80, 390, canvas.width - 160, 460);\n                }\n\n                // Keep the instructions directly over the live POV scene, with no panel or inset-photo frame.\n                const topShade = ctx.createLinearGradient(0, 205, 0, 390);\n                topShade.addColorStop(0, 'rgba(22,14,10,.58)');\n                topShade.addColorStop(1, 'rgba(22,14,10,0)');\n                ctx.fillStyle = topShade; ctx.fillRect(0, 200, canvas.width, 200);\n                const bottomShade = ctx.createLinearGradient(0, 910, 0, 1160);\n                bottomShade.addColorStop(0, 'rgba(22,14,10,0)');\n                bottomShade.addColorStop(1, 'rgba(22,14,10,.74)');\n                ctx.fillStyle = bottomShade; ctx.fillRect(0, 910, canvas.width, 250);\n\n                ctx.save(); ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(12,7,4,.96)'; ctx.shadowBlur = 10;\n                ctx.fillStyle = '#fff0d2'; ctx.font = '700 27px \"Noto Sans TC\", sans-serif';\n                ctx.fillText(phaseTitles[data.phase], centerX, 286, canvas.width - 58);\n                ctx.font = '500 19px \"Noto Sans TC\", sans-serif';\n                ctx.fillText(instructions[data.phase], centerX, 322, canvas.width - 58);\n                const progress = data.phase === 4 ? data.riseProgress / 100 : ([0,1,3].includes(data.phase) ? data.progress / 100 : 1);\n                const meterX = 54, meterY = 1005, meterW = canvas.width - 108, meterH = 24;\n                ctx.fillStyle = 'rgba(31,20,15,.82)'; ctx.beginPath(); ctx.roundRect(meterX, meterY + 4, meterW, meterH, 12); ctx.fill();\n                const grad = ctx.createLinearGradient(meterX, meterY, meterX + meterW, meterY); grad.addColorStop(0, '#9a542e'); grad.addColorStop(.55, '#d39a58'); grad.addColorStop(1, '#f0d08a');\n                ctx.fillStyle = grad; ctx.beginPath(); ctx.roundRect(meterX, meterY, Math.max(10, meterW * progress), meterH, 12); ctx.fill();\n                ctx.fillStyle = '#fff2d5'; ctx.font = '600 21px \"Noto Sans TC\", sans-serif';\n                ctx.fillText(data.phase === 4 ? `\u9996\u6b21\u767c\u9175 \u00b7 ${Math.floor(data.riseProgress)}%` : (data.phase === 2 ? '\u5c55\u5ef6\u5b8c\u6210 \u00b7 \u7b49\u5f85\u52a0\u5165\u725b\u6cb9' : `\u63c9\u9eb5\u9032\u5ea6 \u00b7 ${Math.floor(data.progress)}%`), centerX, meterY + 58);\n                ctx.restore();\n            }\n            function initStep3() {\n                currentStepData = {\n                    phase: 0,\n                    distance: 0,\n                    dragging: false,\n                    lastX: 0,\n                    lastY: 0,\n                    cursorX: canvas.width / 2,\n                    cursorY: canvas.height / 2 + 30,\n                    phaseChangedAt: Date.now(),\n                    previousPhase: undefined,\n                    lastCutAt: 0,\n                    cutRolls: new Array(6).fill(false)\n                };\n                stepContent.innerHTML = '';\n                actionButton.classList.add('hidden');\n                actionButton.disabled = true;\n                actionButton.onclick = null;\n\n                // Use the complete visible board/photo as the interaction area; perspective artwork varies by asset.\n                const bounds = () => ({ x: 12, y: canvas.height * .32, w: canvas.width - 24, h: canvas.height * .52 });\n                const inside = (pos, b) => pos.x >= b.x && pos.x <= b.x + b.w && pos.y >= b.y && pos.y <= b.y + b.h;\n                const finish = () => {\n                    currentStepData.previousPhase = currentStepData.phase;\n                    currentStepData.phase = 4;\n                    currentStepData.phaseChangedAt = Date.now();\n                    currentStepData.dragging = false;\n                    stepScores[2] = 10;\n                    actionButton.classList.remove('hidden');\n                    actionButton.textContent = 'Next \u00b7 \u4e8c\u6b21\u767c\u9175\u8207\u70d8\u70e4';\n                    actionButton.disabled = false;\n                    actionButton.onclick = () => { playSound('step_complete_sound'); nextStep(); };\n                    playSound('step_complete_sound');\n                };\n                const handlePointerDown = e => {\n                    e.preventDefault();\n                    const pos = getInputPosition(e), b = bounds();\n                    currentStepData.cursorX = pos.x;\n                    currentStepData.cursorY = pos.y;\n                    if (!inside(pos, b)) return;\n                    if (currentStepData.phase === 3) {\n                        // The photo's baked-in perspective does not line up reliably with canvas coordinates.\n                        // Use a generous, clearly bounded board hit area: every tap or short knife stroke there\n                        // makes the next cut, instead of requiring pixel-perfect taps on six invisible slices.\n                        const cutZone = { x: 18, y: canvas.height / 2 - 215, w: canvas.width - 36, h: 495 };\n                        if (!inside(pos, cutZone)) return;\n                        const nextCut = currentStepData.cutRolls.findIndex(cut => !cut);\n                        if (nextCut >= 0) {\n                            currentStepData.cutRolls[nextCut] = true;\n                            currentStepData.lastCutAt = Date.now();\n                            currentStepData.lastCutIndex = nextCut;\n                            currentStepData.cursorX = pos.x;\n                            currentStepData.cursorY = pos.y;\n                            playSound('knife_cutting_sound', .65);\n                            if (currentStepData.cutRolls.every(Boolean)) finish();\n                        }\n                        return;\n                    }\n                    if (currentStepData.phase < 3) {\n                        currentStepData.dragging = true;\n                        if (canvas.setPointerCapture && e.pointerId !== undefined) {\n                            try { canvas.setPointerCapture(e.pointerId); } catch (_) {}\n                        }\n                        currentStepData.lastX = pos.x;\n                        currentStepData.lastY = pos.y;\n                        playSound('select_sound', .35);\n                    }\n                };\n                const handlePointerMove = e => {\n                    const pos = getInputPosition(e);\n                    currentStepData.cursorX = pos.x;\n                    currentStepData.cursorY = pos.y;\n                    if (!currentStepData.dragging || currentStepData.phase > 2) return;\n                    e.preventDefault();\n                    const b = bounds();\n                    if (!inside(pos, b)) return;\n                    const distance = Math.hypot(pos.x - currentStepData.lastX, pos.y - currentStepData.lastY);\n                    if (distance > 2) {\n                        currentStepData.distance += distance;\n                        currentStepData.lastX = pos.x;\n                        currentStepData.lastY = pos.y;\n                    }\n                    const thresholds = [620, 690, 620];\n                    if (currentStepData.distance >= thresholds[currentStepData.phase]) {\n                        currentStepData.previousPhase = currentStepData.phase;\n                        currentStepData.phase++;\n                        currentStepData.phaseChangedAt = Date.now();\n                        currentStepData.distance = 0;\n                        currentStepData.dragging = false;\n                        playSound('step_complete_sound', .55);\n                    }\n                };\n                const handlePointerUp = () => { currentStepData.dragging = false; };\n                canvas.addEventListener('pointerdown', handlePointerDown);\n                canvas.addEventListener('pointermove', handlePointerMove);\n                canvas.addEventListener('pointerup', handlePointerUp);\n                canvas.addEventListener('pointercancel', handlePointerUp);\n            }\n\n            function renderStep3Legacy() {\n                drawBackground();\n                const centerX = canvas.width / 2;\n                const data = currentStepData;\n                const phase = data.phase;\n                const board = { x: 42, y: 430, w: canvas.width - 84, h: 445 };\n                const dough = { x: centerX - 218, y: board.y + 96, w: 436, h: 255 };\n                const thresholds = [620, 690, 620];\n                const movementProgress = phase < 3 ? Math.min(1, data.distance / thresholds[phase]) : 1;\n                const cutCount = data.cutRolls ? data.cutRolls.filter(Boolean).length : 0;\n                const titles = [\n                    '1 \u00b7 \u64c0\u958b\u767c\u9175\u9eb5\u7cf0',\n                    '2 \u00b7 \u5857\u725b\u6cb9\uff0c\u92ea\u6eff\u8089\u6842\u9ed1\u7cd6\u8207\u6838\u6843',\n                    '3 \u00b7 \u5c07\u9921\u6599\u9eb5\u7247\u7dca\u5be6\u6372\u8d77',\n                    '4 \u00b7 \u5207\u6210\u516d\u500b\u7d04 7 cm \u751f\u9eb5\u7cf0',\n                    '\u672a\u70d8\u7117\u9eb5\u7cf0 \u00b7 \u6e96\u5099\u4e8c\u6b21\u767c\u9175\u8207\u5165\u7210'\n                ];\n                const directions = [\n                    '\u5de6\u53f3\u4f86\u56de\u62d6\u52d5\u64c0\u9eb5\u68cd\uff0c\u628a\u9eb5\u7cf0\u64c0\u6210\u5747\u52fb\u9577\u65b9\u5f62\u3002',\n                    '\u62d6\u52d5\u62b9\u5200\uff0c\u5c07\u878d\u5316\u725b\u6cb9\u3001\u8089\u6842\u9ed1\u7cd6\u8207\u6838\u6843\u5747\u52fb\u92ea\u958b\u3002',\n                    '\u5de6\u53f3\u62d6\u52d5\u64c0\u9eb5\u68cd\uff0c\u5c07\u9921\u6599\u7dca\u5be6\u6372\u9032\u9eb5\u7cf0\u3002',\n                    '\u5728\u9eb5\u7cf0\u4e0a\u9ede\u6309\u516d\u6b21\u5207\u4ef6\uff1b\u6bcf\u4e00\u4e0b\u90fd\u6703\u7559\u4e0b\u6e05\u695a\u5200\u75d5\u3002',\n                    '\u516d\u500b\u8089\u6842\u6372\u4ecd\u662f\u751f\u9eb5\u7cf0\uff0c\u4e0b\u4e00\u6b65\u624d\u9032\u884c\u4e8c\u6b21\u767c\u9175\u8207\u70d8\u70e4\u3002'\n                ];\n\n                // This step is now drawn as an active work surface, not a static composite scene image.\n                // The board, dough, filling, tools and cuts update with the player's real drag/tap progress.\n                ctx.save();\n                ctx.shadowColor = 'rgba(17,10,6,.48)'; ctx.shadowBlur = 24; ctx.shadowOffsetY = 17;\n                const boardGrad = ctx.createLinearGradient(board.x, board.y, board.x + board.w, board.y + board.h);\n                boardGrad.addColorStop(0, '#70472d'); boardGrad.addColorStop(.28, '#b47b4b');\n                boardGrad.addColorStop(.58, '#8d5936'); boardGrad.addColorStop(1, '#563722');\n                ctx.fillStyle = boardGrad; ctx.beginPath(); ctx.roundRect(board.x, board.y, board.w, board.h, 28); ctx.fill();\n                ctx.restore();\n                ctx.save(); ctx.beginPath(); ctx.roundRect(board.x + 12, board.y + 12, board.w - 24, board.h - 24, 22); ctx.clip();\n                for (let i = 0; i < 15; i++) {\n                    const gy = board.y + 26 + i * 28;\n                    ctx.strokeStyle = i % 3 === 0 ? 'rgba(245,205,151,.15)' : 'rgba(48,27,15,.12)';\n                    ctx.lineWidth = i % 3 === 0 ? 3 : 1.5;\n                    ctx.beginPath(); ctx.moveTo(board.x + 8, gy);\n                    ctx.bezierCurveTo(board.x + board.w * .25, gy - 8, board.x + board.w * .7, gy + 9, board.x + board.w - 8, gy - 2); ctx.stroke();\n                }\n                ctx.restore();\n                ctx.save(); ctx.strokeStyle = 'rgba(248,220,176,.58)'; ctx.lineWidth = 3;\n                ctx.beginPath(); ctx.roundRect(board.x + 9, board.y + 9, board.w - 18, board.h - 18, 22); ctx.stroke(); ctx.restore();\n\n                function drawRawSheet(fillAmount = 0) {\n                    const flatAmount = phase === 0 ? .46 + .54 * movementProgress : 1;\n                    const sheetW = dough.w * flatAmount, sheetH = dough.h * (.60 + .40 * flatAmount);\n                    const sx = centerX - sheetW / 2, sy = dough.y + (dough.h - sheetH) / 2;\n                    ctx.save(); ctx.shadowColor = 'rgba(42,24,13,.34)'; ctx.shadowBlur = 16; ctx.shadowOffsetY = 9;\n                    const doughGrad = ctx.createLinearGradient(sx, sy, sx, sy + sheetH);\n                    doughGrad.addColorStop(0, '#f7e7c9'); doughGrad.addColorStop(.36, '#f0d9b4'); doughGrad.addColorStop(.78, '#e4c398'); doughGrad.addColorStop(1, '#c99e70');\n                    ctx.fillStyle = doughGrad; ctx.beginPath(); ctx.roundRect(sx, sy, sheetW, sheetH, 29); ctx.fill(); ctx.restore();\n                    ctx.save(); ctx.strokeStyle = 'rgba(255,249,232,.72)'; ctx.lineWidth = 4;\n                    ctx.beginPath(); ctx.roundRect(sx + 5, sy + 5, sheetW - 10, sheetH - 10, 25); ctx.stroke(); ctx.restore();\n\n                    const reveal = Math.max(0, Math.min(1, fillAmount));\n                    if (reveal > 0) {\n                        const coveredW = sheetW * reveal;\n                        ctx.save(); ctx.beginPath(); ctx.roundRect(sx + 12, sy + 12, coveredW - Math.min(12, coveredW), sheetH - 24, 19); ctx.clip();\n                        const butterGrad = ctx.createLinearGradient(sx, sy, sx, sy + sheetH);\n                        butterGrad.addColorStop(0, 'rgba(255,242,201,.95)'); butterGrad.addColorStop(.5, 'rgba(238,204,133,.94)'); butterGrad.addColorStop(1, 'rgba(201,151,82,.92)');\n                        ctx.globalAlpha = Math.min(1, reveal * 2.7); ctx.fillStyle = butterGrad; ctx.fillRect(sx, sy, coveredW, sheetH);\n                        // Brown sugar and cinnamon build up gradually over the butter rather than appearing as a pasted photo.\n                        const sugarAmount = Math.max(0, Math.min(1, (reveal - .12) / .72));\n                        ctx.globalAlpha = .8 * sugarAmount;\n                        for (let i = 0; i < 210; i++) {\n                            const rx = sx + 18 + ((i * 73) % Math.max(25, sheetW - 36));\n                            const ry = sy + 18 + ((i * 47) % Math.max(25, sheetH - 36));\n                            if (rx > sx + coveredW - 8) continue;\n                            const r = 1.4 + (i % 4) * .45;\n                            ctx.fillStyle = i % 4 === 0 ? '#6f351c' : (i % 3 === 0 ? '#9a572b' : '#bd8040');\n                            ctx.beginPath(); ctx.ellipse(rx, ry, r * 1.4, r, (i % 5) * .2, 0, Math.PI * 2); ctx.fill();\n                        }\n                        const walnutAmount = Math.max(0, Math.min(1, (reveal - .48) / .48));\n                        ctx.globalAlpha = walnutAmount;\n                        for (let i = 0; i < 30; i++) {\n                            const wx = sx + 26 + ((i * 127) % Math.max(30, sheetW - 52));\n                            const wy = sy + 24 + ((i * 83) % Math.max(30, sheetH - 48));\n                            if (wx > sx + coveredW - 12) continue;\n                            const nut = ctx.createRadialGradient(wx - 3, wy - 4, 1, wx, wy, 9);\n                            nut.addColorStop(0, '#d7a56b'); nut.addColorStop(.55, '#95603a'); nut.addColorStop(1, '#59351f');\n                            ctx.fillStyle = nut; ctx.beginPath(); ctx.ellipse(wx, wy, 7 + (i % 3), 4.5 + (i % 2), (i % 7) * .35, 0, Math.PI * 2); ctx.fill();\n                            ctx.strokeStyle = 'rgba(245,202,145,.48)'; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.moveTo(wx - 3, wy); ctx.lineTo(wx + 3, wy + 1); ctx.stroke();\n                        }\n                        ctx.restore();\n                    }\n                    return { x: sx, y: sy, w: sheetW, h: sheetH };\n                }\n\n                function drawRawLog(cx, cy, length, height) {\n                    const x = cx - length / 2, y = cy - height / 2;\n                    ctx.save(); ctx.fillStyle = 'rgba(30,16,8,.28)'; ctx.beginPath(); ctx.ellipse(cx, cy + height * .45, length * .53, height * .34, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore();\n                    ctx.save(); ctx.shadowColor = 'rgba(43,24,12,.32)'; ctx.shadowBlur = 12; ctx.shadowOffsetY = 7;\n                    const logGrad = ctx.createLinearGradient(x, y, x, y + height);\n                    logGrad.addColorStop(0, '#f6e2bf'); logGrad.addColorStop(.35, '#e9c99d'); logGrad.addColorStop(.72, '#d7b083'); logGrad.addColorStop(1, '#b88a60');\n                    ctx.fillStyle = logGrad; ctx.beginPath(); ctx.roundRect(x, y, length, height, height * .44); ctx.fill(); ctx.restore();\n                    ctx.save(); ctx.strokeStyle = 'rgba(255,248,229,.62)'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x + 20, y + 10); ctx.quadraticCurveTo(cx, y - 2, x + length - 20, y + 10); ctx.stroke();\n                    ctx.fillStyle = '#a25d32'; ctx.beginPath(); ctx.ellipse(x + 5, cy, height * .43, height * .43, 0, 0, Math.PI * 2); ctx.fill();\n                    ctx.strokeStyle = '#6e351e'; ctx.lineWidth = Math.max(2, height * .045); ctx.beginPath();\n                    for (let r = 1; r <= 3; r++) ctx.ellipse(x + 5, cy, height * (.40 - r * .09), height * (.40 - r * .09), 0, .35, Math.PI * 1.92); ctx.stroke();\n                    ctx.fillStyle = '#e2bd89'; ctx.beginPath(); ctx.arc(x + 5, cy, height * .075, 0, Math.PI * 2); ctx.fill(); ctx.restore();\n                }\n\n                function drawRawRollTop(x, y, rx, ry, seed = 0) {\n                    ctx.save(); ctx.shadowColor = 'rgba(35,18,9,.34)'; ctx.shadowBlur = 10; ctx.shadowOffsetY = 5;\n                    const outside = ctx.createRadialGradient(x - rx * .25, y - ry * .35, 2, x, y, rx * 1.2);\n                    outside.addColorStop(0, '#fff0d5'); outside.addColorStop(.58, '#e8c99d'); outside.addColorStop(1, '#bd9165');\n                    ctx.fillStyle = outside; ctx.beginPath(); ctx.ellipse(x, y, rx, ry, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore();\n                    ctx.save(); ctx.strokeStyle = '#884621'; ctx.lineWidth = Math.max(3, rx * .075); ctx.lineCap = 'round'; ctx.beginPath();\n                    ctx.ellipse(x, y, rx * .72, ry * .7, 0, .2, Math.PI * 1.8); ctx.ellipse(x, y, rx * .45, ry * .43, 0, .5, Math.PI * 1.92); ctx.ellipse(x, y, rx * .18, ry * .17, 0, 0, Math.PI * 1.8); ctx.stroke();\n                    for (let j = 0; j < 3; j++) { const wx = x + Math.sin(seed * 3 + j * 4.1) * rx * .48, wy = y + Math.cos(seed * 5 + j * 2.7) * ry * .43; ctx.fillStyle = '#87512f'; ctx.beginPath(); ctx.ellipse(wx, wy, rx * .10, ry * .12, .4, 0, Math.PI * 2); ctx.fill(); }\n                    ctx.restore();\n                }\n\n                if (phase === 4) {\n                    // The completed stage is still uncooked dough, arranged in a real caramel-lined baking pan.\n                    const pan = { x: centerX - 223, y: board.y + 78, w: 446, h: 270 };\n                    ctx.save(); ctx.shadowColor = 'rgba(20,11,6,.48)'; ctx.shadowBlur = 18; ctx.shadowOffsetY = 10;\n                    const metal = ctx.createLinearGradient(pan.x, pan.y, pan.x, pan.y + pan.h);\n                    metal.addColorStop(0, '#55514a'); metal.addColorStop(.18, '#c4bba9'); metal.addColorStop(.5, '#4b4741'); metal.addColorStop(1, '#24221f');\n                    ctx.fillStyle = metal; ctx.beginPath(); ctx.roundRect(pan.x, pan.y, pan.w, pan.h, 23); ctx.fill(); ctx.restore();\n                    ctx.save(); ctx.beginPath(); ctx.roundRect(pan.x + 16, pan.y + 16, pan.w - 32, pan.h - 32, 15); ctx.clip();\n                    const syrup = ctx.createLinearGradient(pan.x, pan.y, pan.x + pan.w, pan.y + pan.h); syrup.addColorStop(0, '#8a451e'); syrup.addColorStop(.5, '#c27a35'); syrup.addColorStop(1, '#6c3519');\n                    ctx.fillStyle = syrup; ctx.fillRect(pan.x + 16, pan.y + 16, pan.w - 32, pan.h - 32);\n                    for (let i = 0; i < 6; i++) { const col = i % 3, row = Math.floor(i / 3); drawRawRollTop(pan.x + 87 + col * 135, pan.y + 88 + row * 105, 54, 40, i + 2); }\n                    ctx.restore();\n                    ctx.save(); ctx.strokeStyle = 'rgba(255,239,201,.72)'; ctx.lineWidth = 3; ctx.beginPath(); ctx.roundRect(pan.x + 3, pan.y + 3, pan.w - 6, pan.h - 6, 22); ctx.stroke(); ctx.restore();\n                } else {\n                    const filling = phase >= 2 ? 1 : (phase === 1 ? movementProgress : 0);\n                    const sheet = drawRawSheet(filling);\n                    if (phase === 0) {\n                        const px = data.dragging ? data.cursorX : centerX;\n                        const py = data.dragging ? data.cursorY : sheet.y - 8;\n                        draw3DRollingPin(px, py, data.dragging ? .04 : -.10, data.dragging);\n                    } else if (phase === 1) {\n                        // A real spreading tool follows the player's finger; the filling appears only where work is done.\n                        const sx = data.dragging ? data.cursorX : sheet.x + sheet.w * Math.max(.15, movementProgress);\n                        const sy = data.dragging ? data.cursorY : sheet.y + sheet.h * .52;\n                        if (assets.offset_spatula) draw3DOffsetSpatula(sx, sy, -.32);\n                        else {\n                            ctx.save(); ctx.translate(sx, sy); ctx.rotate(-.35); ctx.shadowColor = 'rgba(20,12,7,.35)'; ctx.shadowBlur = 9; ctx.shadowOffsetY = 5;\n                            const blade = ctx.createLinearGradient(-55, 0, 55, 0); blade.addColorStop(0, '#776c5b'); blade.addColorStop(.45, '#fff2cf'); blade.addColorStop(1, '#8b8172');\n                            ctx.fillStyle = blade; ctx.beginPath(); ctx.roundRect(-62, -8, 112, 16, 6); ctx.fill();\n                            const handle = ctx.createLinearGradient(0, 0, 0, 70); handle.addColorStop(0, '#c8965f'); handle.addColorStop(1, '#603b25'); ctx.fillStyle = handle; ctx.beginPath(); ctx.roundRect(38, -7, 18, 80, 8); ctx.fill(); ctx.restore();\n                        }\n                    } else if (phase === 2) {\n                        const rollProgress = movementProgress;\n                        const logLength = 84 + (sheet.w - 84) * rollProgress;\n                        const logCenterX = sheet.x + logLength / 2;\n                        drawRawLog(logCenterX, sheet.y + sheet.h * .52, logLength, 86);\n                        const px = data.dragging ? data.cursorX : sheet.x + sheet.w * rollProgress;\n                        const py = data.dragging ? data.cursorY : sheet.y + sheet.h * .45;\n                        draw3DRollingPin(px, py, data.dragging ? .02 : -.10, data.dragging);\n                    } else if (phase === 3) {\n                        const logX = centerX - 210, logY = sheet.y + sheet.h * .53, logW = 420, logH = 92;\n                        drawRawLog(centerX, logY, logW, logH);\n                        // Each tap adds a visible real cut mark across the raw dough log.\n                        for (let i = 1; i <= Math.min(cutCount, 5); i++) {\n                            const cutX = logX + (logW / 6) * i;\n                            ctx.save(); ctx.strokeStyle = 'rgba(91,47,25,.92)'; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(cutX, logY - logH * .40); ctx.lineTo(cutX, logY + logH * .40); ctx.stroke();\n                            ctx.strokeStyle = 'rgba(255,239,211,.62)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cutX + 3, logY - logH * .34); ctx.lineTo(cutX + 3, logY + logH * .32); ctx.stroke(); ctx.restore();\n                        }\n                        // A metal blade follows the last tap so the player sees the cut happen at the work surface.\n                        const knifeX = data.lastCutAt ? data.cursorX : centerX;\n                        const knifeY = data.lastCutAt ? data.cursorY - 34 : logY - 90;\n                        ctx.save(); ctx.translate(knifeX, knifeY); ctx.rotate(-.62 + (data.lastCutAt && Date.now() - data.lastCutAt < 190 ? .16 : 0));\n                        ctx.shadowColor = 'rgba(20,12,7,.38)'; ctx.shadowBlur = 9; ctx.shadowOffsetY = 5;\n                        const steel = ctx.createLinearGradient(-8, 0, 8, 0); steel.addColorStop(0, '#777d7a'); steel.addColorStop(.45, '#fff9e8'); steel.addColorStop(1, '#7f8988');\n                        ctx.fillStyle = steel; ctx.beginPath(); ctx.moveTo(-5, -84); ctx.lineTo(6, -84); ctx.lineTo(7, 33); ctx.lineTo(0, 52); ctx.lineTo(-7, 33); ctx.closePath(); ctx.fill();\n                        const handle = ctx.createLinearGradient(-12, 0, 12, 0); handle.addColorStop(0, '#38241a'); handle.addColorStop(.5, '#a06a3f'); handle.addColorStop(1, '#422719'); ctx.fillStyle = handle; ctx.beginPath(); ctx.roundRect(-11, 42, 22, 92, 9); ctx.fill(); ctx.restore();\n                    }\n                }\n\n                ctx.save(); ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(18,11,7,.95)'; ctx.shadowBlur = 10;\n                ctx.fillStyle = '#fff2d5'; ctx.font = '700 27px \"Noto Sans TC\", sans-serif';\n                ctx.fillText(titles[phase], centerX, 340, canvas.width - 62);\n                ctx.font = '500 19px \"Noto Sans TC\", sans-serif'; ctx.fillText(directions[phase], centerX, 378, canvas.width - 62);\n                if (phase === 3) {\n                    ctx.fillStyle = 'rgba(42,27,19,.88)'; ctx.beginPath(); ctx.roundRect(centerX - 130, board.y + board.h + 14, 260, 42, 16); ctx.fill();\n                    ctx.fillStyle = '#fff1d2'; ctx.font = '600 20px \"Noto Sans TC\", sans-serif'; ctx.fillText(`\u5207\u4ef6\u9032\u5ea6 \u00b7 ${cutCount} / 6`, centerX, board.y + board.h + 42);\n                }\n                const progress = phase < 3 ? movementProgress : (phase === 3 ? cutCount / 6 : 1);\n                const meterY = phase === 3 ? board.y + board.h + 76 : board.y + board.h + 42;\n                const meterW = canvas.width - 150, meterX = (canvas.width - meterW) / 2;\n                ctx.fillStyle = 'rgba(31,20,15,.9)'; ctx.beginPath(); ctx.roundRect(meterX, meterY + 5, meterW, 22, 11); ctx.fill();\n                const meter = ctx.createLinearGradient(meterX, meterY, meterX + meterW, meterY); meter.addColorStop(0, '#9a542e'); meter.addColorStop(.55, '#d39a58'); meter.addColorStop(1, '#f0d08a');\n                ctx.fillStyle = meter; ctx.beginPath(); ctx.roundRect(meterX, meterY, Math.max(10, meterW * progress), 22, 11); ctx.fill();\n                if (phase === 4) {\n                    ctx.fillStyle = '#fff0cf'; ctx.font = '600 19px \"Noto Sans TC\", sans-serif';\n                    ctx.fillText('\u516d\u500b\u67d4\u8edf\u751f\u9eb5\u7cf0\u5df2\u653e\u5165\u7126\u7cd6\u8207\u6838\u6843\u4e2d', centerX, meterY + 52, canvas.width - 60);\n                }\n                ctx.restore();\n            }\n\n            // Step 3 photorealistic presentation: use the real 3D dough, filling, log, and cut-roll assets\n            // instead of the previous hand-drawn cartoon-like board and pastry shapes.\n            function renderStep3() {\n                drawBackground();\n                const centerX = canvas.width / 2;\n                const data = currentStepData;\n                const phase = data.phase;\n                const cutCount = data.cutRolls ? data.cutRolls.filter(Boolean).length : 0;\n                const thresholds = [620, 690, 620];\n                const movementProgress = phase < 3 ? Math.min(1, data.distance / thresholds[phase]) : 1;\n                const stageAssets = [\n                    'cinnamon_roll_step3_pov_roll_dough',\n                    'cinnamon_roll_step3_pov_spread_filling',\n                    'cinnamon_roll_step3_pov_roll_log',\n                    'cinnamon_roll_step3_pov_cut_rolls',\n                    'cinnamon_roll_step3_pov_cut_rolls_ready'\n                ];\n                const sceneAsset = stageAssets[phase];\n                const fallbackAssets = [\n                    'cinnamon_roll_step3_roll_dough_action',\n                    'cinnamon_roll_step3_spread_filling_action',\n                    'cinnamon_roll_step3_roll_log_action',\n                    'cinnamon_roll_step3_cut_log_action',\n                    'cinnamon_roll_step3_pov_cut_rolls'\n                ];\n                const scene = { x: 0, y: 0, w: canvas.width, h: canvas.height };\n                const activeImage = assets[sceneAsset] ? sceneAsset : (assets[fallbackAssets[phase]] ? fallbackAssets[phase] : null);\n\n                // Full-screen realistic POV action scenes replace the static text-only/card-like presentation.\n                // Each completed gesture transitions into the next real baking action; touch motion gently moves the view.\n                let drawn = false;\n                const now = Date.now();\n                const motion = data.dragging ? 1 : 0;\n                const cutAge = data.lastCutAt ? Math.max(0, 1 - (now - data.lastCutAt) / 260) : 0;\n                const phaseAge = Math.max(0, Math.min(1, (now - (data.phaseChangedAt || now)) / 320));\n                const currentAlpha = data.previousPhase !== undefined && phaseAge < 1 ? phaseAge : 1;\n                const dragOffsetX = data.dragging ? Math.max(-14, Math.min(14, (data.cursorX - centerX) * .035)) : 0;\n                const dragOffsetY = data.dragging ? Math.max(-10, Math.min(10, (data.cursorY - canvas.height * .57) * .025)) : 0;\n                const impactX = cutAge ? Math.sin(now / 22) * 4 * cutAge : 0;\n                const drawActionScene = (assetId, alpha) => {\n                    if (!assetId || !assets[assetId] || alpha <= 0) return false;\n                    const workProgress = phase === 3 ? cutCount / 6 : movementProgress;\n                    const strokeTravel = (workProgress - .5) * (phase === 2 ? 92 : 64);\n                    const zoom = 1.018 + motion * .014 + workProgress * .018 + cutAge * .006;\n                    const drawW = canvas.width * zoom, drawH = canvas.height * zoom;\n                    const actionShift = phase === 1 ? strokeTravel : -strokeTravel;\n                    const cutFocus = phase === 3 ? .36 + (cutCount / 6) * .28 : .5;\n                    ctx.save(); ctx.globalAlpha = alpha;\n                    const ok = drawPhotoCover(assetId, (canvas.width - drawW) / 2 + dragOffsetX + impactX + actionShift,\n                        (canvas.height - drawH) / 2 + dragOffsetY, drawW, drawH, cutFocus, .52);\n                    ctx.restore(); return ok;\n                };\n                if (data.previousPhase !== undefined && phaseAge < 1) drawn = drawActionScene(stageAssets[data.previousPhase], 1 - phaseAge) || drawn;\n                drawn = drawActionScene(activeImage, currentAlpha) || drawn;\n                if (!drawn) {\n                    // Reliable bakery-photo fallback avoids the transparent checkerboard cutout assets.\n                    drawBackground();\n                    const fallback = phase >= 4 ? 'cinnamon_roll_3d_raw_dough_tray_realistic'\n                        : (phase === 3 ? 'cinnamon_roll_3d_filled_dough_log'\n                        : (phase === 2 ? 'cinnamon_roll_3d_filled_dough_log'\n                        : (phase === 1 ? 'cinnamon_roll_3d_filled_dough_sheet' : 'cinnamon_roll_3d_unfilled_rolled_sheet')));\n                    drawPhotoContain(fallback, 36, 390, canvas.width - 72, 540);\n                }\n\n                const titles = [\n                    '1 \u00b7 \u64c0\u958b\u767c\u9175\u9eb5\u7cf0',\n                    '2 \u00b7 \u5857\u725b\u6cb9\uff0c\u92ea\u6eff\u8089\u6842\u9ed1\u7cd6\u8207\u6838\u6843',\n                    '3 \u00b7 \u5c07\u9921\u6599\u9eb5\u7247\u7dca\u5be6\u6372\u8d77',\n                    '4 \u00b7 \u5207\u6210\u516d\u500b\u7d04 7 cm \u751f\u9eb5\u7cf0',\n                    '\u672a\u70d8\u7117\u9eb5\u7cf0 \u00b7 \u6e96\u5099\u4e8c\u6b21\u767c\u9175\u8207\u5165\u7210'\n                ];\n                const directions = [\n                    '\u5de6\u53f3\u4f86\u56de\u62d6\u52d5\u64c0\u9eb5\u68cd\uff0c\u628a\u9eb5\u7cf0\u64c0\u6210\u5747\u52fb\u9577\u65b9\u5f62\u3002',\n                    '\u62d6\u52d5\u7acb\u9ad4\u62b9\u5200\uff0c\u5c07\u725b\u6cb9\u3001\u8089\u6842\u9ed1\u7cd6\u8207\u6838\u6843\u5747\u52fb\u92ea\u958b\u3002',\n                    '\u5de6\u53f3\u62d6\u52d5\u64c0\u9eb5\u68cd\uff0c\u628a\u5df2\u92ea\u9921\u7684\u9eb5\u7247\u7dca\u5be6\u6372\u8d77\u3002',\n                    '\u5728\u5be6\u7269\u9eb5\u7cf0\u756b\u9762\u4e0a\u9ede\u6309\u516d\u6b21\u5b8c\u6210\u5207\u4ef6\u3002',\n                    '\u516d\u500b\u5207\u597d\u7684\u8089\u6842\u6372\u4ecd\u662f\u751f\u9eb5\u7cf0\uff0c\u4e0b\u4e00\u6b65\u624d\u6703\u5165\u7210\u3002'\n                ];\n\n                // Discreet legibility scrims; the 3D scene itself remains unobstructed and border-free.\n                const topShade = ctx.createLinearGradient(0, 220, 0, 390);\n                topShade.addColorStop(0, 'rgba(20,12,8,.66)');\n                topShade.addColorStop(1, 'rgba(20,12,8,0)');\n                ctx.fillStyle = topShade; ctx.fillRect(0, 210, canvas.width, 190);\n                ctx.save(); ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(12,7,4,.95)'; ctx.shadowBlur = 9;\n                ctx.fillStyle = '#fff0d2'; ctx.font = '700 27px \"Noto Sans TC\", sans-serif';\n                ctx.fillText(titles[phase], centerX, 330, canvas.width - 56);\n                ctx.font = '500 19px \"Noto Sans TC\", sans-serif';\n                ctx.fillText(directions[phase], centerX, 367, canvas.width - 56);\n\n                if (phase === 3) {\n                    ctx.fillStyle = '#fff1d2'; ctx.font = '600 21px \"Noto Sans TC\", sans-serif';\n                    ctx.fillText(`\u5207\u4ef6\u9032\u5ea6 \u00b7 ${cutCount} / 6`, centerX, 976);\n                }\n                const progress = phase < 3 ? movementProgress : (phase === 3 ? cutCount / 6 : 1);\n                const meterW = canvas.width - 150, meterX = (canvas.width - meterW) / 2;\n                const meterY = phase === 3 ? 1003 : 978;\n                ctx.fillStyle = 'rgba(31,20,15,.88)'; ctx.beginPath(); ctx.roundRect(meterX, meterY + 5, meterW, 22, 11); ctx.fill();\n                const meter = ctx.createLinearGradient(meterX, meterY, meterX + meterW, meterY);\n                meter.addColorStop(0, '#9a542e'); meter.addColorStop(.55, '#d39a58'); meter.addColorStop(1, '#f0d08a');\n                ctx.fillStyle = meter; ctx.beginPath(); ctx.roundRect(meterX, meterY, Math.max(10, meterW * progress), 22, 11); ctx.fill();\n                if (phase === 4) {\n                    ctx.fillStyle = '#fff0cf'; ctx.font = '600 19px \"Noto Sans TC\", sans-serif';\n                    ctx.fillText('\u516d\u500b\u67d4\u8edf\u751f\u9eb5\u7cf0\u5df2\u653e\u5165\u7126\u7cd6\u8207\u6838\u6843\u4e2d', centerX, 1060, canvas.width - 60);\n                }\n                ctx.restore();\n            }\n\n            // Step 3: hands-on rolling, filling, rolling-up, and cutting \u2014 no step-through button gameplay.\n            // This later declaration intentionally replaces the earlier Step 3 renderer and input handlers.\n            function initStep3Interactive() {\n                currentStepData = {\n                    phase: 0, dragging: false, startX: 0, startY: 0, lastX: 0, lastY: 0,\n                    cursorX: canvas.width / 2, cursorY: canvas.height * .64,\n                    rollPasses: 0, lastRollDirection: 0, rollTracks: [], rollDragDistance: 0, rollTotalDistance: 0,\n                    fillDistance: 0, fillStrokes: [], currentStroke: [], fillCoverage: new Array(8).fill(false),\n                    rollUpDistance: 0, cutPositions: [], cutStrokeLength: 0,\n                    phaseChangedAt: Date.now(), actionFeedback: '',\n                    pointerPath: [], activePointerId: null\n                };\n                stepContent.innerHTML = '';\n                actionButton.classList.add('hidden');\n                actionButton.disabled = true;\n                actionButton.onclick = null;\n\n                // Keep the touch target generous on phones: the POV photo's dough fills most of the lower work area.\n                const inWorkArea = p => p.x >= canvas.width * .05 && p.x <= canvas.width * .95 && p.y >= canvas.height * .40 && p.y <= canvas.height * .90;\n                const setPhase = next => {\n                    if (currentStepData.phase === 1 && currentStepData.currentStroke && currentStepData.currentStroke.length > 1) {\n                        currentStepData.fillStrokes.push(currentStepData.currentStroke.slice());\n                        currentStepData.currentStroke = [];\n                    }\n                    currentStepData.phase = next;\n                    currentStepData.phaseChangedAt = Date.now();\n                    currentStepData.dragging = false;\n                    currentStepData.pointerPath = [];\n                    currentStepData.actionFeedback = '';\n                    playSound('step_complete_sound', .65);\n                    lib.haptics.play('success');\n                    if (next === 4) {\n                        stepScores[2] = 10;\n                        actionButton.classList.remove('hidden');\n                        actionButton.textContent = 'Next \u00b7 \u4e8c\u6b21\u767c\u9175\u8207\u70d8\u70e4';\n                        actionButton.disabled = false;\n                        actionButton.onclick = () => { playSound('step_complete_sound'); nextStep(); };\n                    }\n                };\n                const beginDrag = e => {\n                    if (currentStepData.phase >= 4) return;\n                    e.preventDefault();\n                    const p = getInputPosition(e);\n                    currentStepData.cursorX = currentStepData.lastX = currentStepData.startX = p.x;\n                    currentStepData.cursorY = currentStepData.lastY = currentStepData.startY = p.y;\n                    if (!inWorkArea(p)) return;\n                    currentStepData.dragging = true;\n                    currentStepData.activePointerId = e.pointerId ?? null;\n                    currentStepData.pointerPath = [p];\n                    currentStepData.cutStrokeLength = 0;\n                    currentStepData.rollDragDistance = 0;\n                    currentStepData.currentStroke = currentStepData.phase === 1 ? [p] : [];\n                    if (canvas.setPointerCapture && e.pointerId !== undefined) {\n                        try { canvas.setPointerCapture(e.pointerId); } catch (_) {}\n                    }\n                    if (currentStepData.phase === 0 || currentStepData.phase === 2) playSound('select_sound', .3);\n                    if (currentStepData.phase === 1) playSound('spreading_sound', .22);\n                };\n                const moveDrag = e => {\n                    if (!currentStepData.dragging || (currentStepData.activePointerId !== null && e.pointerId !== currentStepData.activePointerId)) return;\n                    e.preventDefault();\n                    // Use coalesced touch samples where available, avoiding lost movement on fast mobile swipes.\n                    const samples = typeof e.getCoalescedEvents === 'function' ? e.getCoalescedEvents() : [];\n                    for (const sample of (samples && samples.length ? samples : [e])) {\n                        const p = getInputPosition(sample);\n                        currentStepData.cursorX = p.x;\n                        currentStepData.cursorY = p.y;\n                        const prev = { x: currentStepData.lastX, y: currentStepData.lastY };\n                        const dx = p.x - prev.x, dy = p.y - prev.y;\n                        const d = Math.hypot(dx, dy);\n                        if (d < .5) continue;\n                        currentStepData.pointerPath.push(p);\n                        if (currentStepData.pointerPath.length > 180) currentStepData.pointerPath.shift();\n                        const phase = currentStepData.phase;\n                        if (phase === 0) {\n                            // Once a thumb starts on the work surface, pointer capture keeps the roll responsive even if it drifts past the photo edge.\n                            currentStepData.rollDragDistance += Math.abs(dx);\n                            currentStepData.rollTotalDistance += Math.abs(dx);\n                            currentStepData.rollTracks.push({ y: p.y });\n                            if (currentStepData.rollTracks.length > 24) currentStepData.rollTracks.shift();\n                        } else if (phase === 1 && inWorkArea(prev) && inWorkArea(p)) {\n                            // A wide spatula covers a forgiving 4-by-2 area across the whole touchable dough surface.\n                            const samplesPerSegment = Math.max(1, Math.ceil(d / 8));\n                            for (let i = 0; i <= samplesPerSegment; i++) {\n                                const t = i / samplesPerSegment;\n                                const sx = prev.x + dx * t, sy = prev.y + dy * t;\n                                const left = canvas.width * .08, right = canvas.width * .92;\n                                const top = canvas.height * .43, bottom = canvas.height * .87;\n                                if (sx >= left && sx <= right && sy >= top && sy <= bottom) {\n                                    const col = Math.min(3, Math.floor((sx - left) / (right - left) * 4));\n                                    const row = Math.min(1, Math.floor((sy - top) / (bottom - top) * 2));\n                                    currentStepData.fillCoverage[row * 4 + col] = true;\n                                    if (col > 0 && sx - (left + col * (right - left) / 4) < canvas.width * .065) currentStepData.fillCoverage[row * 4 + col - 1] = true;\n                                    if (col < 3 && (left + (col + 1) * (right - left) / 4) - sx < canvas.width * .065) currentStepData.fillCoverage[row * 4 + col + 1] = true;\n                                    if (row > 0 && sy - (top + row * (bottom - top) / 2) < canvas.height * .055) currentStepData.fillCoverage[(row - 1) * 4 + col] = true;\n                                    if (row < 1 && (top + (row + 1) * (bottom - top) / 2) - sy < canvas.height * .055) currentStepData.fillCoverage[(row + 1) * 4 + col] = true;\n                                }\n                            }\n                            currentStepData.fillDistance = currentStepData.fillCoverage.filter(Boolean).length;\n                            currentStepData.currentStroke.push(p);\n                            if (currentStepData.currentStroke.length > 220) currentStepData.currentStroke.shift();\n                        } else if (phase === 2) {\n                            // Keep rolling responsive to upward, diagonal, or short repeated thumb swipes after pointer capture.\n                            currentStepData.rollUpDistance += Math.max(0, -dy) + Math.abs(dx) * .42;\n                        } else if (phase === 3) {\n                            currentStepData.cutStrokeLength += d;\n                        }\n                        currentStepData.lastX = p.x;\n                        currentStepData.lastY = p.y;\n                        if (phase === 0 && currentStepData.rollTotalDistance >= canvas.width * .62) setPhase(1);\n                        else if (phase === 1 && currentStepData.fillDistance >= 7) setPhase(2);\n                        else if (phase === 2 && currentStepData.rollUpDistance >= canvas.height * .28) setPhase(3);\n                        if (!currentStepData.dragging) break;\n                    }\n                };\n                const endDrag = e => {\n                    if (!currentStepData.dragging || (e && currentStepData.activePointerId !== null && e.pointerId !== currentStepData.activePointerId)) return;\n                    const p = e ? getInputPosition(e) : { x: currentStepData.lastX, y: currentStepData.lastY };\n                    const phase = currentStepData.phase;\n                    const dy = p.y - currentStepData.startY;\n                    currentStepData.dragging = false;\n                    currentStepData.activePointerId = null;\n                    if (phase === 1 && currentStepData.currentStroke.length > 1) {\n                        currentStepData.fillStrokes.push(currentStepData.currentStroke.slice());\n                        if (currentStepData.fillStrokes.length > 38) currentStepData.fillStrokes.shift();\n                        currentStepData.currentStroke = [];\n                    } else if (phase === 3 && currentStepData.cutStrokeLength >= canvas.height * .055 && Math.abs(dy) >= canvas.height * .045) {\n                        // Each deliberate knife-like swipe makes one real cut, in a new position along the log.\n                        const x = Math.max(canvas.width * .13, Math.min(canvas.width * .87, (currentStepData.startX + p.x) / 2));\n                        if (!currentStepData.cutPositions.some(v => Math.abs(v - x) < canvas.width * .045)) {\n                            currentStepData.cutPositions.push(x);\n                            currentStepData.actionFeedback = `Cut ${currentStepData.cutPositions.length} / 6`;\n                            playSound('knife_cutting_sound', .7);\n                            lib.haptics.play('tick');\n                            if (currentStepData.cutPositions.length >= 6) setPhase(4);\n                        } else {\n                            currentStepData.actionFeedback = 'Make the next cut in a fresh position along the log.';\n                        }\n                    }\n                };\n                const cancelDrag = e => {\n                    if (e && currentStepData.activePointerId !== null && e.pointerId !== currentStepData.activePointerId) return;\n                    currentStepData.dragging = false;\n                    currentStepData.activePointerId = null;\n                    currentStepData.currentStroke = [];\n                };\n                canvas.addEventListener('pointerdown', beginDrag);\n                canvas.addEventListener('pointermove', moveDrag);\n                canvas.addEventListener('pointerup', endDrag);\n                canvas.addEventListener('pointercancel', cancelDrag);\n            }\n\n            function renderStep3Interactive() {\n                const data = currentStepData;\n                const phase = data.phase;\n                const w = canvas.width, h = canvas.height, cx = w / 2;\n                const plainScene = 'cinnamon_roll_step3_pov_roll_dough';\n                const fillScene = 'cinnamon_roll_step3_pov_spread_filling';\n                const rollScene = 'cinnamon_roll_step3_pov_roll_log';\n                const cutScene = 'cinnamon_roll_step3_pov_cut_rolls';\n                const readyScene = 'cinnamon_roll_step3_pov_cut_rolls_ready';\n                const rollProgress = Math.min(1, data.rollTotalDistance / (w * .62));\n                const fillProgress = Math.min(1, data.fillDistance / 7);\n                const rollUpProgress = Math.min(1, data.rollUpDistance / (h * .28));\n                const cutProgress = Math.min(1, data.cutPositions.length / 6);\n\n                // Use full-bleed first-person bakery scenes; the player performs each operation directly on the work surface.\n                // The unfilled dough scene remains visible while rolling and spreading, so filling only appears where dragged.\n                if (phase <= 1) {\n                    drawPhotoCover(plainScene, 0, 0, w, h, .5, .52);\n                } else if (phase === 2) {\n                    drawPhotoCover(fillScene, 0, 0, w, h, .5, .52);\n                    if (assets[rollScene]) {\n                        ctx.save(); ctx.globalAlpha = rollUpProgress;\n                        drawPhotoCover(rollScene, 0, 0, w, h, .5, .52);\n                        ctx.restore();\n                    }\n                } else if (phase === 3) {\n                    drawPhotoCover(rollScene, 0, 0, w, h, .5, .52);\n                    if (assets[cutScene] && cutProgress > 0) {\n                        ctx.save(); ctx.globalAlpha = cutProgress;\n                        drawPhotoCover(cutScene, 0, 0, w, h, .5, .52);\n                        ctx.restore();\n                    }\n                } else {\n                    drawPhotoCover(readyScene, 0, 0, w, h, .5, .52);\n                }\n\n                // Rolling: strokes alternate direction and leave a subtle pressure/flour trail on the real dough surface.\n                if (phase === 0 || (phase === 1 && data.rollTracks.length)) {\n                    ctx.save();\n                    data.rollTracks.forEach((track, i) => {\n                        const y = track.y;\n                        ctx.globalAlpha = .11 + (i % 3) * .025;\n                        ctx.strokeStyle = '#fff8e8'; ctx.lineWidth = 23; ctx.lineCap = 'round';\n                        ctx.beginPath(); ctx.moveTo(w * .18, y); ctx.lineTo(w * .82, y); ctx.stroke();\n                        ctx.globalAlpha = .18; ctx.strokeStyle = 'rgba(120,82,48,.55)'; ctx.lineWidth = 2;\n                        ctx.beginPath(); ctx.moveTo(w * .18, y + 12); ctx.lineTo(w * .82, y + 12); ctx.stroke();\n                    });\n                    // During a live roll, show a soft pressure path rather than drawing a second rolling pin over the real one.\n                    if (phase === 0 && data.dragging) {\n                        const y = (data.startY + data.cursorY) / 2;\n                        const pressure = ctx.createLinearGradient(Math.min(data.startX, data.cursorX), y, Math.max(data.startX, data.cursorX), y);\n                        pressure.addColorStop(0, 'rgba(255,248,230,0)'); pressure.addColorStop(.5, 'rgba(255,248,230,.38)'); pressure.addColorStop(1, 'rgba(255,248,230,0)');\n                        ctx.globalAlpha = .75; ctx.strokeStyle = pressure; ctx.lineWidth = 34; ctx.lineCap = 'round';\n                        ctx.beginPath(); ctx.moveTo(data.startX, y); ctx.lineTo(data.cursorX, y); ctx.stroke();\n                    }\n                    ctx.restore();\n                }\n\n                // Filling: finger/spatula movement paints butter and cinnamon sugar onto the dough, with walnut pieces along the track.\n                if (phase === 1) {\n                    const doughClip = () => {\n                        ctx.beginPath();\n                        ctx.moveTo(w * .10, h * .56); ctx.lineTo(w * .90, h * .55);\n                        ctx.lineTo(w * .88, h * .79); ctx.lineTo(w * .13, h * .81); ctx.closePath(); ctx.clip();\n                    };\n                    ctx.save(); doughClip();\n                    const strokes = data.fillStrokes.concat(data.currentStroke && data.currentStroke.length > 1 ? [data.currentStroke] : []);\n                    strokes.forEach((points, strokeIndex) => {\n                        if (!points || points.length < 2) return;\n                        ctx.save(); ctx.lineCap = 'round'; ctx.lineJoin = 'round';\n                        // Soft butter sheen under the darker cinnamon-sugar layer, following the player's actual spatula path.\n                        ctx.shadowColor = 'rgba(92,48,20,.24)'; ctx.shadowBlur = 9;\n                        ctx.strokeStyle = 'rgba(231,180,93,.42)'; ctx.lineWidth = 42;\n                        ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y);\n                        for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);\n                        ctx.stroke();\n                        ctx.shadowBlur = 3; ctx.strokeStyle = 'rgba(112,57,29,.72)'; ctx.lineWidth = 25;\n                        ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y);\n                        for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);\n                        ctx.stroke();\n                        // Fine cinnamon granules and occasional walnut fragments follow the actual spatula path.\n                        for (let i = 0; i < points.length; i += 7) {\n                            const p = points[i];\n                            ctx.fillStyle = i % 2 ? '#63351f' : '#9a5c32';\n                            ctx.beginPath(); ctx.arc(p.x + Math.sin(i * 1.7) * 8, p.y + Math.cos(i * 1.3) * 8, 2 + (i % 3), 0, Math.PI * 2); ctx.fill();\n                            if ((i + strokeIndex) % 5 === 0) {\n                                const nut = ctx.createRadialGradient(p.x - 3, p.y - 4, 1, p.x, p.y, 9);\n                                nut.addColorStop(0, '#d6a56b'); nut.addColorStop(.65, '#8d5834'); nut.addColorStop(1, '#59341f');\n                                ctx.fillStyle = nut; ctx.beginPath(); ctx.ellipse(p.x + 5, p.y - 4, 7, 4.5, .45, 0, Math.PI * 2); ctx.fill();\n                            }\n                        }\n                        ctx.restore();\n                    });\n                    ctx.restore();\n                    if (data.dragging) draw3DOffsetSpatula(data.cursorX + 15, data.cursorY - 8, -.5);\n                }\n\n                // Show temporary cut marks only while cutting the log. The completed ready-pan POV already\n                // contains six finished raw rolls; carrying these screen-space marks forward makes them look\n                // like sticks embedded in the dough, so clear the overlay on the completed stage.\n                if (phase === 3) {\n                    data.cutPositions.forEach((x, i) => {\n                        ctx.save(); ctx.lineCap = 'round'; ctx.shadowColor = 'rgba(32,18,10,.55)'; ctx.shadowBlur = 5;\n                        ctx.strokeStyle = 'rgba(76,39,22,.88)'; ctx.lineWidth = 7;\n                        ctx.beginPath(); ctx.moveTo(x - 11, h * .55); ctx.lineTo(x + 9, h * .76); ctx.stroke();\n                        ctx.strokeStyle = 'rgba(255,236,203,.66)'; ctx.lineWidth = 2;\n                        ctx.beginPath(); ctx.moveTo(x - 8, h * .56); ctx.lineTo(x + 11, h * .75); ctx.stroke();\n                        ctx.restore();\n                        ctx.save(); ctx.fillStyle = '#fff0d2'; ctx.font = '700 17px \"Noto Sans TC\", sans-serif'; ctx.textAlign = 'center';\n                        ctx.fillText(String(i + 1), x, h * .52); ctx.restore();\n                    });\n                }\n\n                const titles = [\n                    '1 \u00b7 \u64c0\u958b\u767c\u9175\u9eb5\u7cf0',\n                    '2 \u00b7 \u5747\u52fb\u5857\u725b\u6cb9\u3001\u8089\u6842\u9ed1\u7cd6\u8207\u6838\u6843',\n                    '3 \u00b7 \u5c07\u9921\u6599\u9eb5\u7247\u7dca\u5be6\u6372\u8d77',\n                    '4 \u00b7 \u5207\u6210\u516d\u500b\u7d04 7 cm \u751f\u9eb5\u7cf0',\n                    '\u5df2\u5b8c\u6210 \u00b7 \u516d\u500b\u751f\u8089\u6842\u6372\u6e96\u5099\u5165\u7210'\n                ];\n                const directions = [\n                    `\u76f4\u63a5\u5de6\u53f3\u62d6\u52d5\u64c0\u9eb5\u68cd\uff0c\u9032\u5ea6\u6703\u8ddf\u624b\u6307\u9023\u7e8c\u79fb\u52d5 \u00b7 ${Math.floor(rollProgress * 100)}%`,\n                    '\u5728\u9eb5\u7247\u4e0a\u4f86\u56de\u62d6\u52d5\u62b9\u5200\uff0c\u89aa\u624b\u628a\u725b\u6cb9\u3001\u8089\u6842\u9ed1\u7cd6\u548c\u6838\u6843\u5747\u52fb\u62b9\u958b\u3002',\n                    '\u7531\u9eb5\u7247\u908a\u7de3\u5411\u524d\u62d6\u52d5\uff0c\u9010\u6b65\u628a\u9921\u6599\u9eb5\u7247\u6372\u6210\u7dca\u5be6\u9577\u5377\u3002',\n                    '\u6cbf\u9eb5\u7cf0\u9577\u5377\u5411\u4e0b\u5283\u516d\u5200\uff1b\u6bcf\u4e00\u5200\u90fd\u8981\u5be6\u969b\u62d6\u904e\u9eb5\u7cf0\u3002',\n                    '\u516d\u500b\u8089\u6842\u6372\u4fdd\u6301\u672a\u70d8\u7117\u751f\u9eb5\u7cf0\uff0c\u4e0b\u4e00\u6b65\u9032\u884c\u4e8c\u6b21\u767c\u9175\u8207\u70d8\u70e4\u3002'\n                ];\n                const progress = phase === 0 ? rollProgress : phase === 1 ? fillProgress : phase === 2 ? rollUpProgress : phase === 3 ? cutProgress : 1;\n                const scrim = ctx.createLinearGradient(0, 190, 0, 435);\n                scrim.addColorStop(0, 'rgba(20,12,8,.72)'); scrim.addColorStop(1, 'rgba(20,12,8,0)');\n                ctx.fillStyle = scrim; ctx.fillRect(0, 185, w, 255);\n                ctx.save(); ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(12,7,4,.98)'; ctx.shadowBlur = 10;\n                ctx.fillStyle = '#fff0d2'; ctx.font = '700 27px \"Noto Sans TC\", sans-serif'; ctx.fillText(titles[phase], cx, 284, w - 58);\n                ctx.font = '500 19px \"Noto Sans TC\", sans-serif'; ctx.fillText(directions[phase], cx, 325, w - 58);\n                if (data.actionFeedback && data.actionFeedback !== directions[phase]) {\n                    ctx.fillStyle = '#f4d69e'; ctx.font = '600 17px \"Noto Sans TC\", sans-serif'; ctx.fillText(data.actionFeedback, cx, 360, w - 56);\n                }\n                if (phase === 3) { ctx.fillStyle = '#fff1d2'; ctx.font = '700 22px \"Noto Sans TC\", sans-serif'; ctx.fillText(`\u5be6\u969b\u5207\u4ef6\u9032\u5ea6 \u00b7 ${data.cutPositions.length} / 6`, cx, h * .82); }\n                const meterW = w - 150, meterX = (w - meterW) / 2, meterY = h * .84;\n                ctx.fillStyle = 'rgba(31,20,15,.88)'; ctx.beginPath(); ctx.roundRect(meterX, meterY + 5, meterW, 22, 11); ctx.fill();\n                const meter = ctx.createLinearGradient(meterX, meterY, meterX + meterW, meterY); meter.addColorStop(0, '#9a542e'); meter.addColorStop(.55, '#d39a58'); meter.addColorStop(1, '#f0d08a');\n                ctx.fillStyle = meter; ctx.beginPath(); ctx.roundRect(meterX, meterY, Math.max(10, meterW * progress), 22, 11); ctx.fill();\n                if (phase === 4) { ctx.fillStyle = '#fff0cf'; ctx.font = '600 19px \"Noto Sans TC\", sans-serif'; ctx.fillText('\u516d\u500b\u67d4\u8edf\u751f\u9eb5\u7cf0\u5df2\u653e\u5165\u7126\u7cd6\u6838\u6843\u70e4\u76e4', cx, meterY + 48, w - 60); }\n                ctx.restore();\n            }\n\n            // Step 4: Baking\n            function initStep4() {\n                currentStepData = { baking: false, bakingProgress: 0, stage: 0 };\n                stepContent.innerHTML = '';\n                actionButton.textContent = 'Second Rise & Bake';\n                actionButton.disabled = false;\n                actionButton.onclick = () => {\n                    if (!currentStepData.baking) {\n                        currentStepData.baking = true;\n                        actionButton.disabled = true;\n                        playSound('oven_door_sound');\n                        const bakingInterval = setInterval(() => {\n                            currentStepData.bakingProgress += 100 / 5;\n                            if (currentStepData.bakingProgress >= 33 && currentStepData.stage === 0) currentStepData.stage = 1;\n                            else if (currentStepData.bakingProgress >= 66 && currentStepData.stage === 1) currentStepData.stage = 2;\n                            else if (currentStepData.bakingProgress >= 100) {\n                                clearInterval(bakingInterval);\n                                currentStepData.stage = 3;\n                                stepScores[3] = 10;\n                                playSound('oven_door_sound');\n                                actionButton.textContent = 'Next';\n                                actionButton.disabled = false;\n                                actionButton.onclick = () => { playSound('step_complete_sound'); nextStep(); };\n                            }\n                        }, 1000);\n                    }\n                };\n            }\n\n            function renderStep4() {\n                drawBackground();\n                const centerX = canvas.width / 2;\n                // Make the oven a prominent, freestanding 3D object on the bakery counter\u2014not a small inset/photo card.\n                const ovenBox = { x: 34, y: 245, w: canvas.width - 68, h: 700 };\n                const ovenAsset = currentStepData.baking && currentStepData.stage < 3 ? 'oven_closed' : 'oven_open';\n                const ovenDrawn = drawImageFit(ovenAsset, ovenBox.x, ovenBox.y, ovenBox.w, ovenBox.h);\n\n                if (!currentStepData.baking || currentStepData.stage >= 3) {\n                    // Show the real physical open oven with its tray at the rack, using separate transparent 3D props.\n                    const trayAsset = currentStepData.stage >= 3 ? 'cinnamon_roll_3d_baked_tray' : 'cinnamon_roll_3d_pan_raw_proof';\n                    if (assets[trayAsset]) {\n                        const trayW = Math.min(390, canvas.width * .56);\n                        drawImageFit(trayAsset, centerX - trayW / 2, 608, trayW, 205);\n                    } else if (!currentStepData.baking) {\n                        drawPanRolls(centerX, 700, 1, .72, false);\n                    }\n                } else {\n                    // Warm light through the closed oven window makes the baking state legible while retaining the real oven prop.\n                    const glow = ctx.createRadialGradient(centerX, 650, 14, centerX, 650, 225);\n                    const heat = currentStepData.stage >= 2 ? .23 : .14;\n                    glow.addColorStop(0, `rgba(255,160,62,${heat})`);\n                    glow.addColorStop(1, 'rgba(255,160,62,0)');\n                    ctx.fillStyle = glow;\n                    ctx.fillRect(centerX - 230, 480, 460, 340);\n                }\n\n                if (!ovenDrawn) {\n                    // Procedural fallback: a dimensional cream enamel oven, preserving the same centered physical-object layout.\n                    const x = 110, y = 305, w = canvas.width - 220, h = 570;\n                    ctx.save();\n                    ctx.shadowColor = 'rgba(15,9,5,.42)'; ctx.shadowBlur = 28; ctx.shadowOffsetY = 15;\n                    const enamel = ctx.createLinearGradient(x, y, x + w, y);\n                    enamel.addColorStop(0, '#9a8c78'); enamel.addColorStop(.18, '#fff4dd'); enamel.addColorStop(.55, '#eee0c7'); enamel.addColorStop(1, '#a99a83');\n                    ctx.fillStyle = enamel; ctx.beginPath(); ctx.roundRect(x, y, w, h, 30); ctx.fill();\n                    ctx.shadowColor = 'transparent';\n                    ctx.fillStyle = '#604a3c'; ctx.beginPath(); ctx.roundRect(x + 70, y + 190, w - 140, 310, 22); ctx.fill();\n                    ctx.fillStyle = currentStepData.baking ? '#41281d' : '#a56b39'; ctx.beginPath(); ctx.roundRect(x + 90, y + 210, w - 180, 270, 16); ctx.fill();\n                    ctx.fillStyle = '#c5a36c'; ctx.beginPath(); ctx.roundRect(x + 90, y + 115, w - 180, 22, 10); ctx.fill();\n                    for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.arc(x + 125 + i * ((w - 250) / 2), y + 70, 19, 0, Math.PI * 2); ctx.fillStyle = '#89704c'; ctx.fill(); }\n                    ctx.restore();\n                }\n\n                ctx.save(); ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(12,7,4,.92)'; ctx.shadowBlur = 9;\n                if (!currentStepData.baking) {\n                    ctx.fillStyle = '#fff1d2'; ctx.font = '700 25px \"Noto Sans TC\", sans-serif';\n                    ctx.fillText('\u6253\u958b\u5be6\u9ad4\u7117\u7210 \u00b7 \u653e\u5165\u5df2\u4e8c\u6b21\u767c\u9175\u7684\u751f\u9eb5\u7cf0', centerX, 975, canvas.width - 55);\n                    ctx.font = '500 19px \"Noto Sans TC\", sans-serif';\n                    ctx.fillText('\u8089\u6842\u6372\u4ecd\u662f\u672a\u70d8\u7117\u9eb5\u7cf0\uff1b\u6309\u4e0b\u65b9\u6309\u9215\u958b\u59cb\u70d8\u70e4\u3002', centerX, 1010, canvas.width - 55);\n                    ctx.restore();\n                    drawTutorialArrow(centerX, canvas.height - 130, 0);\n                    return;\n                }\n\n                const barX = 55, barY = canvas.height - 250, barW = canvas.width - 110;\n                ctx.fillStyle = 'rgba(31,20,15,.88)'; ctx.beginPath(); ctx.roundRect(barX, barY + 5, barW, 25, 13); ctx.fill();\n                const bakeGrad = ctx.createLinearGradient(barX, barY, barX + barW, barY); bakeGrad.addColorStop(0, '#9a542e'); bakeGrad.addColorStop(1, '#edc981');\n                ctx.fillStyle = bakeGrad; ctx.beginPath(); ctx.roundRect(barX, barY, barW * currentStepData.bakingProgress / 100, 25, 13); ctx.fill();\n                ctx.fillStyle = '#fff2d5'; ctx.font = '700 27px \"Noto Sans TC\", sans-serif';\n                ctx.fillText(currentStepData.stage >= 3 ? '\u70d8\u7119\u5b8c\u6210 \u00b7 \u7126\u7cd6\u6838\u6843\u8089\u6842\u6372\u51fa\u7210' : `\u5be6\u9ad4\u7117\u7210\u70d8\u7119\u4e2d \u00b7 ${Math.floor(currentStepData.bakingProgress / 100 * 30)} / 30 \u5206\u9418`, centerX, canvas.height - 190);\n                ctx.restore();\n                if (currentStepData.bakingProgress >= 100) drawTutorialArrow(centerX, canvas.height - 130, 0);\n            }\n\n            // Step 5: Cooling\n            function initStep5() {\n                currentStepData = { coolingProgress: 0, steamParticles: [] };\n                stepContent.innerHTML = '';\n                actionButton.classList.add('hidden');\n                const coolingInterval = setInterval(() => {\n                    currentStepData.coolingProgress += 100 / 5;\n                    if (currentStepData.coolingProgress >= 100) {\n                        clearInterval(coolingInterval);\n                        stepScores[4] = 10;\n                        actionButton.classList.remove('hidden');\n                        actionButton.textContent = 'Next';\n                        actionButton.disabled = false;\n                        actionButton.onclick = () => {\n                            playSound('step_complete_sound');\n                            nextStep();\n                        };\n                    }\n                }, 1000);\n            }\n            function renderStep5() {\n                drawBackground();\n                const soakAsset = assets.cinnamon_roll_3d_baked_tray ? 'cinnamon_roll_3d_baked_tray' : 'cinnamon_roll_3d_caramel_finish';\n                if (drawPhotoContain(soakAsset, 42, 300, canvas.width - 84, 650)) {\n                    ctx.save(); ctx.textAlign = 'center'; ctx.fillStyle = '#fff1d2'; ctx.font = '700 27px \"Noto Sans TC\", sans-serif'; ctx.shadowColor = 'rgba(15,9,6,.9)'; ctx.shadowBlur = 9;\n                    ctx.fillText('\u7126\u7cd6\u7cd6\u6db2\u6162\u6162\u6ef2\u5165\u71b1\u9a30\u9a30\u7684\u8089\u6842\u6372', canvas.width / 2, 250, canvas.width - 60);\n                    ctx.font = '600 22px \"Noto Sans TC\", sans-serif'; ctx.fillText(`\u56de\u6f64\u4e2d \u00b7 ${Math.floor(currentStepData.coolingProgress)}%`, canvas.width / 2, canvas.height - 205);\n                    ctx.restore();\n                    const px = 56, py = canvas.height - 250, pw = canvas.width - 112;\n                    ctx.fillStyle = 'rgba(34,22,17,.86)'; ctx.beginPath(); ctx.roundRect(px, py + 5, pw, 20, 10); ctx.fill();\n                    const g = ctx.createLinearGradient(px, py, px + pw, py); g.addColorStop(0, '#9a542e'); g.addColorStop(1, '#edc981'); ctx.fillStyle = g; ctx.beginPath(); ctx.roundRect(px, py, pw * currentStepData.coolingProgress / 100, 20, 10); ctx.fill();\n                    return;\n                }\n                \n                const rackX = canvas.width / 2 - 200;\n                const rackY = canvas.height / 2 + 100;\n                drawImage('cooling_rack', rackX, rackY, 400, null);\n                \n                const cakeX = canvas.width / 2 - 175;\n                const cakeY = rackY - 30;\n                drawCinnamonRoll(cakeX, cakeY - 60, 350, 210);\n                // Glossy syrup glaze catches warm highlights and runs down the soft roll.\n                ctx.save(); ctx.fillStyle = 'rgba(166,83,32,.58)'; ctx.strokeStyle = 'rgba(244,190,111,.9)'; ctx.lineWidth = 6;\n                ctx.beginPath(); ctx.ellipse(canvas.width / 2, cakeY - 47, 137, 36, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();\n                for (let i = -2; i <= 2; i++) { const x = canvas.width / 2 + i * 48; ctx.beginPath(); ctx.moveTo(x, cakeY - 40); ctx.bezierCurveTo(x - 8, cakeY - 10, x + 10, cakeY + 8, x + 4, cakeY + 30 + (i % 2 ? 8 : 0)); ctx.stroke(); }\n                ctx.restore();\n                \n                if (currentStepData.coolingProgress < 95) {\n                    const alpha = Math.max(0, (95 - currentStepData.coolingProgress) / 95);\n                    if (Math.random() < 0.3 * alpha) {\n                        currentStepData.steamParticles.push({\n                            x: canvas.width / 2 + (Math.random() - 0.5) * 200,\n                            y: cakeY - 60,\n                            vx: (Math.random() - 0.5) * 0.5,\n                            vy: -1 - Math.random() * 1.5,\n                            size: 15 + Math.random() * 25,\n                            life: 1.0,\n                            drift: Math.random() * Math.PI * 2\n                        });\n                    }\n\n                    ctx.save();\n                    for (let i = currentStepData.steamParticles.length - 1; i >= 0; i--) {\n                        const p = currentStepData.steamParticles[i];\n                        p.x += p.vx + Math.sin(p.drift + Date.now() / 500) * 0.5;\n                        p.y += p.vy;\n                        p.life -= 0.008;\n                        p.size += 0.2;\n                        \n                        if (p.life <= 0) {\n                            currentStepData.steamParticles.splice(i, 1);\n                            continue;\n                        }\n\n                        const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size);\n                        grad.addColorStop(0, `rgba(255, 255, 255, ${0.3 * p.life * alpha})`);\n                        grad.addColorStop(0.5, `rgba(245, 245, 255, ${0.1 * p.life * alpha})`);\n                        grad.addColorStop(1, 'rgba(255, 255, 255, 0)');\n                        \n                        ctx.fillStyle = grad;\n                        ctx.beginPath();\n                        ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);\n                        ctx.fill();\n                    }\n                    ctx.restore();\n                }\n                \n                ctx.fillStyle = '#fff8ed';\n                ctx.font = '600 36px \"Noto Sans TC\", sans-serif';\n                ctx.textAlign = 'center';\n                ctx.fillText(`Syrup soaking into soft rolls \u00b7 ${Math.floor(currentStepData.coolingProgress)}%`, canvas.width / 2, canvas.height - 200);\n            }\n\n            // Step 6: Placing Cake on Turntable\n            function initStep6() {\n                currentStepData = { placingProgress: 0, placing: false };\n                stepContent.innerHTML = '';\n                \n                actionButton.textContent = 'Lift Warm Rolls from Syrup';\n                actionButton.disabled = false;\n                actionButton.onclick = () => {\n                    currentStepData.placing = true;\n                    actionButton.disabled = true;\n                    playSound('select_sound');\n                    \n                    const placeInterval = setInterval(() => {\n                        currentStepData.placingProgress += 4;\n                        if (currentStepData.placingProgress >= 100) {\n                            clearInterval(placeInterval);\n                            stepScores[5] = 10;\n                            actionButton.textContent = 'Next';\n                            actionButton.disabled = false;\n                            actionButton.onclick = () => {\n                                playSound('step_complete_sound');\n                                nextStep();\n                            };\n                        }\n                    }, 40);\n                };\n            }\n\n            function renderStep6() {\n                drawBackground();\n                const centerX = canvas.width / 2;\n                const progress = currentStepData.placingProgress / 100;\n                const sceneAsset = currentStepData.placing && assets.cinnamon_roll_3d_warm_roll_turntable ? 'cinnamon_roll_3d_warm_roll_turntable' : (assets.cinnamon_roll_3d_baked_tray ? 'cinnamon_roll_3d_baked_tray' : 'cinnamon_roll_3d_caramel_finish');\n                if (assets[sceneAsset]) {\n                    const lift = currentStepData.placing ? Math.sin(progress * Math.PI) * 22 : 0;\n                    drawPhotoContain(sceneAsset, 30, 300 - lift, canvas.width - 60, 650);\n                    ctx.save(); ctx.textAlign = 'center'; ctx.fillStyle = '#fff1d2'; ctx.font = '700 28px \"Noto Sans TC\", sans-serif'; ctx.shadowColor = 'rgba(12,7,4,.92)'; ctx.shadowBlur = 10;\n                    ctx.fillText(currentStepData.placing ? `\u71b1\u9a30\u9a30\u8089\u6842\u6372\u79fb\u81f3\u8f49\u76e4 \u00b7 ${Math.floor(progress * 100)}%` : '\u525b\u51fa\u7210\u7684\u7126\u7cd6\u6838\u6843\u8089\u6842\u6372', centerX, 255, canvas.width - 70); ctx.restore();\n                    if (!currentStepData.placing || progress >= 1) drawTutorialArrow(centerX, canvas.height - 130, 0);\n                    return;\n                }\n                const turntableY = canvas.height / 2 + 150, rackY = canvas.height / 2 + 100;\n                drawImage('cooling_rack', centerX - 200, rackY, 400, null);\n                drawCinnamonRoll(centerX - 175, rackY - 90, 350, 210);\n                if (currentStepData.placing && progress >= 1) drawTutorialArrow(centerX, canvas.height - 130, 0);\n            }\n\n            // Step 7: Prepare a mellow caramel glaze\n            function initStep7() {\n                currentStepData = { whippingProgress: 0, lastX: canvas.width / 2, lastY: canvas.height / 2, mixerX: canvas.width / 2, mixerY: canvas.height / 2 - 100, whipping: false, mixerSound: null, glazePoured: 0 };\n                stepContent.innerHTML = '';\n                const pourInterval = setInterval(() => { currentStepData.glazePoured = Math.min(1, currentStepData.glazePoured + .05); if (currentStepData.glazePoured >= 1) clearInterval(pourInterval); }, 50);\n                const startWhipping = e => {\n                    if (currentStepData.glazePoured < 1) return;\n                    e.preventDefault(); currentStepData.whipping = true;\n                    const pos = getInputPosition(e); currentStepData.lastX = pos.x; currentStepData.lastY = pos.y;\n                    if (!currentStepData.mixerSound) currentStepData.mixerSound = playSound('mixer_sound', .5, true);\n                };\n                const doWhipping = e => {\n                    const pos = getInputPosition(e); currentStepData.mixerX = pos.x; currentStepData.mixerY = pos.y;\n                    if (!currentStepData.whipping) return;\n                    e.preventDefault(); const distance = Math.hypot(pos.x - currentStepData.lastX, pos.y - currentStepData.lastY);\n                    if (distance > 5) { currentStepData.whippingProgress = Math.min(100, currentStepData.whippingProgress + distance / 25); currentStepData.lastX = pos.x; currentStepData.lastY = pos.y; }\n                };\n                const stopWhipping = () => { currentStepData.whipping = false; if (currentStepData.mixerSound) { currentStepData.mixerSound.source.stop(); currentStepData.mixerSound = null; } };\n                canvas.addEventListener('mousedown', startWhipping); canvas.addEventListener('touchstart', startWhipping);\n                canvas.addEventListener('mousemove', doWhipping); canvas.addEventListener('touchmove', doWhipping);\n                canvas.addEventListener('mouseup', stopWhipping); canvas.addEventListener('touchend', stopWhipping);\n                actionButton.textContent = 'Prepare Caramel Glaze'; actionButton.disabled = true;\n                actionButton.onclick = () => { if (currentStepData.whippingProgress >= 100) { stepScores[6] = 10; playSound('step_complete_sound'); nextStep(); } };\n            }\n\n            function updateStep7() { if (currentStepData.whippingProgress >= 100) actionButton.disabled = false; }\n\n            function renderStep7() {\n                drawBackground();\n                const centerX = canvas.width / 2, centerY = canvas.height / 2 + 20, progress = currentStepData.whippingProgress / 100;\n                // Use the real ceramic caramel bowl as the complete visual; no flat drawn sauce ellipse or paper-like swirls.\n                const hasSeparateSpoonBowl = !!assets.cinnamon_roll_3d_caramel_bowl_no_spoon;\n                if (hasSeparateSpoonBowl) drawImageFit('cinnamon_roll_3d_caramel_bowl_no_spoon', 52, centerY - 220, canvas.width - 104, 510);\n                else drawImageFit('cinnamon_roll_3d_caramel_bowl', 52, centerY - 220, canvas.width - 104, 510);\n                if (hasSeparateSpoonBowl) {\n                    const spoonX = currentStepData.whipping ? currentStepData.mixerX : centerX + 65;\n                    const spoonY = currentStepData.whipping ? currentStepData.mixerY : centerY - 50;\n                    const spoonAngle = currentStepData.whipping ? Math.sin(Date.now() / 150) * 0.35 - 0.2 : -0.25;\n                    draw3DStirringSpoon(spoonX, spoonY, spoonAngle);\n                }\n                ctx.save();\n                const barX = 54, barY = canvas.height - 250, barW = canvas.width - 108;\n                ctx.fillStyle = 'rgba(31,20,15,.88)'; ctx.beginPath(); ctx.roundRect(barX, barY + 5, barW, 25, 13); ctx.fill();\n                const glazeGrad = ctx.createLinearGradient(barX, barY, barX + barW, barY); glazeGrad.addColorStop(0, '#9a542e'); glazeGrad.addColorStop(1, '#edc981');\n                ctx.fillStyle = glazeGrad; ctx.beginPath(); ctx.roundRect(barX, barY, barW * progress, 25, 13); ctx.fill();\n                ctx.fillStyle = '#fff1d2'; ctx.font = '700 26px \"Noto Sans TC\", sans-serif'; ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(14,8,4,.9)'; ctx.shadowBlur = 8;\n                ctx.fillText(`\u7126\u7cd6\u91ac\u652a\u62cc\u81f3\u9806\u6ed1 \u00b7 ${Math.floor(currentStepData.whippingProgress)}%`, centerX, canvas.height - 190); ctx.restore();\n                if (progress < 1) drawTutorialArrow(centerX + Math.cos(Date.now() / 300) * 95, centerY + Math.sin(Date.now() / 300) * 35, 0);\n                else drawTutorialArrow(centerX, canvas.height - 150, 0);\n            }\n\n\n            // Step 8: Spread glossy caramel glaze over the tender cinnamon rolls\n            function initStep8() {\n                currentStepData = { spreadingProgress: 0, rotation: 0, isRotating: false, spreadingSound: null, lastX: 0 };\n                stepContent.innerHTML = '';\n                actionButton.textContent = 'Spread Caramel Glaze';\n                const handleStart = e => {\n                    const pos = getInputPosition(e); currentStepData.isRotating = true; currentStepData.lastX = pos.x;\n                    if (!currentStepData.spreadingSound) currentStepData.spreadingSound = playSound('spreading_sound', .4, true);\n                };\n                const handleMove = e => {\n                    if (!currentStepData.isRotating) return;\n                    const pos = getInputPosition(e), dx = pos.x - currentStepData.lastX;\n                    currentStepData.rotation += dx * .01; currentStepData.lastX = pos.x;\n                    currentStepData.spreadingProgress = Math.min(100, currentStepData.spreadingProgress + Math.abs(dx) * .05);\n                };\n                const handleEnd = () => {\n                    currentStepData.isRotating = false;\n                    if (currentStepData.spreadingSound) { currentStepData.spreadingSound.source.stop(); currentStepData.spreadingSound = null; }\n                };\n                canvas.addEventListener('mousedown', handleStart); canvas.addEventListener('mousemove', handleMove); canvas.addEventListener('mouseup', handleEnd);\n                canvas.addEventListener('touchstart', handleStart); canvas.addEventListener('touchmove', handleMove); canvas.addEventListener('touchend', handleEnd);\n                actionButton.disabled = true;\n                actionButton.onclick = () => { if (currentStepData.spreadingProgress >= 100) { stepScores[7] = 10; playSound('step_complete_sound'); nextStep(); } };\n            }\n\n            function renderStep8() {\n                drawBackground();\n                const centerX = canvas.width / 2, progress = currentStepData.spreadingProgress / 100;\n                // Real 3D roll-on-turntable renders; reveal the finished glaze image as the player spreads it.\n                const sceneX = 24, sceneY = 305, sceneW = canvas.width - 48, sceneH = 630;\n                const baseAsset = assets.cinnamon_roll_3d_warm_roll_turntable ? 'cinnamon_roll_3d_warm_roll_turntable' : 'cinnamon_roll_3d_caramel_finish';\n                drawPhotoContain(baseAsset, sceneX, sceneY, sceneW, sceneH);\n                if (assets.cinnamon_roll_3d_glazed_roll_turntable && progress > 0) {\n                    const glazeImage = assets.cinnamon_roll_3d_glazed_roll_turntable.img;\n                    const fitScale = Math.min(sceneW / glazeImage.width, sceneH / glazeImage.height);\n                    const drawW = glazeImage.width * fitScale, drawH = glazeImage.height * fitScale;\n                    const drawX = sceneX + (sceneW - drawW) / 2, drawY = sceneY + (sceneH - drawH) / 2;\n                    ctx.save(); ctx.beginPath(); ctx.rect(drawX, drawY + drawH * (1 - progress), drawW, drawH * progress); ctx.clip();\n                    ctx.globalAlpha = .35 + .65 * progress; ctx.drawImage(glazeImage, drawX, drawY, drawW, drawH); ctx.restore();\n                }\n                const spatX = currentStepData.isRotating ? currentStepData.lastX : centerX + 145;\n                const spatY = sceneY + sceneH * .49;\n                draw3DOffsetSpatula(spatX, spatY, -.32);\n                ctx.save(); ctx.fillStyle = '#fff1d2'; ctx.font = '700 27px \"Noto Sans TC\", sans-serif'; ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(12,7,4,.9)'; ctx.shadowBlur = 9;\n                ctx.fillText(`\u5747\u52fb\u6dcb\u4e0a\u6fc3\u90c1\u7126\u7cd6\u91ac \u00b7 ${Math.floor(currentStepData.spreadingProgress)}%`, centerX, 260, canvas.width - 60);\n                const barX = 54, barY = canvas.height - 250, barW = canvas.width - 108;\n                ctx.fillStyle = 'rgba(31,20,15,.88)'; ctx.beginPath(); ctx.roundRect(barX, barY + 5, barW, 22, 12); ctx.fill();\n                const coatGrad = ctx.createLinearGradient(barX, barY, barX + barW, barY); coatGrad.addColorStop(0, '#9a542e'); coatGrad.addColorStop(1, '#edc981');\n                ctx.fillStyle = coatGrad; ctx.beginPath(); ctx.roundRect(barX, barY, barW * progress, 22, 11); ctx.fill(); ctx.restore();\n                if (progress < 1) drawTutorialArrow(centerX + 145, sceneY + sceneH * .43, Math.PI / 2);\n                else { actionButton.disabled = false; drawTutorialArrow(centerX, canvas.height - 130, 0); }\n            }\n            function initStep9() {\n                currentStepData = {\n                    selectedTopping: 'roll_pecans', rotation: 0, isRotating: false, isDraggingDeco: false,\n                    selectedDecoIndex: -1, dragStartPos: { x: 0, y: 0 }, zoom: 1.2,\n                    toppings: [\n                        { id: 'roll_pecans', name: 'Chopped walnuts', asset: 'cinnamon_roll_3d_chopped_walnuts' },\n                        { id: 'roll_icing', name: 'Caramel drizzle', asset: 'cinnamon_roll_3d_caramel_drizzle' },\n                        { id: 'roll_cinnamon', name: 'Cinnamon dust', asset: 'cinnamon_roll_3d_cinnamon_dust' }\n                    ]\n                };\n                stepContent.innerHTML = '';\n                const toppingList = document.createElement('div');\n                toppingList.className = 'ingredient-list'; toppingList.style.position = 'absolute'; toppingList.style.top = '140px'; toppingList.style.left = '0'; toppingList.style.zIndex = '105';\n                currentStepData.toppings.forEach(topping => {\n                    const item = document.createElement('div'); item.className = `topping-item ${currentStepData.selectedTopping === topping.id ? 'selected' : ''}`;\n                    const assetObj = assets[topping.asset];\n                    const iconHtml = assetObj ? `<img class=\"topping-icon\" src=\"${assetObj.img.src}\" alt=\"${topping.name}\">` : `<div class=\"topping-icon\" style=\"background:#8b522b;border-radius:12px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;\">3D</div>`;\n                    item.innerHTML = `${iconHtml}<span class=\"topping-name\">${topping.name}</span>`;\n                    item.onclick = e => { e.stopPropagation(); currentStepData.selectedTopping = topping.id; document.querySelectorAll('.topping-item').forEach(el => el.classList.remove('selected')); item.classList.add('selected'); playSound('select_sound'); };\n                    toppingList.appendChild(item);\n                });\n                stepContent.appendChild(toppingList);\n                const handleStart = e => {\n                    const pos = getInputPosition(e), cx = canvas.width / 2, ty = canvas.height / 2 + 150, zoom = currentStepData.zoom;\n                    const rx = 175 * zoom, ry = rx * .25, topY = ty - 200 * zoom;\n                    currentStepData.dragStartPos = pos; currentStepData.hasMoved = false;\n                    let found = -1;\n                    for (let i = cakeData.decorations.length - 1; i >= 0; i--) {\n                        const d = cakeData.decorations[i], a = d.angle + currentStepData.rotation;\n                        if (Math.hypot(pos.x - (cx + rx * Math.sin(a)), pos.y - (topY + d.y * zoom + ry * Math.cos(a))) < 55 * zoom) { found = i; break; }\n                    }\n                    currentStepData.selectedDecoIndex = found; currentStepData.isDraggingDeco = found >= 0;\n                };\n                const handleMove = e => {\n                    const pos = getInputPosition(e), dxm = pos.x - currentStepData.dragStartPos.x, dym = pos.y - currentStepData.dragStartPos.y;\n                    if (Math.hypot(dxm, dym) > 10) currentStepData.hasMoved = true;\n                    if (currentStepData.isDraggingDeco && currentStepData.selectedDecoIndex >= 0) {\n                        const cx = canvas.width / 2, ty = canvas.height / 2 + 150, zoom = currentStepData.zoom, rx = 175 * zoom, ry = rx * .25, topY = ty - 200 * zoom;\n                        const angle = Math.asin(Math.max(-1, Math.min(1, (pos.x - cx) / rx)));\n                        const d = cakeData.decorations[currentStepData.selectedDecoIndex]; d.angle = angle - currentStepData.rotation;\n                        d.y = Math.max(-20, Math.min(180, (pos.y - topY - ry * Math.cos(angle)) / zoom));\n                    }\n                };\n                const handleEnd = e => {\n                    const pos = getInputPosition(e), cx = canvas.width / 2, ty = canvas.height / 2 + 150, zoom = currentStepData.zoom, rx = 175 * zoom, ry = rx * .25, topY = ty - 200 * zoom;\n                    if (!currentStepData.hasMoved && !currentStepData.isDraggingDeco) {\n                        const dx = pos.x - cx, dy = pos.y - topY;\n                        if (Math.abs(dx) <= rx && dy >= -35 && dy <= 145 * zoom) {\n                            const angle = Math.asin(Math.max(-1, Math.min(1, dx / rx)));\n                            cakeData.decorations.push({ angle, y: (dy - ry * Math.cos(angle)) / zoom, type: currentStepData.selectedTopping });\n                            playSound('place_decoration_sound');\n                        }\n                    }\n                    currentStepData.isDraggingDeco = false;\n                };\n                canvas.addEventListener('mousedown', handleStart); canvas.addEventListener('mousemove', handleMove); canvas.addEventListener('mouseup', handleEnd);\n                canvas.addEventListener('touchstart', handleStart); canvas.addEventListener('touchmove', handleMove); canvas.addEventListener('touchend', handleEnd);\n                actionButton.textContent = 'Finish Caramel & Walnuts'; actionButton.disabled = false;\n                actionButton.onclick = () => { stepScores[8] = Math.min(10, cakeData.decorations.filter(d => d.type === 'roll_pecans' || d.type === 'roll_icing').length * 2); playSound('step_complete_sound'); nextStep(); };\n            }\n\n            function renderStep9() {\n                drawBackground();\n                const centerX = canvas.width / 2, zoom = currentStepData.zoom, turntableY = canvas.height / 2 + 150;\n                const radius = 175 * zoom, radiusY = radius * .25, topY = turntableY - 200 * zoom;\n                if (assets.cinnamon_roll_3d_turntable) drawImageFit('cinnamon_roll_3d_turntable', centerX - 250 * zoom, turntableY - 74 * zoom, 500 * zoom, 180 * zoom);\n                else drawTurntable(centerX, turntableY, currentStepData.rotation, zoom);\n                drawCinnamonRoll(centerX - 175 * zoom, topY, 350 * zoom, 200 * zoom);\n                if (assets.cinnamon_roll_3d_caramel_drizzle) {\n                    for (let i = 0; i < 4; i++) drawImage('cinnamon_roll_3d_caramel_drizzle', centerX - 125 * zoom + i * 48 * zoom, topY + 58 * zoom, 105 * zoom, null);\n                } else {\n                    ctx.save(); ctx.globalAlpha = .72; ctx.strokeStyle = '#c87932'; ctx.lineWidth = 9; ctx.lineCap = 'round';\n                    for (let i = 0; i < 5; i++) { const y = topY + 40 + i * 24; ctx.beginPath(); ctx.moveTo(centerX - 120 * zoom, y); ctx.bezierCurveTo(centerX - 30 * zoom, y - 15, centerX + 25 * zoom, y + 17, centerX + 120 * zoom, y - 3); ctx.stroke(); }\n                    ctx.restore();\n                }\n                cakeData.decorations.forEach(d => {\n                    const a = d.angle + currentStepData.rotation, z = Math.cos(a);\n                    if (z > 0) {\n                        const x = centerX + radius * Math.sin(a), y = topY + d.y * zoom + radiusY * z;\n                        if (d.type === 'roll_pecans' && assets.cinnamon_roll_3d_chopped_walnuts) drawImage('cinnamon_roll_3d_chopped_walnuts', x - 30 * zoom, y - 24 * zoom, 60 * zoom, null);\n                        else if (d.type === 'roll_icing' && assets.cinnamon_roll_3d_caramel_drizzle) drawImage('cinnamon_roll_3d_caramel_drizzle', x - 30 * zoom, y - 20 * zoom, 60 * zoom, null);\n                        else if (d.type === 'roll_cinnamon' && assets.cinnamon_roll_3d_cinnamon_dust) drawImage('cinnamon_roll_3d_cinnamon_dust', x - 28 * zoom, y - 20 * zoom, 56 * zoom, null);\n                        else drawRollTopping(d.type, x, y, (d.type === 'roll_pecans' ? 56 : 48) * zoom);\n                    }\n                });\n                if (cakeData.decorations.length < 4) drawTutorialArrow(centerX, topY + 50 * zoom, 0);\n            }\n            function syncPhotoControls() {\n                photoZoom.value = String(photoStudioState.zoom);\n                photoTilt.value = String(photoStudioState.tilt);\n                photoZoomValue.textContent = `${Number(photoStudioState.zoom).toFixed(2)}\u00d7`;\n                photoTiltValue.textContent = `${Number(photoStudioState.tilt).toFixed(1)}\u00b0`;\n            }\n\n            function photoDragStart(e) {\n                if (!photoMode || e.target.closest('.photo-mode-controls, .photo-mode-heading, .photo-mode-close')) return;\n                const pos = getInputPosition(e);\n                photoStudioState.dragging = true;\n                photoStudioState.dragX = pos.x;\n                photoStudioState.dragY = pos.y;\n                if (canvas.setPointerCapture && e.pointerId !== undefined) canvas.setPointerCapture(e.pointerId);\n                e.preventDefault();\n            }\n            function photoDragMove(e) {\n                if (!photoMode || !photoStudioState.dragging) return;\n                const pos = getInputPosition(e);\n                photoStudioState.panX = Math.max(-canvas.width * .24, Math.min(canvas.width * .24, photoStudioState.panX + pos.x - photoStudioState.dragX));\n                photoStudioState.panY = Math.max(-canvas.height * .18, Math.min(canvas.height * .18, photoStudioState.panY + pos.y - photoStudioState.dragY));\n                photoStudioState.dragX = pos.x;\n                photoStudioState.dragY = pos.y;\n                e.preventDefault();\n            }\n            function photoDragEnd() { photoStudioState.dragging = false; }\n\n            function exitPhotoMode() {\n                photoMode = false;\n                photoStudioState.dragging = false;\n                canvas.removeEventListener('pointerdown', photoDragStart);\n                canvas.removeEventListener('pointermove', photoDragMove);\n                canvas.removeEventListener('pointerup', photoDragEnd);\n                canvas.removeEventListener('pointercancel', photoDragEnd);\n                gameWorld.classList.remove('photo-mode');\n                photoModePanel.classList.add('hidden');\n                showFinalScore();\n            }\n            photoModeClose.addEventListener('click', exitPhotoMode);\n            photoZoom.addEventListener('input', () => { photoStudioState.zoom = Number(photoZoom.value); syncPhotoControls(); });\n            photoTilt.addEventListener('input', () => { photoStudioState.tilt = Number(photoTilt.value); syncPhotoControls(); });\n            photoModePanel.querySelectorAll('[data-photo-adjust]').forEach(button => {\n                button.addEventListener('click', () => {\n                    const action = button.dataset.photoAdjust;\n                    if (action === 'zoom-in') photoStudioState.zoom = Math.min(1.8, photoStudioState.zoom + .08);\n                    if (action === 'zoom-out') photoStudioState.zoom = Math.max(1, photoStudioState.zoom - .08);\n                    if (action === 'tilt-left') photoStudioState.tilt = Math.max(-12, photoStudioState.tilt - 1.5);\n                    if (action === 'tilt-right') photoStudioState.tilt = Math.min(12, photoStudioState.tilt + 1.5);\n                    syncPhotoControls();\n                });\n            });\n            photoShutter.addEventListener('click', () => {\n                if (!photoMode) return;\n                const now = Date.now();\n                if (now - photoStudioState.lastCaptureAt < 500) return;\n                photoStudioState.lastCaptureAt = now;\n                photoFlash.classList.remove('flash-active');\n                void photoFlash.offsetWidth;\n                photoFlash.classList.add('flash-active');\n                photoFeedback.textContent = 'Captured for fun \u00b7 no image file was saved';\n                playSound('click_sound', .8);\n                setTimeout(() => {\n                    photoFlash.classList.remove('flash-active');\n                    if (photoMode) photoFeedback.textContent = 'In-game photo only \u00b7 not saved to your device';\n                }, 1200);\n            });\n\n            function showFinalScore() {\n                const totalScore = stepScores.reduce((a, b) => a + b, 0);\n                let rating = '';\n                \n                if (totalScore >= 54) rating = 'Master Baker';\n                else if (totalScore >= 36) rating = 'Expert Baker';\n                else if (totalScore >= 18) rating = 'Qualified Baker';\n                else rating = 'Apprentice Baker';\n                \n                const modal = document.createElement('div');\n                modal.className = 'modal';\n                modal.innerHTML = `\n                    <div class=\"modal-title\">\u2615 Twin Brothers \u7389\u6842\u6372\u5b8c\u6210\uff01</div>\n                    <div class=\"modal-content\">\n                        <div class=\"score-display\">${totalScore.toFixed(0)} Points</div>\n                        <div class=\"rating-display\">${rating}</div>\n                        <p style=\"margin-top: 16px;\">\u6fd5\u6f64\u67d4\u8edf\u3001\u8089\u6842\u9999\u6c23\u6eab\u548c\uff0c\u914d\u4e0a\u5747\u52fb\u7126\u7cd6\u91ac\u8207\u6838\u6843\u788e\u7684 Twin Brothers Coffee \u53f0\u5317\u8089\u6842\u6372\u5b8c\u6210\u4e86\uff01</p>\n                    </div>\n                    <button class=\"action-button photo-studio-button\" type=\"button\">Open Photo Studio \u00b7 \u62cd\u7167\u6a21\u5f0f</button>\n                    <button class=\"action-button\" type=\"button\">Bake Again</button>\n                `;\n                const backdrop = document.createElement('div');\n                backdrop.className = 'modal-backdrop';\n                document.body.appendChild(backdrop);\n                document.body.appendChild(modal);\n                const photoStudioBtn = modal.querySelector('.photo-studio-button');\n                photoStudioBtn.onclick = enterPhotoMode;\n                const restartBtn = modal.querySelector('.action-button:not(.photo-studio-button)');\n                restartBtn.onclick = restartGame;\n            }\n\n            function enterPhotoMode() {\n                const modal = document.querySelector('.modal');\n                const backdrop = document.querySelector('.modal-backdrop');\n                if (modal) modal.remove();\n                if (backdrop) backdrop.remove();\n                photoMode = true;\n                photoStudioState.zoom = 1.15;\n                photoStudioState.tilt = 0;\n                photoStudioState.panX = 0;\n                photoStudioState.panY = 0;\n                syncPhotoControls();\n                photoModePanel.classList.remove('hidden');\n                gameWorld.classList.add('photo-mode');\n                canvas.addEventListener('pointerdown', photoDragStart);\n                canvas.addEventListener('pointermove', photoDragMove);\n                canvas.addEventListener('pointerup', photoDragEnd);\n                canvas.addEventListener('pointercancel', photoDragEnd);\n            }\n\n            function initStep10() {\n                currentStepData = { completed: true };\n                stepContent.innerHTML = '';\n                actionButton.textContent = 'Finish \u00b7 \u5b8c\u6210';\n                actionButton.disabled = false;\n                actionButton.onclick = () => { actionButton.classList.add('hidden'); stepScores[8] = 10; playSound('step_complete_sound', 1.2); showFinalScore(); };\n            }\n\n            function renderStep10() {\n                drawBackground();\n                const photoId = assets.cinnamon_roll_3d_coffee_showcase ? 'cinnamon_roll_3d_coffee_showcase' : (assets.cinnamon_roll_3d_caramel_finish ? 'cinnamon_roll_3d_caramel_finish' : (assets.twin_brothers_cinnamon_roll_coffee ? 'twin_brothers_cinnamon_roll_coffee' : 'twin_brothers_cinnamon_roll_box'));\n                // Photo Studio controls frame the hero dessert inside the canvas; no screenshot or device file is created.\n                const photoX = 24, photoY = 250, photoW = canvas.width - 48, photoH = 760;\n                ctx.save();\n                ctx.translate(photoX + photoW / 2 + photoStudioState.panX, photoY + photoH / 2 + photoStudioState.panY);\n                ctx.rotate(photoStudioState.tilt * Math.PI / 180);\n                ctx.scale(photoStudioState.zoom, photoStudioState.zoom);\n                if (!drawPhotoCover(photoId, -photoW / 2, -photoH / 2, photoW, photoH, .5, .55)) drawCinnamonRoll(-photoW / 2 + 21, -photoH / 2 + 40, photoW - 42, photoH - 80);\n                ctx.restore();\n                ctx.save();\n                const shade = ctx.createLinearGradient(0, 900, 0, 1115); shade.addColorStop(0, 'rgba(28,17,12,0)'); shade.addColorStop(1, 'rgba(28,17,12,.72)');\n                ctx.fillStyle = shade; ctx.fillRect(24, 900, canvas.width - 48, 205);\n                ctx.textAlign = 'center'; ctx.shadowColor = 'rgba(12,7,4,.9)'; ctx.shadowBlur = 10;\n                ctx.fillStyle = '#f3d69a'; ctx.font = '700 28px Georgia, serif';\n                ctx.fillText('TWIN BROTHERS COFFEE', canvas.width / 2, 1070);\n                ctx.fillStyle = '#fff3da'; ctx.font = '600 21px \"Noto Sans TC\", sans-serif';\n                ctx.fillText('\u53f0\u5317\u6fd5\u5f0f\u7126\u7cd6\u6838\u6843\u8089\u6842\u6372 \u00b7 \u5b8c\u6210', canvas.width / 2, 1108);\n                ctx.restore();\n            }\n\n\n            function restartGame() {\n                const modal = document.querySelector('.modal');\n                const backdrop = document.querySelector('.modal-backdrop');\n                if (modal) modal.remove();\n                if (backdrop) backdrop.remove();\n\n                currentStep = 0;\n                stepScores = new Array(9).fill(0);\n                cakeData = { decorations: [] };\n                \n                cleanupEventListeners();\n                updateUI();\n                initCurrentStep();\n                actionButton.classList.remove('hidden');\n                actionButton.disabled = false;\n            }\n\n            function cleanupEventListeners() {\n                // Remove all event listeners from canvas by cloning and replacing it\n                const oldCanvas = canvas;\n                const newCanvas = oldCanvas.cloneNode(true);\n                oldCanvas.parentNode.replaceChild(newCanvas, oldCanvas);\n                canvas = newCanvas;\n                ctx = canvas.getContext('2d');\n                \n                // Also clear the UI content panel\n                stepContent.innerHTML = '';\n            }\n\n            function nextStep() {\n                currentStep++;\n                if (currentStep < 9) {\n                    cleanupEventListeners();\n                    updateUI();\n                    initCurrentStep();\n                }\n            }\n\n            function initCurrentStep() {\n                // Ensure audio context is resumed on any interaction start\n                if (audioContext.state === 'suspended') {\n                    const resumeAudio = () => {\n                        audioContext.resume();\n                        document.removeEventListener('mousedown', resumeAudio);\n                        document.removeEventListener('touchstart', resumeAudio);\n                    };\n                    document.addEventListener('mousedown', resumeAudio);\n                    document.addEventListener('touchstart', resumeAudio);\n                }\n\n                const stepInits = [\n                    initStep1, initStep2, initStep3Interactive, initStep4, initStep5, initStep6, initStep7, initStep8, initStep10\n                ];\n                \n                if (stepInits[currentStep]) {\n                    stepInits[currentStep]();\n                }\n            }\n\n            function getInputPosition(e) {\n                const rect = canvas.getBoundingClientRect();\n                let clientX, clientY;\n                \n                if (e.touches && e.touches.length > 0) {\n                    clientX = e.touches[0].clientX;\n                    clientY = e.touches[0].clientY;\n                } else if (e.changedTouches && e.changedTouches.length > 0) {\n                    clientX = e.changedTouches[0].clientX;\n                    clientY = e.changedTouches[0].clientY;\n                } else {\n                    clientX = e.clientX;\n                    clientY = e.clientY;\n                }\n                \n                // The host scales the fixed 720x1280 iframe with CSS transforms on phones.\n                // Convert screen-space coordinates back into the canvas's internal coordinate space.\n                const scaleX = rect.width > 0 ? canvas.width / rect.width : 1;\n                const scaleY = rect.height > 0 ? canvas.height / rect.height : 1;\n                const x = ((clientX || 0) - rect.left) * scaleX;\n                const y = ((clientY || 0) - rect.top) * scaleY;\n                return { x, y };\n            }\n\n            let lastTime = 0;\n            function gameLoop(timestamp) {\n                const deltaTime = Math.min((timestamp - lastTime) / 1000, 0.11);\n                lastTime = timestamp;\n\n                ctx.clearRect(0, 0, canvas.width, canvas.height);\n                if (mode === 'edit') {\n                    drawBackground();\n                    const panelX = canvas.width / 2 - 270, panelY = canvas.height / 2 - 128, panelW = 540, panelH = 256;\n                    const panelGrad = ctx.createLinearGradient(panelX, panelY, panelX + panelW, panelY + panelH);\n                    panelGrad.addColorStop(0, 'rgba(93,62,43,.94)'); panelGrad.addColorStop(.52, 'rgba(55,37,28,.96)'); panelGrad.addColorStop(1, 'rgba(34,24,19,.97)');\n                    ctx.save(); ctx.shadowColor = 'rgba(10,6,4,.58)'; ctx.shadowBlur = 26; ctx.shadowOffsetY = 12;\n                    ctx.fillStyle = panelGrad; ctx.strokeStyle = '#d2b477'; ctx.lineWidth = 3;\n                    ctx.beginPath(); ctx.roundRect(panelX, panelY, panelW, panelH, 28); ctx.fill(); ctx.stroke();\n                    ctx.strokeStyle = 'rgba(255,239,204,.28)'; ctx.lineWidth = 2;\n                    ctx.beginPath(); ctx.roundRect(panelX + 9, panelY + 9, panelW - 18, panelH - 18, 21); ctx.stroke(); ctx.restore();\n                    ctx.fillStyle = '#fff0d2';\n                    ctx.font = '40px \"Noto Sans TC\", sans-serif';\n                    ctx.textAlign = 'center';\n                    ctx.fillText('Edit Mode', canvas.width / 2, canvas.height / 2);\n                    ctx.font = '32px \"Noto Sans TC\", sans-serif';\n                    ctx.fillText('Adjust settings in the right panel', canvas.width / 2, canvas.height / 2 + 50);\n                    ctx.fillText(`Kitchen: ${window.gameConfig.kitchenStyle === 'twin_brothers' ? 'Twin Brothers Coffee' : (window.gameConfig.kitchenStyle.includes('japanese') ? 'Japanese' : 'Modern')}`, canvas.width / 2, canvas.height / 2 + 80);\n                } else {\n                    const stepUpdates = [\n                        updateStep1, updateStep2, () => {}, () => {}, () => {},\n                        () => {}, updateStep7, () => {}, () => {}, () => {}\n                    ];\n                    const stepRenders = [\n                        renderStep1, renderStep2, renderStep3Interactive, renderStep4, renderStep5,\n                        renderStep6, renderStep7, renderStep8, renderStep10\n                    ];\n                    if (stepUpdates[currentStep]) stepUpdates[currentStep](deltaTime);\n                    if (stepRenders[currentStep]) stepRenders[currentStep]();\n                }\n\n                drawLuxuryFrame();\n                requestAnimationFrame(gameLoop);\n            }\n\n            if (mode === 'play') {\n                updateUI();\n                initCurrentStep();\n            }\n\n            requestAnimationFrame(gameLoop);\n        }\n    </script>\n</body>\n</html>", "game_config": "{\n  \"cakeFlavor\": \"twin_brothers_cinnamon_caramel_walnut\",\n  \"cakeSize\": 7,\n  \"decorationTheme\": \"twin_brothers\",\n  \"showHints\": false,\n  \"soundVolume\": 50,\n  \"musicVolume\": 30,\n  \"language\": \"en-US\",\n  \"kitchenStyle\": \"twin_brothers\",\n  \"timeOfDay\": \"morning\",\n  \"ingredients\": {\n    \"flour\": {\n      \"target\": 300,\n      \"tolerance\": 5,\n      \"current\": 0\n    },\n    \"yeast\": {\n      \"target\": 7,\n      \"tolerance\": 2,\n      \"current\": 0\n    },\n    \"sugar\": {\n      \"target\": 30,\n      \"tolerance\": 3,\n      \"current\": 0\n    },\n    \"salt\": {\n      \"target\": 5,\n      \"tolerance\": 1,\n      \"current\": 0\n    },\n    \"milk\": {\n      \"target\": 120,\n      \"tolerance\": 5,\n      \"current\": 0\n    },\n    \"water\": {\n      \"target\": 50,\n      \"tolerance\": 5,\n      \"current\": 0\n    },\n    \"eggs\": {\n      \"target\": 1,\n      \"tolerance\": 0,\n      \"current\": 0\n    },\n    \"butter\": {\n      \"target\": 45,\n      \"tolerance\": 5,\n      \"current\": 0\n    },\n    \"brownSugar\": {\n      \"target\": 100,\n      \"tolerance\": 5,\n      \"current\": 0\n    },\n    \"cinnamon\": {\n      \"target\": 18,\n      \"tolerance\": 2,\n      \"current\": 0\n    },\n    \"fillingButter\": {\n      \"target\": 45,\n      \"tolerance\": 5,\n      \"current\": 0\n    },\n    \"caramel\": {\n      \"target\": 120,\n      \"tolerance\": 5,\n      \"current\": 0\n    },\n    \"walnuts\": {\n      \"target\": 60,\n      \"tolerance\": 5,\n      \"current\": 0\n    }\n  }\n}", "game_design_document": "## Core Game Loop and Mechanics\n\n**\u6696\u5fc3\u86cb\u7cd5\u65e5\u8a18**\u662f\u4e00\u6b3e\u6b65\u9a5f\u5f0f\u70d8\u7119\u6a21\u64ec\u904a\u6232\uff0c\u73a9\u5bb6\u5c07\u9ad4\u9a57\u88fd\u4f5c\u751f\u65e5\u86cb\u7cd5\u7684\u5b8c\u6574\u6d41\u7a0b\u3002\u904a\u6232\u63a1\u7528\u6eab\u99a8\u6cbb\u7652\u7684\u98a8\u683c\uff0c\u6c92\u6709\u6642\u9593\u58d3\u529b\uff0c\u8b93\u73a9\u5bb6\u53ef\u4ee5\u653e\u9b06\u5730\u4eab\u53d7\u70d8\u7119\u904e\u7a0b\u3002\n\n### \u904a\u6232\u6d41\u7a0b\n\n\u73a9\u5bb6\u5c07\u4f9d\u5e8f\u5b8c\u6210\u4ee5\u4e0b\u6b65\u9a5f\uff1a\n\n1. **\u6e96\u5099\u6750\u6599\u968e\u6bb5**\n   - \u73a9\u5bb6\u9700\u8981\u5f9e\u5eda\u623f\u6ac3\u5b50\u4e2d\u9078\u64c7\u6b63\u78ba\u7684\u6750\u6599\uff08\u9eb5\u7c89\u3001\u7cd6\u3001\u96de\u86cb\u3001\u725b\u5976\u3001\u5976\u6cb9\u3001\u6ce1\u6253\u7c89\uff09\n   - \u4f7f\u7528\u96fb\u5b50\u79e4\u79e4\u91cf\u6bcf\u7a2e\u6750\u6599\u5230\u6b63\u78ba\u7684\u91cd\u91cf\n   - \u6750\u6599\u6703\u986f\u793a\u76ee\u6a19\u91cd\u91cf\u548c\u7576\u524d\u91cd\u91cf\uff0c\u73a9\u5bb6\u9700\u8981\u9ede\u64ca\u6dfb\u52a0\u6309\u9215\u4f86\u589e\u52a0\u6750\u6599\n   - \u7576\u91cd\u91cf\u5728\u76ee\u6a19\u7bc4\u570d\u5167\uff08\u00b15g\uff09\u6642\u6703\u986f\u793a\u7da0\u8272\u52fe\u9078\u6a19\u8a18\n\n2. **\u6df7\u5408\u9eb5\u7cca\u968e\u6bb5**\n   - \u5c07\u6750\u6599\u4f9d\u5e8f\u52a0\u5165\u652a\u62cc\u7897\u4e2d\n   - \u4f7f\u7528\u624b\u6301\u652a\u62cc\u5668\u652a\u62cc\u9eb5\u7cca\n   - \u73a9\u5bb6\u9700\u8981\u5728\u7897\u4e0a\u4f86\u56de\u6ed1\u52d5\uff08\u79fb\u52d5\u7aef\uff09\u6216\u79fb\u52d5\u6ed1\u9f20\uff08\u684c\u9762\u7aef\uff09\u4f86\u652a\u62cc\n   - \u652a\u62cc\u9032\u5ea6\u689d\u6703\u986f\u793a\u6df7\u5408\u7a0b\u5ea6\uff0c\u9700\u8981\u9054\u5230100%\n   - \u9eb5\u7cca\u7684\u984f\u8272\u548c\u8cea\u5730\u6703\u96a8\u8457\u652a\u62cc\u9010\u6f38\u8b8a\u5316\n\n3. **\u5012\u5165\u6a21\u5177\u968e\u6bb5**\n   - \u5c07\u652a\u62cc\u597d\u7684\u9eb5\u7cca\u5012\u5165\u5713\u5f62\u86cb\u7cd5\u6a21\u5177\n   - \u73a9\u5bb6\u9700\u8981\u50be\u659c\u652a\u62cc\u7897\uff08\u62d6\u52d5\u6216\u50be\u659c\u624b\u52e2\uff09\n   - \u9eb5\u7cca\u6703\u4ee5\u6d41\u9ad4\u52d5\u756b\u7684\u65b9\u5f0f\u6d41\u5165\u6a21\u5177\n   - \u9700\u8981\u5012\u5165\u9069\u91cf\u7684\u9eb5\u7cca\uff08\u7d0480%\u6eff\uff09\n\n4. **\u70d8\u70e4\u968e\u6bb5**\n   - \u5c07\u86cb\u7cd5\u6a21\u5177\u653e\u5165\u70e4\u7bb1\n   - \u8a2d\u5b9a\u6b63\u78ba\u7684\u6eab\u5ea6\uff08180\u00b0C\uff09\u548c\u6642\u9593\uff0830\u5206\u9418\uff09\n   - \u73a9\u5bb6\u53ef\u4ee5\u901a\u904e\u70e4\u7bb1\u7a97\u53e3\u770b\u5230\u86cb\u7cd5\u9010\u6f38\u81a8\u8139\u548c\u8b8a\u8272\n   - \u6709\u6eab\u5ea6\u8a08\u548c\u8a08\u6642\u5668\u986f\u793a\u7576\u524d\u72c0\u614b\n   - \u6642\u9593\u52a0\u901f\u6a5f\u5236\uff1a\u5be6\u969b\u7b49\u5f85\u6642\u9593\u7d0430\u79d2\uff0c\u4f46\u986f\u793a\u70ba30\u5206\u9418\n   - \u86cb\u7cd5\u6703\u7d93\u6b77\uff1a\u751f\u9eb5\u7cca \u2192 \u958b\u59cb\u81a8\u8139 \u2192 \u8868\u9762\u91d1\u9ec3 \u2192 \u5b8c\u6210\n\n5. **\u51b7\u537b\u968e\u6bb5**\n   - \u5c07\u70e4\u597d\u7684\u86cb\u7cd5\u5f9e\u70e4\u7bb1\u53d6\u51fa\n   - \u653e\u5728\u51b7\u537b\u67b6\u4e0a\n   - \u86cb\u7cd5\u6703\u5192\u51fa\u71b1\u6c23\uff0c\u6eab\u5ea6\u8a08\u986f\u793a\u6eab\u5ea6\u9010\u6f38\u4e0b\u964d\n   - \u7b49\u5f85\u7d0415\u79d2\uff08\u904a\u6232\u6642\u959310\u5206\u9418\uff09\n\n6. **\u5207\u7247\u968e\u6bb5**\n   - \u4f7f\u7528\u9577\u5200\u5c07\u86cb\u7cd5\u6a6b\u5207\u6210\u5169\u5c64\n   - \u73a9\u5bb6\u9700\u8981\u6cbf\u8457\u4e2d\u9593\u7684\u865b\u7dda\u5f15\u5c0e\u7dda\u6c34\u5e73\u6ed1\u52d5\u5200\u5b50\n   - \u5207\u5f97\u8d8a\u5e73\u6574\uff0c\u8a55\u5206\u8d8a\u9ad8\n\n7. **\u6253\u767c\u9bae\u5976\u6cb9\u968e\u6bb5**\n   - \u5c07\u9bae\u5976\u6cb9\u5012\u5165\u652a\u62cc\u7897\n   - \u4f7f\u7528\u96fb\u52d5\u652a\u62cc\u5668\u6253\u767c\n   - \u73a9\u5bb6\u9700\u8981\u6301\u7e8c\u652a\u62cc\uff08\u5713\u5468\u904b\u52d5\uff09\n   - \u5976\u6cb9\u6703\u7d93\u6b77\uff1a\u6db2\u614b \u2192 \u958b\u59cb\u8b8a\u7a20 \u2192 \u5f62\u6210\u8edf\u6027\u767c\u6ce1 \u2192 \u786c\u6027\u767c\u6ce1\n   - \u9032\u5ea6\u689d\u986f\u793a\u6253\u767c\u7a0b\u5ea6\n\n8. **\u7d44\u88dd\u86cb\u7cd5\u968e\u6bb5**\n   - \u5728\u5e95\u5c64\u86cb\u7cd5\u4e0a\u5857\u62b9\u9bae\u5976\u6cb9\n   - \u73a9\u5bb6\u4f7f\u7528\u62b9\u5200\uff08\u62d6\u52d5\u624b\u52e2\uff09\u5c07\u5976\u6cb9\u5747\u52fb\u5857\u62b9\n   - \u653e\u4e0a\u7b2c\u4e8c\u5c64\u86cb\u7cd5\n   - \u5728\u6574\u500b\u86cb\u7cd5\u5916\u5c64\u5857\u62b9\u9bae\u5976\u6cb9\n   - \u9700\u8981\u5857\u62b9\u9802\u90e8\u548c\u5074\u9762\n\n9. **\u88dd\u98fe\u968e\u6bb5**\n   - \u9078\u64c7\u88dd\u98fe\u5143\u7d20\uff1a\u8349\u8393\u3001\u85cd\u8393\u3001\u5de7\u514b\u529b\u7247\u3001\u7cd6\u73e0\u3001\u881f\u71ed\n   - \u9ede\u64ca\u6216\u62d6\u52d5\u5c07\u88dd\u98fe\u7269\u653e\u7f6e\u5728\u86cb\u7cd5\u4e0a\n   - \u53ef\u4ee5\u81ea\u7531\u5275\u4f5c\uff0c\u6c92\u6709\u56fa\u5b9a\u8981\u6c42\n   - \u6700\u5f8c\u63d2\u4e0a\u751f\u65e5\u881f\u71ed\n\n10. **\u5b8c\u6210\u6176\u795d**\n    - \u5c55\u793a\u5b8c\u6210\u7684\u86cb\u7cd5\n    - \u9ede\u71c3\u881f\u71ed\uff0c\u64ad\u653e\u751f\u65e5\u5feb\u6a02\u97f3\u6a02\n    - \u986f\u793a\u88fd\u4f5c\u8a55\u5206\uff08\u57fa\u65bc\u5404\u6b65\u9a5f\u7684\u5b8c\u6210\u5ea6\uff09\n    - \u53ef\u4ee5\u62cd\u7167\u4fdd\u5b58\u6216\u91cd\u65b0\u88fd\u4f5c\n\n### \u904a\u6232\u7279\u8272\n\n- **\u771f\u5be6\u6b65\u9a5f**\uff1a\u5b8c\u5168\u6309\u7167\u73fe\u5be6\u4e16\u754c\u7684\u70d8\u7119\u6d41\u7a0b\u8a2d\u8a08\n- **\u8996\u89ba\u56de\u994b**\uff1a\u6bcf\u500b\u6b65\u9a5f\u90fd\u6709\u6e05\u6670\u7684\u8996\u89ba\u8b8a\u5316\u548c\u9032\u5ea6\u63d0\u793a\n- **\u7121\u58d3\u529b\u9ad4\u9a57**\uff1a\u6c92\u6709\u6642\u9593\u9650\u5236\uff0c\u73a9\u5bb6\u53ef\u4ee5\u6309\u81ea\u5df1\u7684\u7bc0\u594f\u9032\u884c\n- **\u6559\u80b2\u610f\u7fa9**\uff1a\u73a9\u5bb6\u53ef\u4ee5\u5b78\u7fd2\u5230\u771f\u5be6\u7684\u70d8\u7119\u77e5\u8b58\n- **\u6210\u5c31\u611f**\uff1a\u5b8c\u6210\u5f8c\u53ef\u4ee5\u770b\u5230\u81ea\u5df1\u89aa\u624b\u88fd\u4f5c\u7684\u7cbe\u7f8e\u86cb\u7cd5\n\n### Win/Lose Conditions\n\n\u9019\u662f\u4e00\u6b3e\u6c92\u6709\u5931\u6557\u7684\u904a\u6232\uff0c\u91cd\u9ede\u5728\u65bc\u9ad4\u9a57\u904e\u7a0b\uff1a\n- **\u5b8c\u6210\u689d\u4ef6**\uff1a\u5b8c\u6210\u6240\u670910\u500b\u6b65\u9a5f\n- **\u8a55\u5206\u7cfb\u7d71**\uff1a\n  - \u6750\u6599\u79e4\u91cf\u6e96\u78ba\u5ea6\uff08\u6bcf\u7a2e\u6750\u65990-10\u5206\uff09\n  - \u652a\u62cc\u5b8c\u6210\u5ea6\uff080-10\u5206\uff09\n  - \u70d8\u70e4\u6eab\u5ea6\u548c\u6642\u9593\u6e96\u78ba\u5ea6\uff080-10\u5206\uff09\n  - \u5207\u7247\u5e73\u6574\u5ea6\uff080-10\u5206\uff09\n  - \u9bae\u5976\u6cb9\u6253\u767c\u7a0b\u5ea6\uff080-10\u5206\uff09\n  - \u88dd\u98fe\u5275\u610f\uff080-10\u5206\uff09\n  - \u7e3d\u5206\uff1a0-70\u5206\n- **\u8a55\u7d1a**\uff1a\n  - 60-70\u5206\uff1a\u5b8c\u7f8e\u86cb\u7cd5\u5e2b\n  - 45-59\u5206\uff1a\u512a\u79c0\u86cb\u7cd5\u5e2b\n  - 30-44\u5206\uff1a\u5408\u683c\u86cb\u7cd5\u5e2b\n  - 0-29\u5206\uff1a\u65b0\u624b\u86cb\u7cd5\u5e2b\n\n## Starting Scope\n\n\u7b2c\u4e00\u7248\u5be6\u73fe\u5c07\u5305\u542b\u5b8c\u6574\u768410\u500b\u6b65\u9a5f\uff0c\u4f46\u6703\u7c21\u5316\u67d0\u4e9b\u7d30\u7bc0\uff1a\n\n**\u7b2c\u4e00\u7248\u5305\u542b\uff1a**\n- \u6240\u670910\u500b\u6838\u5fc3\u6b65\u9a5f\u7684\u57fa\u672c\u5be6\u73fe\n- \u6e05\u6670\u7684UI\u5f15\u5c0e\u548c\u9032\u5ea6\u63d0\u793a\n- \u57fa\u672c\u7684\u8996\u89ba\u56de\u994b\u548c\u52d5\u756b\n- \u7c21\u55ae\u7684\u8a55\u5206\u7cfb\u7d71\n- \u5b8c\u6210\u6176\u795d\u756b\u9762\n\n**\u7b2c\u4e00\u7248\u7c21\u5316\uff1a**\n- \u88dd\u98fe\u9078\u9805\u9650\u5236\u57285-6\u7a2e\u57fa\u672c\u5143\u7d20\n- \u53ea\u88fd\u4f5c\u4e00\u7a2e\u53e3\u5473\u7684\u86cb\u7cd5\uff08\u9999\u8349\u86cb\u7cd5\uff09\n- \u7c21\u5316\u7684\u7269\u7406\u6a21\u64ec\uff08\u9eb5\u7cca\u6d41\u52d5\u3001\u5976\u6cb9\u5857\u62b9\uff09\n\n**\u672a\u4f86\u53ef\u64f4\u5c55\uff1a**\n- \u66f4\u591a\u86cb\u7cd5\u53e3\u5473\uff08\u5de7\u514b\u529b\u3001\u62b9\u8336\u3001\u7d05\u7d72\u7d68\uff09\n- \u66f4\u591a\u88dd\u98fe\u9078\u9805\u548c\u4e3b\u984c\n- \u4e0d\u540c\u5f62\u72c0\u7684\u86cb\u7cd5\uff08\u65b9\u5f62\u3001\u5fc3\u5f62\uff09\n- \u591a\u5c64\u86cb\u7cd5\uff083\u5c64\u6216\u66f4\u591a\uff09\n- \u66f4\u8907\u96dc\u7684\u88dd\u98fe\u6280\u5de7\uff08\u88f1\u82b1\u3001\u7cd6\u971c\u7e6a\u756b\uff09\n- \u6210\u5c31\u7cfb\u7d71\u548c\u6536\u85cf\u518a\n- \u5206\u4eab\u529f\u80fd\n\n## Game Perspective\n\n\u904a\u6232\u63a1\u7528**\u56fa\u5b9a\u8996\u89d2\uff08fixed-screen\uff09**\uff0c\u6a21\u64ec\u73a9\u5bb6\u7ad9\u5728\u5eda\u623f\u5de5\u4f5c\u53f0\u524d\u7684\u7b2c\u4e00\u4eba\u7a31\u8996\u89d2\u3002\n\n- \u6bcf\u500b\u6b65\u9a5f\u90fd\u6709\u5c08\u9580\u7684\u5834\u666f\u8996\u5716\n- \u76f8\u6a5f\u4f4d\u7f6e\u56fa\u5b9a\uff0c\u5c55\u793a\u7576\u524d\u64cd\u4f5c\u5340\u57df\n- \u6240\u6709\u4e92\u52d5\u5143\u7d20\u90fd\u5728\u53ef\u898b\u7bc4\u570d\u5167\n- UI\u5143\u7d20\u56fa\u5b9a\u5728\u87a2\u5e55\u908a\u7de3\uff08\u9802\u90e8\u986f\u793a\u6b65\u9a5f\u9032\u5ea6\uff0c\u5e95\u90e8\u986f\u793a\u64cd\u4f5c\u63d0\u793a\uff09\n\n## Game World and Level Design\n\n\u904a\u6232\u8a2d\u5b9a\u5728\u4e00\u500b\u6eab\u99a8\u7684\u5bb6\u5ead\u5eda\u623f\u4e2d\uff0c\u6574\u500b\u904a\u6232\u5c31\u662f\u4e00\u500b\u5b8c\u6574\u7684\u95dc\u5361\uff08\u88fd\u4f5c\u4e00\u500b\u86cb\u7cd5\uff09\u3002\n\n### \u5834\u666f\u8a2d\u8a08\n\n**\u5eda\u623f\u74b0\u5883\uff1a**\n- \u6eab\u6696\u7684\u6728\u8cea\u5de5\u4f5c\u53f0\n- \u80cc\u666f\u6709\u6ae5\u6ac3\u3001\u70e4\u7bb1\u3001\u51b0\u7bb1\u7b49\u5eda\u623f\u8a2d\u5099\n- \u67d4\u548c\u7684\u81ea\u7136\u5149\u5f9e\u7a97\u6236\u7167\u5165\n- \u7246\u4e0a\u6709\u88dd\u98fe\u756b\u548c\u6642\u9418\n- \u6574\u9ad4\u8272\u8abf\u6eab\u6696\u8212\u9069\uff08\u7c73\u8272\u3001\u6dfa\u6728\u8272\u3001\u67d4\u548c\u7684\u767d\u8272\uff09\n\n**\u5404\u6b65\u9a5f\u5834\u666f\uff1a**\n\n1. **\u6e96\u5099\u6750\u6599\u5834\u666f**\uff1a\u5de5\u4f5c\u53f0\u4e0a\u6709\u96fb\u5b50\u79e4\uff0c\u80cc\u666f\u6709\u6253\u958b\u7684\u6ae5\u6ac3\u986f\u793a\u5404\u7a2e\u6750\u6599\n2. **\u6df7\u5408\u9eb5\u7cca\u5834\u666f**\uff1a\u5de5\u4f5c\u53f0\u4e2d\u592e\u6709\u5927\u652a\u62cc\u7897\uff0c\u65c1\u908a\u6709\u96fb\u52d5\u652a\u62cc\u5668\n3. **\u5012\u5165\u6a21\u5177\u5834\u666f**\uff1a\u5de5\u4f5c\u53f0\u4e0a\u6709\u86cb\u7cd5\u6a21\u5177\uff0c\u652a\u62cc\u7897\u50be\u659c\u5012\u5165\n4. **\u70d8\u70e4\u5834\u666f**\uff1a\u805a\u7126\u5728\u70e4\u7bb1\uff0c\u53ef\u4ee5\u900f\u904e\u73bb\u7483\u7a97\u770b\u5230\u5167\u90e8\n5. **\u51b7\u537b\u5834\u666f**\uff1a\u5de5\u4f5c\u53f0\u4e0a\u6709\u51b7\u537b\u67b6\uff0c\u86cb\u7cd5\u5728\u4e0a\u9762\u5192\u71b1\u6c23\n6. **\u5207\u7247\u5834\u666f**\uff1a\u5de5\u4f5c\u53f0\u4e0a\u6709\u86cb\u7cd5\u548c\u9577\u5200\uff0c\u6709\u5207\u5272\u5f15\u5c0e\u7dda\n7. **\u6253\u767c\u9bae\u5976\u6cb9\u5834\u666f**\uff1a\u5de5\u4f5c\u53f0\u4e0a\u6709\u652a\u62cc\u7897\u548c\u96fb\u52d5\u652a\u62cc\u5668\n8. **\u7d44\u88dd\u5834\u666f**\uff1a\u5de5\u4f5c\u53f0\u4e0a\u6709\u86cb\u7cd5\u5c64\u548c\u62b9\u5200\uff0c\u65c1\u908a\u6709\u9bae\u5976\u6cb9\u7897\n9. **\u88dd\u98fe\u5834\u666f**\uff1a\u5de5\u4f5c\u53f0\u4e0a\u6709\u5b8c\u6574\u86cb\u7cd5\uff0c\u5468\u570d\u6709\u5404\u7a2e\u88dd\u98fe\u6750\u6599\n10. **\u5b8c\u6210\u5834\u666f**\uff1a\u5c55\u793a\u5b8c\u6210\u7684\u86cb\u7cd5\uff0c\u80cc\u666f\u6709\u6176\u795d\u5143\u7d20\uff08\u6c23\u7403\u3001\u5f69\u5e36\uff09\n\n### \u5834\u666f\u8f49\u63db\n\n- \u6bcf\u5b8c\u6210\u4e00\u500b\u6b65\u9a5f\uff0c\u6703\u6709\u7c21\u77ed\u7684\u8f49\u5834\u52d5\u756b\uff08\u6de1\u5165\u6de1\u51fa\uff09\n- \u8f49\u5834\u6642\u986f\u793a\u4e0b\u4e00\u6b65\u9a5f\u7684\u540d\u7a31\u548c\u7c21\u77ed\u8aaa\u660e\n- \u4fdd\u6301\u6d41\u66a2\u7684\u904a\u6232\u9ad4\u9a57\uff0c\u8f49\u5834\u6642\u9593\u7d041\u79d2\n\n## Edit Mode Design\n\nEdit Mode\u8b93\u73a9\u5bb6\u53ef\u4ee5\u81ea\u8a02\u904a\u6232\u7684\u67d0\u4e9b\u5143\u7d20\uff0c\u589e\u52a0\u500b\u4eba\u5316\u9ad4\u9a57\u3002\n\n### \u53ef\u7de8\u8f2f\u5c6c\u6027\n\n**\u86cb\u7cd5\u5ba2\u88fd\u5316\uff1a**\n- **\u86cb\u7cd5\u53e3\u5473**\uff1a\u9999\u8349\uff08\u9810\u8a2d\uff09\u3001\u5de7\u514b\u529b\u3001\u8349\u8393\u3001\u62b9\u8336\n  - \u5f71\u97ff\u86cb\u7cd5\u984f\u8272\u548c\u67d0\u4e9b\u6750\u6599\n- **\u86cb\u7cd5\u5c3a\u5bf8**\uff1a6\u540b\uff08\u9810\u8a2d\uff09\u30018\u540b\u300110\u540b\n  - \u5f71\u97ff\u6750\u6599\u7528\u91cf\u548c\u70d8\u70e4\u6642\u9593\n- **\u88dd\u98fe\u4e3b\u984c**\uff1a\u7d93\u5178\uff08\u9810\u8a2d\uff09\u3001\u5f69\u8679\u3001\u512a\u96c5\u3001\u7ae5\u8da3\n  - \u5f71\u97ff\u53ef\u7528\u7684\u88dd\u98fe\u5143\u7d20\u548c\u984f\u8272\u65b9\u6848\n\n**\u904a\u6232\u8a2d\u5b9a\uff1a**\n- **\u986f\u793a\u63d0\u793a**\uff1a\u958b\u555f\uff08\u9810\u8a2d\uff09/\u95dc\u9589\n  - \u63a7\u5236\u662f\u5426\u986f\u793a\u64cd\u4f5c\u63d0\u793a\u548c\u5f15\u5c0e\u7dda\n- **\u97f3\u6548\u97f3\u91cf**\uff1a0-100%\uff08\u9810\u8a2d50%\uff09\n- **\u97f3\u6a02\u97f3\u91cf**\uff1a0-100%\uff08\u9810\u8a2d30%\uff09\n- **\u8a9e\u8a00**\uff1a\u7e41\u9ad4\u4e2d\u6587\uff08\u9810\u8a2d\uff09\u3001\u82f1\u6587\n\n**\u5eda\u623f\u74b0\u5883\uff1a**\n- **\u5eda\u623f\u98a8\u683c**\uff1a\u73fe\u4ee3\uff08\u9810\u8a2d\uff09\u3001\u9109\u6751\u3001\u5de5\u696d\u98a8\u3001\u65e5\u5f0f\n  - \u6539\u8b8a\u80cc\u666f\u548c\u5de5\u4f5c\u53f0\u7684\u8996\u89ba\u98a8\u683c\n- **\u6642\u9593**\uff1a\u65e9\u6668\uff08\u9810\u8a2d\uff09\u3001\u4e0b\u5348\u3001\u508d\u665a\n  - \u5f71\u97ff\u5149\u7dda\u548c\u7a97\u5916\u666f\u8272\n\n### Edit Mode UI\n\n- Edit Mode\u901a\u904e\u4e3b\u9078\u55ae\u9032\u5165\n- \u4f7f\u7528\u5206\u9801\u5f0f\u4ecb\u9762\u7d44\u7e54\u4e0d\u540c\u985e\u5225\u7684\u8a2d\u5b9a\n- \u6bcf\u500b\u9078\u9805\u90fd\u6709\u9810\u89bd\u5716\u793a\n- \u66f4\u6539\u6703\u5373\u6642\u53cd\u6620\u5728\u904a\u6232\u4e2d\n- \u6709\u300c\u91cd\u7f6e\u70ba\u9810\u8a2d\u300d\u6309\u9215\n\n### \u9650\u5236\n\n- \u6838\u5fc3\u904a\u6232\u6d41\u7a0b\u4e0d\u53ef\u66f4\u6539\uff0810\u500b\u6b65\u9a5f\u9806\u5e8f\u56fa\u5b9a\uff09\n- \u6750\u6599\u6bd4\u4f8b\u7531\u7cfb\u7d71\u81ea\u52d5\u8abf\u6574\uff0c\u78ba\u4fdd\u904a\u6232\u5e73\u8861\n- \u67d0\u4e9b\u9032\u968e\u9078\u9805\u9700\u8981\u5b8c\u6210\u904a\u6232\u4e00\u6b21\u5f8c\u89e3\u9396\n\n## Control Design\n\n\u904a\u6232\u63a1\u7528\u7c21\u55ae\u76f4\u89c0\u7684\u9ede\u64ca/\u89f8\u63a7\u64cd\u4f5c\uff0c\u9069\u5408\u4f11\u9592\u73a9\u5bb6\u3002\n\n### \u79fb\u52d5\u7aef\u63a7\u5236\uff08\u89f8\u63a7\uff09\n\n**\u57fa\u672c\u64cd\u4f5c\uff1a**\n- **\u9ede\u64ca**\uff1a\u9078\u64c7\u7269\u54c1\u3001\u78ba\u8a8d\u64cd\u4f5c\u3001\u9ede\u64ca\u6309\u9215\n- **\u62d6\u52d5**\uff1a\u79fb\u52d5\u7269\u54c1\u3001\u5857\u62b9\u5976\u6cb9\u3001\u88dd\u98fe\u86cb\u7cd5\n- **\u9577\u6309**\uff1a\u6301\u7e8c\u64cd\u4f5c\uff08\u5982\u6301\u7e8c\u6dfb\u52a0\u6750\u6599\uff09\n- **\u6ed1\u52d5**\uff1a\u652a\u62cc\u52d5\u4f5c\u3001\u5207\u86cb\u7cd5\n\n**\u5177\u9ad4\u6b65\u9a5f\u64cd\u4f5c\uff1a**\n\n1. **\u6e96\u5099\u6750\u6599**\uff1a\u9ede\u64ca\u6750\u6599\u9078\u64c7\uff0c\u9577\u6309\u300c\u6dfb\u52a0\u300d\u6309\u9215\u589e\u52a0\u91cd\u91cf\n2. **\u6df7\u5408\u9eb5\u7cca**\uff1a\u5728\u7897\u4e0a\u4f86\u56de\u6ed1\u52d5\u624b\u6307\u9032\u884c\u652a\u62cc\n3. **\u5012\u5165\u6a21\u5177**\uff1a\u62d6\u52d5\u652a\u62cc\u7897\u50be\u659c\u5012\u51fa\u9eb5\u7cca\n4. **\u70d8\u70e4**\uff1a\u9ede\u64ca\u6eab\u5ea6\u548c\u6642\u9593\u6309\u9215\u8abf\u6574\uff0c\u9ede\u64ca\u300c\u958b\u59cb\u70d8\u70e4\u300d\n5. **\u51b7\u537b**\uff1a\u7121\u9700\u64cd\u4f5c\uff0c\u81ea\u52d5\u9032\u884c\n6. **\u5207\u7247**\uff1a\u6cbf\u8457\u5f15\u5c0e\u7dda\u6c34\u5e73\u6ed1\u52d5\u624b\u6307\n7. **\u6253\u767c\u9bae\u5976\u6cb9**\uff1a\u5728\u7897\u4e0a\u756b\u5713\u5708\u652a\u62cc\n8. **\u7d44\u88dd**\uff1a\u62d6\u52d5\u62b9\u5200\u5857\u62b9\u5976\u6cb9\n9. **\u88dd\u98fe**\uff1a\u9ede\u64ca\u9078\u64c7\u88dd\u98fe\u7269\uff0c\u9ede\u64ca\u86cb\u7cd5\u653e\u7f6e\n10. **\u5b8c\u6210**\uff1a\u9ede\u64ca\u881f\u71ed\u9ede\u71c3\n\n### \u684c\u9762\u7aef\u63a7\u5236\uff08\u6ed1\u9f20+\u9375\u76e4\uff09\n\n**\u57fa\u672c\u64cd\u4f5c\uff1a**\n- **\u5de6\u9375\u9ede\u64ca**\uff1a\u9078\u64c7\u7269\u54c1\u3001\u78ba\u8a8d\u64cd\u4f5c\n- **\u5de6\u9375\u62d6\u52d5**\uff1a\u79fb\u52d5\u7269\u54c1\u3001\u5857\u62b9\u3001\u88dd\u98fe\n- **\u6ed1\u9f20\u79fb\u52d5**\uff1a\u652a\u62cc\u3001\u5207\u5272\u52d5\u4f5c\n- **\u6efe\u8f2a**\uff1a\u8abf\u6574\u6578\u503c\uff08\u53ef\u9078\uff09\n\n**\u9375\u76e4\u5feb\u6377\u9375\uff1a**\n- **Space**\uff1a\u78ba\u8a8d/\u7e7c\u7e8c\u4e0b\u4e00\u6b65\n- **ESC**\uff1a\u6253\u958b\u9078\u55ae\n- **\u6578\u5b57\u93751-5**\uff1a\u5feb\u901f\u9078\u64c7\u88dd\u98fe\u7269\uff08\u5728\u88dd\u98fe\u968e\u6bb5\uff09\n\n### UI\u5143\u7d20\n\n**\u56fa\u5b9aUI\uff08\u59cb\u7d42\u986f\u793a\uff09\uff1a**\n- **\u9802\u90e8**\uff1a\n  - \u7576\u524d\u6b65\u9a5f\u540d\u7a31\u548c\u7de8\u865f\uff08\u5982\u300c\u6b65\u9a5f3/10\uff1a\u5012\u5165\u6a21\u5177\u300d\uff09\n  - \u9032\u5ea6\u689d\u986f\u793a\u6574\u9ad4\u5b8c\u6210\u5ea6\n- **\u53f3\u4e0a\u89d2**\uff1a\u9078\u55ae\u6309\u9215\uff08\u66ab\u505c\u3001\u8a2d\u5b9a\u3001\u91cd\u65b0\u958b\u59cb\uff09\n- **\u5e95\u90e8**\uff1a\u7576\u524d\u64cd\u4f5c\u63d0\u793a\u6587\u5b57\n\n**\u4e92\u52d5\u5143\u7d20\uff1a**\n- \u6240\u6709\u53ef\u4e92\u52d5\u7269\u54c1\u6709\u9ad8\u4eae\u908a\u6846\u6216\u767c\u5149\u6548\u679c\n- \u6309\u9215\u6700\u5c0f\u5c3a\u5bf896x96\u50cf\u7d20\uff08\u9069\u5408\u89f8\u63a7\uff09\n- \u91cd\u8981\u6309\u9215\u653e\u5728\u87a2\u5e55\u4e0b\u534a\u90e8\uff08\u62c7\u6307\u53cb\u597d\u5340\u57df\uff09\n\n### \u56de\u994b\u6a5f\u5236\n\n- **\u8996\u89ba\u56de\u994b**\uff1a\n  - \u9ede\u64ca\u6642\u6709\u6309\u9215\u6309\u4e0b\u52d5\u756b\n  - \u5b8c\u6210\u6b65\u9a5f\u6642\u6709\u52fe\u9078\u6a19\u8a18\u548c\u7c92\u5b50\u6548\u679c\n  - \u932f\u8aa4\u64cd\u4f5c\u6642\u7269\u54c1\u8f15\u5fae\u9707\u52d5\n  \n- **\u97f3\u6548\u56de\u994b**\uff1a\n  - \u9ede\u64ca\u97f3\u6548\n  - \u5b8c\u6210\u6b65\u9a5f\u7684\u6210\u529f\u97f3\u6548\n  - \u5404\u7a2e\u64cd\u4f5c\u7684\u64ec\u771f\u97f3\u6548\uff08\u652a\u62cc\u8072\u3001\u70e4\u7bb1\u8072\u7b49\uff09\n\n- **\u89f8\u89ba\u56de\u994b**\uff08\u79fb\u52d5\u7aef\uff09\uff1a\n  - \u91cd\u8981\u64cd\u4f5c\u6642\u7684\u8f15\u5fae\u9707\u52d5\n\n## Art Style and Visual Theme\n\n### \u6574\u9ad4\u98a8\u683c\n\n**\u6eab\u99a8\u6cbb\u7652\u7684\u63d2\u756b\u98a8\u683c**\n- \u67d4\u548c\u7684\u8272\u5f69\u548c\u5713\u6f64\u7684\u9020\u578b\n- \u985e\u4f3c\u624b\u7e6a\u6c34\u5f69\u7684\u8cea\u611f\n- \u7d30\u7bc0\u8c50\u5bcc\u4f46\u4e0d\u904e\u65bc\u8907\u96dc\n- \u6eab\u6696\u3001\u8212\u9069\u3001\u4ee4\u4eba\u653e\u9b06\u7684\u8996\u89ba\u9ad4\u9a57\n\n### \u8272\u5f69\u65b9\u6848\n\n**\u4e3b\u8272\u8abf\uff1a**\n- \u6eab\u6696\u7684\u7c73\u8272\u548c\u5976\u6cb9\u8272\uff08#F5E6D3, #FFF8E7\uff09\n- \u67d4\u548c\u7684\u7c89\u8272\u7cfb\uff08#FFE4E1, #FFC0CB\uff09\n- \u6dfa\u6728\u8272\uff08#D2B48C, #DEB887\uff09\n- \u67d4\u548c\u7684\u767d\u8272\uff08#FFFAF0\uff09\n\n**\u9ede\u7db4\u8272\uff1a**\n- \u8349\u8393\u7d05\uff08#FF6B6B\uff09\n- \u85cd\u8393\u85cd\uff08#4A90E2\uff09\n- \u62b9\u8336\u7da0\uff08#A8D5BA\uff09\n- \u5de7\u514b\u529b\u68d5\uff08#8B4513\uff09\n\n### \u8996\u89ba\u5143\u7d20\u8a2d\u8a08\n\n**\u5eda\u623f\u74b0\u5883\uff1a**\n- \u6728\u8cea\u7d0b\u7406\u7684\u5de5\u4f5c\u53f0\u548c\u6ae5\u6ac3\n- \u67d4\u548c\u7684\u9670\u5f71\u548c\u5149\u7dda\u6548\u679c\n- \u7a97\u6236\u900f\u9032\u7684\u81ea\u7136\u5149\n- \u80cc\u666f\u6709\u8f15\u5fae\u7684\u666f\u6df1\u6a21\u7cca\n\n**\u98df\u6750\u548c\u5de5\u5177\uff1a**\n- \u5beb\u5be6\u4f46\u7565\u5e36\u5361\u901a\u5316\u7684\u98a8\u683c\n- \u6e05\u6670\u7684\u8f2a\u5ed3\u7dda\n- \u9069\u5ea6\u7684\u9ad8\u5149\u548c\u9670\u5f71\n- \u6750\u8cea\u611f\u660e\u986f\uff08\u91d1\u5c6c\u3001\u9676\u74f7\u3001\u98df\u7269\uff09\n\n**\u86cb\u7cd5\u548c\u88dd\u98fe\uff1a**\n- \u9bae\u5976\u6cb9\u6709\u5149\u6fa4\u548c\u8cea\u611f\n- \u6c34\u679c\u8272\u5f69\u9bae\u8c54\u98fd\u6eff\n- \u86cb\u7cd5\u9ad4\u6709\u7d30\u81a9\u7684\u8cea\u5730\n- \u88dd\u98fe\u5143\u7d20\u7cbe\u7dfb\u53ef\u611b\n\n**UI\u5143\u7d20\uff1a**\n- \u5713\u89d2\u77e9\u5f62\u6309\u9215\n- \u67d4\u548c\u7684\u9670\u5f71\u548c\u6f38\u5c64\n- \u6e05\u6670\u6613\u8b80\u7684\u5b57\u9ad4\uff08\u4e2d\u6587\u4f7f\u7528\u5713\u9ad4\uff09\n- \u5716\u793a\u7c21\u6f54\u660e\u77ad\n\n### \u52d5\u756b\u98a8\u683c\n\n- \u6d41\u66a2\u7684\u7de9\u52d5\u52d5\u756b\uff08ease-in-out\uff09\n- \u7269\u54c1\u79fb\u52d5\u6709\u8f15\u5fae\u7684\u5f48\u6027\u6548\u679c\n- \u7c92\u5b50\u6548\u679c\u67d4\u548c\uff08\u661f\u661f\u3001\u611b\u5fc3\u3001\u9583\u5149\uff09\n- \u904e\u6e21\u52d5\u756b\u81ea\u7136\u4e0d\u7a81\u5140\n\n### \u8cc7\u7522\u5c3a\u5bf8\u53c3\u8003\n\n- **\u80cc\u666f\u5834\u666f**\uff1a720x1280\uff08\u5168\u87a2\u5e55\uff09\n- **\u5927\u578b\u7269\u54c1**\uff08\u652a\u62cc\u7897\u3001\u86cb\u7cd5\uff09\uff1a300-400\u50cf\u7d20\u5bec\n- **\u4e2d\u578b\u7269\u54c1**\uff08\u5de5\u5177\u3001\u6750\u6599\uff09\uff1a150-250\u50cf\u7d20\u5bec\n- **\u5c0f\u578b\u88dd\u98fe**\uff08\u6c34\u679c\u3001\u7cd6\u73e0\uff09\uff1a50-100\u50cf\u7d20\u5bec\n- **UI\u6309\u9215**\uff1a\u6700\u5c0f96x96\u50cf\u7d20\n\n### \u4e00\u81f4\u6027\u8981\u6c42\n\n- \u6240\u6709\u8cc7\u7522\u4f7f\u7528\u76f8\u540c\u7684\u5149\u6e90\u65b9\u5411\uff08\u5de6\u4e0a\u65b9\uff09\n- \u7d71\u4e00\u7684\u8f2a\u5ed3\u7dda\u7c97\u7d30\uff082-3\u50cf\u7d20\uff09\n- \u4e00\u81f4\u7684\u9670\u5f71\u98a8\u683c\uff08\u67d4\u548c\u3001\u7565\u5e36\u900f\u660e\uff09\n- \u6240\u6709\u98df\u7269\u8cc7\u7522\u4fdd\u6301\u76f8\u4f3c\u7684\u98fd\u548c\u5ea6\u548c\u4eae\u5ea6\n- \u900f\u660e\u80cc\u666f\uff08PNG\u683c\u5f0f\uff09\u7528\u65bc\u6240\u6709\u975e\u80cc\u666f\u8cc7\u7522\n\n## Audio Design\n\n### \u97f3\u6a02\n\n**\u80cc\u666f\u97f3\u6a02\uff1a**\n- \u8f15\u67d4\u7684\u92fc\u7434\u65cb\u5f8b\u914d\u5408\u5f26\u6a02\n- \u7bc0\u594f\u8212\u7de9\uff0860-80 BPM\uff09\n- \u53ef\u7121\u7e2b\u5faa\u74b0\n- \u71df\u9020\u6eab\u99a8\u3001\u653e\u9b06\u7684\u6c1b\u570d\n- \u97f3\u91cf\u8f03\u4f4e\uff0c\u4e0d\u5e72\u64fe\u904a\u6232\u9ad4\u9a57\n\n**\u7279\u6b8a\u97f3\u6a02\uff1a**\n- \u5b8c\u6210\u6176\u795d\u6642\u64ad\u653e\u300c\u751f\u65e5\u5feb\u6a02\u6b4c\u300d\u7684\u8f15\u5feb\u7248\u672c\n\n### \u97f3\u6548\n\n**\u64cd\u4f5c\u97f3\u6548\uff1a**\n- \u9ede\u64ca\u6309\u9215\uff1a\u6e05\u8106\u7684\u300c\u5494\u300d\u8072\n- \u9078\u64c7\u7269\u54c1\uff1a\u67d4\u548c\u7684\u300c\u53ee\u300d\u8072\n- \u5b8c\u6210\u6b65\u9a5f\uff1a\u6109\u6085\u7684\u300c\u53ee\u9234\u300d\u8072\u914d\u5408\u4e0a\u5347\u97f3\u968e\n\n**\u6b65\u9a5f\u97f3\u6548\uff1a**\n- \u79e4\u91cf\u6750\u6599\uff1a\u6750\u6599\u5012\u5165\u7684\u8072\u97f3\n- \u652a\u62cc\u9eb5\u7cca\uff1a\u96fb\u52d5\u652a\u62cc\u5668\u7684\u55e1\u55e1\u8072\n- \u5012\u5165\u6a21\u5177\uff1a\u6db2\u9ad4\u6d41\u52d5\u8072\n- \u70e4\u7bb1\uff1a\u70e4\u7bb1\u9580\u958b\u95dc\u8072\u3001\u70d8\u70e4\u6642\u7684\u8f15\u5fae\u55e1\u55e1\u8072\n- \u5207\u86cb\u7cd5\uff1a\u5200\u5207\u904e\u86cb\u7cd5\u7684\u8072\u97f3\n- \u6253\u767c\u9bae\u5976\u6cb9\uff1a\u652a\u62cc\u5668\u8072\u97f3\uff08\u97f3\u8abf\u96a8\u9032\u5ea6\u8b8a\u5316\uff09\n- \u5857\u62b9\u5976\u6cb9\uff1a\u67d4\u548c\u7684\u5857\u62b9\u8072\n- \u653e\u7f6e\u88dd\u98fe\uff1a\u8f15\u67d4\u7684\u300c\u5575\u300d\u8072\n\n**\u74b0\u5883\u97f3\u6548\uff1a**\n- \u5eda\u623f\u80cc\u666f\u7684\u8f15\u5fae\u74b0\u5883\u97f3\uff08\u53ef\u9078\uff09\n- \u6642\u9418\u6ef4\u7b54\u8072\uff08\u975e\u5e38\u8f15\u5fae\uff09\n\n### \u97f3\u6548\u7279\u6027\n\n- \u6240\u6709\u97f3\u6548\u90fd\u7d93\u904e\u67d4\u5316\u8655\u7406\uff0c\u907f\u514d\u523a\u8033\n- \u97f3\u6548\u9577\u5ea6\u63a7\u5236\u57280.5-2\u79d2\n- \u652f\u63f4\u97f3\u6548\u91cd\u758a\uff08\u591a\u500b\u64cd\u4f5c\u540c\u6642\u9032\u884c\uff09\n- \u97f3\u91cf\u5e73\u8861\uff0c\u4e0d\u6703\u4e92\u76f8\u5e72\u64fe", "persistent_storage_config": null, "game_metadata": {"game_id": "01M6D5MKE79DK0DKJ05SRPE8PS", "title": " Twin Brothers Coffee: Taipei Cinnamon Roll Bakery", "description": "Step into Taipei\u2019s Twin Brothers Coffee and follow its signature cinnamon rolls from kneading and filling to baking, serving with coffee, and packing to share.", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_232b16fc-c8f3-4a97-b2c5-8d27a6a5fa65.webp", "created_at": 1793590972, "updated_at": 1793710220174.5095, "art_style": null, "is_remix": true, "base_game_id": "01M52DVAYAX16ZFWG8MX54JZ30"}, "task_plan": null}