{"asset_map": {"mirror_bathroom_background": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/mirror_bathroom_background_e2ac457b-7fef-4207-b0e6-4b856b0982fb.webp", "type": "image", "prompt": "Transform the bathroom in image 1 into a festive Christmas scene. Remove all Halloween decorations like bats, pumpkins, and cobwebs. Add tasteful holiday details around the edges: warm white fairy lights, evergreen sprigs, small red and gold ornaments, and a tiny wreath. Maintain the exact vertical composition, camera angle, sink, and large central mirror. The style should be premium realistic 3D render with soft lighting, teal and cream base colors, and a cozy holiday glow. Keep the central gameplay area clear and uncluttered. High resolution, 9:16 tall image.", "description": "A polished 3D-rendered vertical bathroom background decorated for a cozy Christmas theme, preserving the layout for grooming gameplay.", "subtype": "background", "aspect_ratio": [848, 1264], "compression_suggestion": "original", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "warm_washcloth_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/warm_washcloth_icon_560c5558-b374-4c94-9a41-6d6f233b4cf1.webp", "type": "image", "prompt": "Generate a premium realistic 3D-rendered prop of a light-blue folded warm washcloth. Based on the item in image 1, transform it into a highly detailed 3D object showing soft cotton terry cloth loops and clean folded edges. Use studio-soft lighting with subtle blue fabric shading and a natural short shadow. Professional quality, high resolution, centered, isolated on a transparent background. No hands, no person, no text, no logos, no additional objects.", "description": "A premium realistic 3D-rendered light-blue folded washcloth prop with soft cotton texture.", "subtype": "ui", "aspect_ratio": [831, 820], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "shaving_cream_tube_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/shaving_cream_tube_icon_154ba167-f8e5-43b9-946a-af793530fb8d.webp", "type": "image", "prompt": "Transform the flat icon in image into a realistic 3D-rendered child-friendly prop. Create a clean, capped shaving cream tube with soft mint and cream packaging, rounded safe-looking edges, and subtle material reflections. Professional studio lighting with gentle shadows, centered, clear silhouette, high-quality 3D render style. Isolated on a transparent background, no text, no logos, no hands, no foam splashes, high resolution, game-ready asset.", "description": "A 3D-rendered, child-friendly shaving cream tube prop with soft pastel colors for the grooming game UI.", "subtype": "ui", "aspect_ratio": [510, 1263], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "rounded_practice_razor_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/rounded_practice_razor_icon_48f6e3f5-1eed-4432-bc4c-074932bb73f1.webp", "type": "image", "prompt": "Generate a professional 3D-rendered icon of a toy-like practice razor, based on the shape of image 1. The object should be made of smooth teal and pale-mint plastic with rounded corners and soft reflections. It must be a clearly SAFE BLADELESS PRACTICE TOOL with a solid rounded safety head, no exposed metal, and no sharp edges. Central composition, isometric view, isolated on a transparent background with soft studio lighting and subtle shadows. High resolution, detailed, polished game-ready asset with no hands, no text, and no logos.", "description": "A 3D-rendered, toy-like teal and pale-mint practice razor icon with a safe bladeless head.", "subtype": "ui", "aspect_ratio": [570, 1034], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "dry_towel_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/dry_towel_icon_f41c7450-4129-4286-8925-8845ae8e4eee.webp", "type": "image", "prompt": "Generate a high-resolution 3D-rendered cotton towel prop. The towel should be soft, light cream and light blue in color, featuring a realistic plush terry cloth texture with visible fibers. Show it neatly folded in layers with soft edges. Use professional studio lighting to create soft highlights and a subtle natural shadow. Centered, isolated on a transparent background, clear and readable for UI use. Maintain the identity of the towel in dry_towel_icon but transition from flat vector to a realistic 3D style. No hands, no text, no logos.", "description": "A realistic 3D cotton towel prop with plush texture for the grooming UI.", "subtype": "ui", "aspect_ratio": [1245, 842], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "gentle_moisturizer_icon": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/gentle_moisturizer_icon_2bb4570f-e3fe-4cec-ad09-09aec4b7ab3f.webp", "type": "image", "prompt": "Transform the existing gentle moisturizer icon into a realistic 3D-rendered skincare bottle prop. Generate a small, clean, rounded pump bottle with a closed dispenser. Use a tasteful unbranded cream-and-mint color palette to match the style of image 1. Include subtle plastic reflections, soft natural lighting, and a small soft shadow. The asset should be centered, professional quality, and isolated on a transparent background. Ensure no readable brand text, no logos, no people, and no hands are visible.", "description": "A realistic 3D-rendered skincare bottle prop with a pale mint and cream palette for the UI.", "subtype": "ui", "aspect_ratio": [440, 932], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "rinse_water_sfx": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/rinse_water_sfx_7fff1b74-56e3-4f08-b1a2-ec70a91c8c31.mp3", "type": "audio", "prompt": "Soft and gentle sound of clean running water trickling into a bowl, light and soothing, subtle liquid texture, no sharp splashes, no heavy rushing water, high-quality foley, child-friendly and calm atmosphere, one-shot effect with a clean fade out.", "description": "Soft, gentle trickle of clean water for a face-rinsing action in a child-friendly grooming game.", "subtype": "effect", "aspect_ratio": null, "compression_suggestion": "original", "duration_secs": 2.21, "loop": false, "opus_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/sounds/effect/rinse_water_sfx_7fff1b74-56e3-4f08-b1a2-ec70a91c8c31.ogg", "animation": null}, "character_fair_3d": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/character_fair_3d_9ec8b83b-8ddf-4222-927e-203cb65f614a.webp", "type": "image", "prompt": "Generate a premium realistic 3D-rendered character portrait cutout of a handsome 11-year-old boy with fair skin and short neat dark brown hair. He has a friendly, relaxed expression and natural child-like proportions. Front-facing head and shoulders view, straight-on camera at eye level, face centered. He is wearing a modest teal crew-neck shirt and is clean-shaven. Soft studio lighting with lifelike skin and hair textures, warm approachable educational tone. High resolution, detailed 3D model, professional quality, transparent background. No facial hair, no nudity, no text, no background.", "description": "A premium 3D-rendered portrait of a fair-skinned 11-year-old boy for a children's grooming game.", "subtype": "character", "aspect_ratio": [947, 975], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "character_light_3d": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/character_light_3d_6d2b1707-b0a0-483d-9890-596c0c914d67.webp", "type": "image", "prompt": "Create a premium realistic 3D-rendered character portrait cutout. Depict an original handsome-looking 11-year-old boy with light skin, short neat dark brown hair, friendly relaxed expression, and natural proportions. Front-facing head and shoulders, wearing a fully covering modest teal crew-neck shirt. He is clean-shaven. Straight-on camera at eye level, face centered, shoulders visible, transparent background, soft studio lighting. Match the exact facial features, hairstyle, clothing, pose, and lighting of image 1, only changing the skin tone to a light complexion. Professional 3D render, lifelike skin and hair textures, warm approachable educational tone, high resolution, detailed, transparent background.", "description": "A premium 3D-rendered portrait of a light-skinned 11-year-old boy, maintaining consistency with other character variants for the grooming game.", "subtype": "character", "aspect_ratio": [993, 1023], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "character_medium_3d": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/character_medium_3d_79e20296-ee7e-4138-8bda-1f47f2d9c27f.webp", "type": "image", "prompt": "Create a premium realistic 3D-rendered character portrait cutout of an original handsome-looking 11-year-old boy with medium warm-brown skin. He has short neat dark brown hair, a friendly relaxed expression, and natural proportions. Front-facing head and shoulders, wearing a fully covering modest teal crew-neck shirt. He is clean-shaven. Straight-on camera at eye level, face centered, shoulders visible, transparent background. Soft studio lighting, lifelike skin and hair materials with a warm approachable educational tone. Maintain identical hairstyle, facial features, shirt, pose, and framing as seen in image 1 and image 2, changing only the skin tone to medium warm-brown. No razor, no shaving action, no nudity, no facial hair, no text, no logos.", "description": "Medium skin tone variant of the 11-year-old boy character for the shaving-care game.", "subtype": "character", "aspect_ratio": [991, 1023], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "character_tan_3d": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/character_tan_3d_4911ddaa-193f-48e8-978a-43a533e3877d.webp", "type": "image", "prompt": "Create a premium realistic 3D-rendered character portrait cutout. Depict an original handsome-looking 11-year-old boy, clearly a child, with tan golden-brown skin, short neat dark brown hair, friendly relaxed expression, natural proportions, front-facing head and shoulders, wearing a fully covering modest teal crew-neck shirt. He is clean-shaven. Straight-on camera at eye level, face centered, shoulders visible, transparent background, soft studio lighting, lifelike skin and hair materials with a warm approachable educational tone. Maintain the exact same hairstyle, facial features, shirt, pose, framing, and lighting as image 1, image 2, and image 3, changing only the skin tone to tan golden-brown. No razor, no shaving action, no nudity, no sexualization, no facial hair, no text, no logos.", "description": "A premium realistic 3D-rendered portrait of an 11-year-old boy with tan skin for a child-friendly grooming game.", "subtype": "character", "aspect_ratio": [991, 1023], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}, "character_deep_3d": {"url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/character_deep_3d_efc9cddc-bf03-4c05-b5f3-664fa7b19fce.webp", "type": "image", "prompt": "Create a premium realistic 3D-rendered character portrait cutout for a gentle, non-blade shaving-care learning game for children. Depict an original handsome-looking 11-year-old boy, clearly a child, with deep brown skin, short neat dark brown hair, friendly relaxed expression, natural proportions, front-facing head and shoulders, wearing a fully covering modest teal crew-neck shirt. He is clean-shaven. Straight-on camera at eye level, face centered, shoulders visible, transparent background, soft studio lighting, lifelike skin and hair materials but warm approachable educational tone. Match the hairstyle, facial features, shirt, pose, framing, and lighting of image 1, only changing the skin tone to deep brown. No razor, no shaving action, no nudity, no sexualization, no facial hair, no text, no logos.", "description": "A premium realistic 3D-rendered portrait of an 11-year-old boy with deep brown skin for a grooming simulation game.", "subtype": "character", "aspect_ratio": [993, 1023], "compression_suggestion": "medium", "duration_secs": null, "loop": null, "opus_url": null, "animation": null}}, "game_code": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n  <title>Shave Tutor \u2014 Gentle Practice Routine</title>\n  <style>\n    /* Reset and fixed-iframe stage */\n    :root {\n      --ink: #24434b;\n      --ink-strong: #19363e;\n      --teal: #28787a;\n      --teal-dark: #1d6265;\n      --mint: #dff2e9;\n      --cream: #fffdf7;\n      --peach: #f7e4d7;\n      --gold: #f0b760;\n      --border: #c9dfdc;\n      --transition-fast: 150ms;\n      --transition-normal: 300ms;\n      --ease-out: cubic-bezier(0, 0, 0.2, 1);\n      --ease-bounce: cubic-bezier(0.68, -0.6, 0.32, 1.6);\n    }\n\n    * {\n      box-sizing: border-box;\n    }\n\n    html,\n    body {\n      width: 100%;\n      height: 100%;\n      margin: 0;\n      padding: 0;\n      overflow: hidden;\n      touch-action: none;\n      -ms-touch-action: none;\n      background: #edf6f5;\n      color: var(--ink);\n      font-family: \"Poppins\", \"Inter\", sans-serif;\n      -webkit-tap-highlight-color: transparent;\n      user-select: none;\n      -webkit-user-select: none;\n    }\n\n    #game-world {\n      position: relative;\n      width: 100%;\n      height: 100%;\n      overflow: hidden;\n      background: #edf6f5;\n      touch-action: none;\n    }\n\n    #game-canvas {\n      position: absolute;\n      inset: 0;\n      display: block;\n      width: 100%;\n      height: 100%;\n      touch-action: none;\n    }\n\n    /* Interactive overlay: the drawing remains click-through */\n    .ui-overlay {\n      position: absolute;\n      inset: 0;\n      pointer-events: none;\n    }\n\n    .ui-overlay > * {\n      pointer-events: auto;\n    }\n\n    button,\n    select {\n      font-family: \"Poppins\", \"Inter\", sans-serif;\n    }\n\n    button {\n      border: 0;\n      cursor: pointer;\n      -webkit-tap-highlight-color: transparent;\n    }\n\n    .clean-button,\n    .cloth-button {\n      position: absolute;\n      top: 1016px;\n      display: flex;\n      align-items: center;\n      text-align: left;\n      border-radius: 24px;\n      box-shadow: 0 8px 18px rgba(27, 69, 72, 0.15);\n      transition:\n        transform var(--transition-fast) var(--ease-out),\n        opacity var(--transition-fast) var(--ease-out),\n        box-shadow var(--transition-fast) var(--ease-out),\n        background-color var(--transition-fast) var(--ease-out);\n      touch-action: none;\n    }\n\n    .clean-button {\n      left: 48px;\n      width: 264px;\n      height: 144px;\n      gap: 12px;\n      padding: 14px;\n      color: #ffffff;\n      background: var(--teal);\n      border: 3px solid #1d6b6d;\n    }\n\n    .cloth-button {\n      left: 332px;\n      width: 340px;\n      height: 144px;\n      gap: 12px;\n      padding: 14px;\n      color: var(--ink-strong);\n      background: #fff7e7;\n      border: 3px solid #dfbd7e;\n      cursor: grab;\n    }\n\n    .stage-tool-button {\n      top: 1078px;\n      width: 308px;\n      height: 88px;\n      cursor: grab;\n      touch-action: none;\n    }\n\n    .clean-button, .cloth-button { top: 1078px; height: 88px; }\n    .clean-button { left: 34px; width: 310px; }\n    .cloth-button:not(.stage-tool-button) { left: 376px; width: 310px; }\n\n    .step-ribbon {\n      position: absolute;\n      top: 986px;\n      left: 30px;\n      display: flex;\n      width: 660px;\n      height: 80px;\n      gap: 6px;\n      pointer-events: auto;\n    }\n\n    .ribbon-step {\n      flex: 1 1 0;\n      min-width: 0;\n      height: 80px;\n      padding: 5px 3px;\n      border: 2px solid #bdd7ce;\n      border-radius: 15px;\n      color: #31565a;\n      background: #f5fcf8;\n      font-size: 17px;\n      line-height: 1.08;\n      font-weight: 700;\n      touch-action: manipulation;\n    }\n    .ribbon-step[aria-current=\"step\"] { color: #fff; background: #28787a; border-color: #1d6265; }\n    .ribbon-step.is-complete:not([aria-current=\"step\"]) { background: #e4f4e9; border-color: #86b99b; }\n    .ribbon-step:focus-visible, .mute-button:focus-visible { outline: 4px solid #f0ae46; outline-offset: 3px; }\n\n    .mute-button {\n      position: absolute;\n      top: 326px;\n      right: 42px;\n      width: 126px;\n      height: 58px;\n      border: 2px solid #9fc9c3;\n      border-radius: 18px;\n      color: #24434b;\n      background: #f7fff9;\n      font-size: 18px;\n      font-weight: 700;\n      touch-action: manipulation;\n    }\n\n    .action-button {\n      position: absolute;\n      top: 1078px;\n      left: 206px;\n      display: flex;\n      width: 308px;\n      height: 88px;\n      align-items: center;\n      gap: 14px;\n      padding: 12px 18px;\n      border: 3px solid #9fc9c3;\n      border-radius: 22px;\n      color: #24434b;\n      background: #f1fbf6;\n      box-shadow: 0 8px 18px rgba(27, 69, 72, 0.15);\n      font-size: 21px;\n      font-weight: 700;\n      touch-action: manipulation;\n    }\n\n    .action-button:disabled { opacity: .58; cursor: not-allowed; box-shadow: none; }\n    .action-button[hidden], .step-ribbon[hidden], .mute-button[hidden] { display: none !important; }\n    .action-emoji { font-size: 34px; width: 48px; flex: 0 0 48px; }\n    .action-copy { display: flex; flex-direction: column; gap: 3px; text-align: left; }\n    .action-caption { font-size: 15px; font-weight: 500; }\n\n    #towel-button, #moisturizer-button { cursor: grab; touch-action: none; }\n    #towel-preview-image, #moisturizer-preview-image { width: 62px; height: 60px; object-fit: contain; pointer-events:none; }\n\n    #cream-button, #towel-button { left: 40px;\n      border-color: #b5d9cc;\n      background: #f0fbf5;\n    }\n\n    #razor-button, #moisturizer-button {\n      left: 372px;\n      width: 308px;\n      border-color: #9fc9c3;\n      background: #edf8f5;\n    }\n\n    .clean-button:hover:not(:disabled),\n    .cloth-button:hover:not(:disabled) {\n      transform: translateY(-3px);\n      box-shadow: 0 12px 22px rgba(27, 69, 72, 0.2);\n    }\n\n    .clean-button:active:not(:disabled),\n    .cloth-button:active:not(:disabled) {\n      transform: translateY(1px) scale(0.985);\n      box-shadow: 0 4px 10px rgba(27, 69, 72, 0.16);\n    }\n\n    .clean-button:focus-visible,\n    .cloth-button:focus-visible,\n    .editor-panel select:focus-visible {\n      outline: 5px solid #f0ae46;\n      outline-offset: 5px;\n    }\n\n    .clean-button:disabled,\n    .cloth-button:disabled,\n    .step-nav-button:disabled {\n      cursor: not-allowed;\n      opacity: 0.56;\n      transform: none;\n      box-shadow: none;\n    }\n\n    .step-nav-button {\n      position: absolute;\n      top: 1172px;\n      display: flex;\n      width: 260px;\n      height: 88px;\n      align-items: center;\n      justify-content: center;\n      border: 3px solid #b9d5cc;\n      border-radius: 24px;\n      color: var(--ink-strong);\n      background: #f7fff9;\n      box-shadow: 0 7px 16px rgba(27, 69, 72, 0.13);\n      font-size: 25px;\n      font-weight: 700;\n      touch-action: manipulation;\n    }\n\n    .step-nav-button:hover:not(:disabled) {\n      background: #eaf7f0;\n      transform: translateY(-2px);\n    }\n\n    .step-nav-button:focus-visible {\n      outline: 5px solid #f0ae46;\n      outline-offset: 5px;\n    }\n\n    #back-button { left: 40px; }\n    #next-button {\n      right: 40px;\n      border-color: #1d6b6d;\n      color: #ffffff;\n      background: var(--teal);\n    }\n\n    #next-button:hover:not(:disabled) { background: var(--teal-dark); }\n\n    .cloth-button[aria-disabled=\"true\"] {\n      border-style: dashed;\n      background: #fff9ee;\n    }\n\n    .cloth-button.is-dragging {\n      opacity: 0.16;\n      transform: none;\n    }\n\n    .control-icon {\n      display: flex;\n      flex: 0 0 56px;\n      align-items: center;\n      justify-content: center;\n      width: 56px;\n      height: 56px;\n      border-radius: 18px;\n      color: var(--teal-dark);\n      background: #d8f0e7;\n      font-size: 32px;\n      font-weight: 700;\n      line-height: 1;\n    }\n\n    .control-copy {\n      display: flex;\n      min-width: 0;\n      flex-direction: column;\n      gap: 6px;\n    }\n\n    .control-title {\n      font-size: 24px;\n      font-weight: 700;\n      line-height: 1.1;\n      white-space: nowrap;\n    }\n\n    .control-caption {\n      font-size: 18px;\n      font-weight: 500;\n      line-height: 1.15;\n    }\n\n    .cloth-swatch {\n      display: block;\n      flex: 0 0 80px;\n      width: 80px;\n      height: 72px;\n      object-fit: contain;\n      border-radius: 14px;\n      background: transparent;\n      pointer-events: none;\n    }\n\n    /* In-frame creator editor */\n    .editor-panel {\n      position: absolute;\n      top: 984px;\n      left: 32px;\n      display: flex;\n      width: 656px;\n      height: 272px;\n      flex-direction: column;\n      padding: 16px 20px;\n      border: 2px solid var(--border);\n      border-radius: 24px;\n      background: rgba(255, 253, 247, 0.98);\n      box-shadow: 0 10px 24px rgba(28, 68, 72, 0.17);\n      color: var(--ink);\n    }\n\n    .editor-heading {\n      display: flex;\n      min-height: 30px;\n      align-items: center;\n      justify-content: space-between;\n      gap: 12px;\n      color: var(--ink-strong);\n      font-size: 22px;\n      font-weight: 700;\n    }\n\n    .editor-heading small {\n      color: #527178;\n      font-size: 18px;\n      font-weight: 500;\n    }\n\n    .editor-fields {\n      display: grid;\n      grid-template-columns: 1fr 1fr;\n      gap: 16px;\n      margin-top: 8px;\n    }\n\n    .editor-field {\n      display: flex;\n      min-width: 0;\n      flex-direction: column;\n      gap: 4px;\n      color: #34545a;\n      font-size: 20px;\n      font-weight: 600;\n    }\n\n    .editor-panel select {\n      width: 100%;\n      height: 96px;\n      padding: 12px 14px;\n      border: 2px solid #9dbfba;\n      border-radius: 16px;\n      color: var(--ink-strong);\n      background: #ffffff;\n      font-size: 22px;\n      font-weight: 600;\n      touch-action: manipulation;\n      transition:\n        border-color var(--transition-fast) var(--ease-out),\n        background-color var(--transition-fast) var(--ease-out);\n    }\n\n    .editor-panel select:hover:not(:disabled) {\n      border-color: var(--teal);\n      background: #f6fffb;\n    }\n\n    .editor-panel select:active:not(:disabled) {\n      background: #eaf6f1;\n    }\n\n    .editor-panel select:disabled {\n      cursor: not-allowed;\n      opacity: 0.55;\n    }\n\n    .editor-help {\n      margin: 8px 0 0;\n      color: #49676c;\n      font-size: 18px;\n      line-height: 1.25;\n    }\n\n    .sr-only {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      overflow: hidden;\n      clip: rect(0, 0, 0, 0);\n      white-space: nowrap;\n      clip-path: inset(50%);\n    }\n\n    [hidden] {\n      display: none !important;\n    }\n\n    /* Give the remaining interface a softly modeled, tactile 3D finish. */\n    .clean-button,\n    .cloth-button,\n    .action-button,\n    .step-nav-button,\n    .ribbon-step,\n    .mute-button {\n      transform-style: preserve-3d;\n      background-image: linear-gradient(155deg, rgba(255, 255, 255, 0.36) 0%, rgba(255, 255, 255, 0.08) 36%, rgba(25, 54, 62, 0.06) 100%);\n      box-shadow:\n        inset 0 3px 0 rgba(255, 255, 255, 0.72),\n        inset 0 -5px 0 rgba(30, 66, 70, 0.12),\n        0 7px 0 rgba(36, 67, 75, 0.12),\n        0 14px 22px rgba(27, 69, 72, 0.18);\n    }\n\n    .clean-button,\n    .cloth-button,\n    .action-button,\n    .step-nav-button {\n      transform: perspective(900px) rotateX(2deg);\n    }\n\n    .clean-button:hover:not(:disabled),\n    .cloth-button:hover:not(:disabled),\n    .action-button:hover:not(:disabled),\n    .step-nav-button:hover:not(:disabled) {\n      transform: perspective(900px) rotateX(2deg) translateY(-3px);\n      box-shadow:\n        inset 0 3px 0 rgba(255, 255, 255, 0.78),\n        inset 0 -5px 0 rgba(30, 66, 70, 0.12),\n        0 9px 0 rgba(36, 67, 75, 0.12),\n        0 18px 25px rgba(27, 69, 72, 0.2);\n    }\n\n    .clean-button:active:not(:disabled),\n    .cloth-button:active:not(:disabled),\n    .action-button:active:not(:disabled),\n    .step-nav-button:active:not(:disabled) {\n      transform: perspective(900px) rotateX(1deg) translateY(3px) scale(0.985);\n      box-shadow:\n        inset 0 2px 5px rgba(30, 66, 70, 0.16),\n        0 2px 0 rgba(36, 67, 75, 0.1),\n        0 5px 9px rgba(27, 69, 72, 0.14);\n    }\n\n    .control-icon {\n      background: linear-gradient(145deg, #f5fff9, #c6e8dc 76%, #b5d9cd);\n      box-shadow: inset 0 2px 3px rgba(255, 255, 255, 0.9), 0 4px 7px rgba(33, 85, 78, 0.18);\n    }\n\n    .control-title,\n    .action-copy > span:first-child {\n      text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);\n    }\n\n    .cloth-swatch,\n    #towel-preview-image,\n    #moisturizer-preview-image {\n      filter: drop-shadow(0 5px 5px rgba(32, 64, 68, 0.2));\n    }\n\n    .editor-panel {\n      background-image: linear-gradient(150deg, rgba(255, 255, 255, 0.72), rgba(255, 253, 247, 0.96) 45%, rgba(215, 235, 226, 0.28));\n      box-shadow: inset 0 3px 0 rgba(255, 255, 255, 0.9), 0 9px 0 rgba(36, 67, 75, 0.08), 0 16px 28px rgba(28, 68, 72, 0.18);\n    }\n\n    .editor-panel select {\n      background-image: linear-gradient(180deg, #ffffff, #f4faf7 72%, #e8f3ee);\n      box-shadow: inset 0 2px 3px rgba(31, 72, 76, 0.08), 0 4px 0 rgba(36, 67, 75, 0.08);\n    }\n\n    .ribbon-step[aria-current=\"step\"] {\n      background-image: linear-gradient(145deg, #59a5a0, #28787a 58%, #1d6265);\n      box-shadow: inset 0 3px 0 rgba(255, 255, 255, 0.42), inset 0 -4px 0 rgba(18, 74, 77, 0.34), 0 5px 0 rgba(36, 67, 75, 0.12), 0 9px 14px rgba(27, 69, 72, 0.18);\n    }\n\n    .ribbon-step.is-complete:not([aria-current=\"step\"]) {\n      background-image: linear-gradient(145deg, #ffffff, #e4f4e9 66%, #d3eadb);\n    }\n\n    /* Cozy Christmas palette complements the polished 3D-rendered bathroom and props. */\n    body, #game-world { background: radial-gradient(ellipse at 50% 10%, #fffdf4 0%, #edf6ef 58%, #dcebe4 100%); }\n    .clean-button, #next-button { background-color: #347d59; border-color: #205c40; color: #fffdf8; }\n    .clean-button:hover:not(:disabled), #next-button:hover:not(:disabled) { background-color: #286b4b; }\n    .cloth-button, .action-button, .step-nav-button, .mute-button {\n      border-color: #b8d0c1;\n      background-color: #f4faf5;\n    }\n    .cloth-button { background-image: linear-gradient(145deg, #fffdf4, #edf6ee 72%, #d6e8db); }\n    .step-nav-button { background-image: linear-gradient(145deg, #fffdf4, #edf6ee 72%, #d6e8db); }\n    .ribbon-step { border-color: #c7dacb; background-color: #fffdf4; }\n    .ribbon-step[aria-current=\"step\"] { background-color: #347d59; border-color: #205c40; background-image: linear-gradient(145deg, #73ad7e, #347d59 58%, #205c40); }\n    .ribbon-step.is-complete:not([aria-current=\"step\"]) { background-color: #fff3d9; border-color: #e6c27f; background-image: linear-gradient(145deg, #fffdf4, #fff1d5 70%, #f1dbad); }\n    .practice-timer {\n      position: absolute; top: 326px; left: 42px; z-index: 2;\n      width: 286px; min-height: 58px; padding: 8px 13px 9px;\n      border: 2px solid #c85152; border-radius: 18px;\n      color: #294d3b; background: linear-gradient(145deg, #fffdf4, #edf6ee 75%, #dcebe0);\n      box-shadow: inset 0 2px 0 rgba(255,255,255,.92), 0 4px 0 rgba(89,49,111,.12), 0 9px 18px rgba(55,39,65,.18);\n      font-size: 15px; font-weight: 800; letter-spacing: .1px; pointer-events: none;\n    }\n    .timer-track { height: 8px; margin-top: 5px; overflow: hidden; border-radius: 99px; background: #d9e8dc; box-shadow: inset 0 1px 2px rgba(42,79,56,.18); }\n    #timer-fill { display: block; width: 0%; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #69ad75, #c85152, #dfb457); transition: width 250ms linear; }\n\n    @media (prefers-reduced-motion: reduce) {\n      *,\n      *::before,\n      *::after {\n        scroll-behavior: auto !important;\n        transition-duration: 0.01ms !important;\n        animation-duration: 0.01ms !important;\n      }\n    }\n  </style>\n</head>\n<body>\n  <main id=\"game-world\" aria-label=\"Shave Tutor warm-water preparation practice\">\n    <canvas\n      id=\"game-canvas\"\n      role=\"img\"\n      aria-label=\"A friendly character in a bathroom mirror with two highlighted cheek areas\"\n    ></canvas>\n\n    <div class=\"ui-overlay\">\n      <button\n        id=\"clean-button\"\n        class=\"clean-button\"\n        type=\"button\"\n        aria-pressed=\"false\"\n        aria-label=\"Clean Hands. Activate before using the cloth.\"\n      >\n        <span class=\"control-icon\" aria-hidden=\"true\">\u2713</span>\n        <span class=\"control-copy\">\n          <span id=\"clean-title\" class=\"control-title\">Clean Hands</span>\n          <span id=\"clean-caption\" class=\"control-caption\">Tap once to begin</span>\n        </span>\n      </button>\n\n      <button\n        id=\"cloth-button\"\n        class=\"cloth-button\"\n        type=\"button\"\n        aria-disabled=\"true\"\n        aria-label=\"Warm cloth. After cleaning your hands, drag across both highlighted cheeks. Keyboard: press Enter to select, use arrow keys to move, and press Enter to wipe.\"\n      >\n        <img id=\"cloth-preview-image\" class=\"cloth-swatch\" alt=\"\" aria-hidden=\"true\">\n        <span class=\"control-copy\">\n          <span class=\"control-title\">Warm Cloth</span>\n          <span class=\"control-caption\">Drag over both cheeks</span>\n        </span>\n      </button>\n\n      <button id=\"cream-button\" class=\"cloth-button stage-tool-button\" type=\"button\" aria-label=\"Pretend shaving cream. Drag across both cheeks and chin, or press Enter to cover the next zone.\" hidden>\n        <img id=\"cream-preview-image\" class=\"cloth-swatch\" alt=\"\" aria-hidden=\"true\">\n        <span class=\"control-copy\">\n          <span class=\"control-title\">Pretend Cream</span>\n          <span class=\"control-caption\">Spread over 3 zones</span>\n        </span>\n      </button>\n\n      <button id=\"razor-button\" class=\"cloth-button stage-tool-button\" type=\"button\" aria-label=\"Rounded pretend practice tool. Drag gently along the arrows, or press Enter for the next guided stroke.\" hidden>\n        <img id=\"razor-preview-image\" class=\"cloth-swatch\" alt=\"\" aria-hidden=\"true\">\n        <span class=\"control-copy\">\n          <span class=\"control-title\">Practice Tool</span>\n          <span class=\"control-caption\">Follow each short arrow</span>\n        </span>\n      </button>\n\n      <nav id=\"step-ribbon\" class=\"step-ribbon\" aria-label=\"Five-step routine\" hidden>\n        <button class=\"ribbon-step\" type=\"button\" data-step=\"0\">1<br>Prep</button>\n        <button class=\"ribbon-step\" type=\"button\" data-step=\"1\">2<br>Cream</button>\n        <button class=\"ribbon-step\" type=\"button\" data-step=\"2\">3<br>Practice</button>\n        <button class=\"ribbon-step\" type=\"button\" data-step=\"3\">4<br>Aftercare</button>\n        <button class=\"ribbon-step\" type=\"button\" data-step=\"4\">5<br>Put away</button>\n      </nav>\n      <button id=\"mute-button\" class=\"mute-button\" type=\"button\" aria-pressed=\"false\" aria-label=\"Mute game sounds\">Sound: On</button>\n      <div id=\"practice-timer\" class=\"practice-timer\" aria-live=\"off\" aria-label=\"Minimum one-minute self-paced practice\">\n        <span id=\"practice-timer-label\">\ud83c\udf19 Cozy practice \u00b7 00:00 / 01:00</span>\n        <div class=\"timer-track\" aria-hidden=\"true\"><span id=\"timer-fill\"></span></div>\n      </div>\n\n      <button id=\"rinse-button\" class=\"action-button\" type=\"button\" hidden>\n        <span class=\"action-emoji\" aria-hidden=\"true\">\ud83d\udca7</span><span class=\"action-copy\"><span>Rinse face</span><span id=\"rinse-caption\" class=\"action-caption\">Rinse away the pretend foam</span></span>\n      </button>\n      <button id=\"towel-button\" class=\"cloth-button stage-tool-button\" type=\"button\" aria-label=\"Clean folded towel. Drag gently across cheeks and chin to pat dry.\" hidden>\n        <img id=\"towel-preview-image\" class=\"cloth-swatch\" alt=\"\" aria-hidden=\"true\"><span class=\"control-copy\"><span class=\"control-title\">Soft Towel</span><span id=\"towel-caption\" class=\"control-caption\">Pat dry gently</span></span>\n      </button>\n      <button id=\"moisturizer-button\" class=\"cloth-button stage-tool-button\" type=\"button\" aria-label=\"Gentle moisturizer. Drag over cheeks and chin after drying.\" hidden>\n        <img id=\"moisturizer-preview-image\" class=\"cloth-swatch\" alt=\"\" aria-hidden=\"true\"><span class=\"control-copy\"><span class=\"control-title\">Moisturizer</span><span id=\"moisturizer-caption\" class=\"control-caption\">A small dab on each zone</span></span>\n      </button>\n      <button id=\"store-button\" class=\"action-button\" type=\"button\" hidden>\n        <span class=\"action-emoji\" aria-hidden=\"true\">\ud83e\uddfa</span><span class=\"action-copy\"><span id=\"store-title\">Put tool away</span><span id=\"store-caption\" class=\"action-caption\">Keep practice tools clean and stored safely</span></span>\n      </button>\n\n      <section id=\"editor-panel\" class=\"editor-panel\" aria-label=\"Appearance editor\" hidden>\n        <div class=\"editor-heading\">\n          <span>Appearance preview</span>\n          <small>Edit mode</small>\n        </div>\n        <div class=\"editor-fields\">\n          <label class=\"editor-field\" for=\"skin-tone-select\">\n            <span>Skin tone</span>\n            <select id=\"skin-tone-select\" aria-label=\"Choose a skin-tone preset\">\n              <option value=\"fair\">Fair</option>\n              <option value=\"light\">Light</option>\n              <option value=\"medium\">Medium</option>\n              <option value=\"tan\">Tan</option>\n              <option value=\"deep\">Deep</option>\n            </select>\n          </label>\n          <label class=\"editor-field\" for=\"facial-hair-select\">\n            <span>Facial hair</span>\n            <select id=\"facial-hair-select\" aria-label=\"Choose facial-hair visibility\">\n              <option value=\"none\">None</option>\n              <option value=\"light\">Light</option>\n              <option value=\"sparse\">Sparse</option>\n            </select>\n          </label>\n        </div>\n        <p class=\"editor-help\">Drag the character in the mirror to nudge the preview.</p>\n      </section>\n\n      <button id=\"back-button\" class=\"step-nav-button\" type=\"button\" aria-label=\"Go back to the previous practice stage\">\u2190 Back</button>\n      <button id=\"next-button\" class=\"step-nav-button\" type=\"button\" aria-label=\"Continue to the next practice stage\">Next \u2192</button>\n      <div id=\"live-region\" class=\"sr-only\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n    </div>\n  </main>\n\n  <script>\n    /*\n      Shave Tutor \u2014 Warm-water preparation\n      This first, self-contained practice step teaches hand hygiene and a gentle\n      warm-cloth sweep over both cheeks. The character and cloth are drawn with\n      code. There is no real razor, scoring, countdown, data storage, or network use.\n    */\n\n    const GAME_WIDTH = 720;\n    const GAME_HEIGHT = 1280;\n\n    const SKIN_PRESETS = {\n      fair: {\n        label: \"Fair\",\n        base: \"#F7DDCC\",\n        shade: \"#EFC3AA\",\n        outline: \"#C88A6E\"\n      },\n      light: {\n        label: \"Light\",\n        base: \"#F0C5A6\",\n        shade: \"#DEA987\",\n        outline: \"#B97859\"\n      },\n      medium: {\n        label: \"Medium\",\n        base: \"#D99B74\",\n        shade: \"#C2815D\",\n        outline: \"#965D42\"\n      },\n      tan: {\n        label: \"Tan\",\n        base: \"#BC805B\",\n        shade: \"#A86B4B\",\n        outline: \"#794A35\"\n      },\n      deep: {\n        label: \"Deep\",\n        base: \"#895B47\",\n        shade: \"#754A3A\",\n        outline: \"#59382F\"\n      }\n    };\n\n    const FACIAL_HAIR_PRESETS = [\"none\", \"light\", \"sparse\"];\n    let activeSession = null;\n    let activeCharacterPortrait = null;\n\n    function is3DPortraitReady() {\n      return !!(activeCharacterPortrait && activeCharacterPortrait.complete && activeCharacterPortrait.naturalWidth > 0);\n    }\n\n    function clamp(value, min, max) {\n      return Math.max(min, Math.min(max, value));\n    }\n\n    function prepareGameConfig() {\n      if (!window.gameConfig || typeof window.gameConfig !== \"object\") {\n        window.gameConfig = {};\n      }\n\n      const config = window.gameConfig;\n\n      if (!config.appearance || typeof config.appearance !== \"object\") {\n        config.appearance = {};\n      }\n\n      if (!SKIN_PRESETS[config.appearance.skinTone]) {\n        config.appearance.skinTone = \"light\";\n      }\n\n      if (!FACIAL_HAIR_PRESETS.includes(config.appearance.facialHair)) {\n        config.appearance.facialHair = \"none\";\n      }\n\n      if (!config.settings || typeof config.settings !== \"object\") {\n        config.settings = {};\n      }\n      if (typeof config.settings.showExplanationCards !== \"boolean\") config.settings.showExplanationCards = true;\n      if (!config.settings.captions || typeof config.settings.captions !== \"object\") config.settings.captions = {};\n      const defaultCaptions = {\n        prep: \"Warm\u2014not hot. Gently prepare both cheeks.\",\n        cream: \"Spread pretend cream over both cheeks and chin.\",\n        practice: \"Follow short arrows with gentle pretend strokes.\",\n        rinse: \"Rinse away the pretend foam with comfortable water.\",\n        dry: \"Pat each area dry; do not rub.\",\n        moisturize: \"Use a small amount and spread it gently.\",\n        store: \"Put the clean practice tool away safely.\"\n      };\n      Object.keys(defaultCaptions).forEach(function (key) {\n        if (typeof config.settings.captions[key] !== \"string\") config.settings.captions[key] = defaultCaptions[key];\n      });\n\n      const creamOpacity = Number(config.settings.creamOpacity);\n      config.settings.creamOpacity = Number.isFinite(creamOpacity)\n        ? clamp(creamOpacity, 0.35, 0.9)\n        : 0.72;\n\n      const gestureAssistance = Number(config.settings.gestureAssistance);\n      config.settings.gestureAssistance = Number.isFinite(gestureAssistance)\n        ? clamp(gestureAssistance, 0, 1)\n        : 0.65;\n\n      if (!config.scene || typeof config.scene !== \"object\") {\n        config.scene = {};\n      }\n\n      const offsetX = Number(config.scene.characterOffsetX);\n      const offsetY = Number(config.scene.characterOffsetY);\n\n      config.scene.characterOffsetX = Number.isFinite(offsetX)\n        ? clamp(offsetX, -28, 28)\n        : 0;\n      config.scene.characterOffsetY = Number.isFinite(offsetY)\n        ? clamp(offsetY, -18, 18)\n        : 0;\n\n      return config;\n    }\n\n    function getCharacterOffset() {\n      const scene = window.gameConfig.scene;\n      const x = Number(scene.characterOffsetX);\n      const y = Number(scene.characterOffsetY);\n\n      return {\n        x: Number.isFinite(x) ? clamp(x, -28, 28) : 0,\n        y: Number.isFinite(y) ? clamp(y, -18, 18) : 0\n      };\n    }\n\n    function getCheekZones() {\n      const offset = getCharacterOffset();\n      const centerX = 360 + offset.x;\n      const centerY = 565 + offset.y + 47;\n\n      const portraitOffsetX = is3DPortraitReady() ? 60 : 72;\n      const portraitOffsetY = is3DPortraitReady() ? 27 : 47;\n      return {\n        left: { x: centerX - portraitOffsetX, y: centerY + portraitOffsetY, radius: 64 },\n        right: { x: centerX + portraitOffsetX, y: centerY + portraitOffsetY, radius: 64 }\n      };\n    }\n\n    function getFaceZones() {\n      const cheeks = getCheekZones();\n      const offset = getCharacterOffset();\n      const centerX = 360 + offset.x;\n      const centerY = 565 + offset.y;\n      return {\n        left: cheeks.left,\n        right: cheeks.right,\n        chin: { x: centerX, y: centerY + (is3DPortraitReady() ? 100 : 139), radius: 54 }\n      };\n    }\n\n    function getGuidedPaths() {\n      const offset = getCharacterOffset();\n      const x = 360 + offset.x;\n      const y = 565 + offset.y;\n      if (is3DPortraitReady()) {\n        return [\n          { start: { x: x - 83, y: y + 5 }, end: { x: x - 55, y: y + 43 }, side: \"left cheek\" },\n          { start: { x: x - 59, y: y + 9 }, end: { x: x - 34, y: y + 46 }, side: \"left cheek\" },\n          { start: { x: x + 83, y: y + 5 }, end: { x: x + 55, y: y + 43 }, side: \"right cheek\" },\n          { start: { x: x + 59, y: y + 9 }, end: { x: x + 34, y: y + 46 }, side: \"right cheek\" },\n          { start: { x: x - 16, y: y + 82 }, end: { x: x - 16, y: y + 115 }, side: \"chin\" },\n          { start: { x: x + 16, y: y + 82 }, end: { x: x + 16, y: y + 115 }, side: \"chin\" }\n        ];\n      }\n      return [\n        { start: { x: x - 112, y: y + 20 }, end: { x: x - 74, y: y + 65 }, side: \"left cheek\" },\n        { start: { x: x - 78, y: y + 25 }, end: { x: x - 48, y: y + 70 }, side: \"left cheek\" },\n        { start: { x: x + 112, y: y + 20 }, end: { x: x + 74, y: y + 65 }, side: \"right cheek\" },\n        { start: { x: x + 78, y: y + 25 }, end: { x: x + 48, y: y + 70 }, side: \"right cheek\" },\n        { start: { x: x - 27, y: y + 112 }, end: { x: x - 27, y: y + 153 }, side: \"chin\" },\n        { start: { x: x + 27, y: y + 112 }, end: { x: x + 27, y: y + 153 }, side: \"chin\" }\n      ];\n    }\n\n    function roundedPath(ctx, x, y, width, height, radius) {\n      const r = Math.min(radius, width / 2, height / 2);\n      ctx.beginPath();\n      ctx.moveTo(x + r, y);\n      ctx.arcTo(x + width, y, x + width, y + height, r);\n      ctx.arcTo(x + width, y + height, x, y + height, r);\n      ctx.arcTo(x, y + height, x, y, r);\n      ctx.arcTo(x, y, x + width, y, r);\n      ctx.closePath();\n    }\n\n    function drawPanel(ctx, x, y, width, height, radius, fill, stroke) {\n      /* A layered, softly beveled surface gives each card a consistent 3D finish. */\n      ctx.save();\n      ctx.shadowColor = \"rgba(20, 48, 49, 0.26)\";\n      ctx.shadowBlur = 24;\n      ctx.shadowOffsetY = 13;\n      roundedPath(ctx, x, y + 4, width, height, radius);\n      ctx.fillStyle = \"rgba(24, 50, 53, 0.16)\";\n      ctx.fill();\n\n      ctx.shadowColor = \"rgba(31, 72, 76, 0.08)\";\n      ctx.shadowBlur = 8;\n      ctx.shadowOffsetY = 2;\n      roundedPath(ctx, x, y, width, height, radius);\n      const surface = ctx.createLinearGradient(x, y, x + width * 0.12, y + height);\n      surface.addColorStop(0, \"rgba(255, 255, 255, 0.58)\");\n      surface.addColorStop(0.12, fill);\n      surface.addColorStop(0.68, fill);\n      surface.addColorStop(0.94, \"rgba(36, 67, 75, 0.035)\");\n      surface.addColorStop(1, \"rgba(36, 67, 75, 0.13)\");\n      ctx.fillStyle = surface;\n      ctx.fill();\n\n      ctx.shadowColor = \"transparent\";\n      ctx.shadowBlur = 0;\n      ctx.shadowOffsetY = 0;\n      /* Gloss across the upper bevel and a narrow shaded lower rim add material depth. */\n      ctx.save();\n      roundedPath(ctx, x + 2, y + 2, width - 4, height - 4, Math.max(2, radius - 2));\n      ctx.clip();\n      const gloss = ctx.createLinearGradient(x, y, x, y + Math.min(36, height * 0.24));\n      gloss.addColorStop(0, \"rgba(255,255,255,0.34)\");\n      gloss.addColorStop(0.68, \"rgba(255,255,255,0.08)\");\n      gloss.addColorStop(1, \"rgba(255,255,255,0)\");\n      ctx.fillStyle = gloss;\n      ctx.fillRect(x, y, width, Math.min(36, height * 0.24));\n      const lowerRim = ctx.createLinearGradient(x, y + height - 12, x, y + height);\n      lowerRim.addColorStop(0, \"rgba(36,67,75,0)\");\n      lowerRim.addColorStop(1, \"rgba(36,67,75,0.12)\");\n      ctx.fillStyle = lowerRim;\n      ctx.fillRect(x, y + height - 12, width, 12);\n      ctx.restore();\n\n      if (stroke) {\n        ctx.lineWidth = 2;\n        ctx.strokeStyle = stroke;\n        roundedPath(ctx, x, y, width, height, radius);\n        ctx.stroke();\n      }\n      ctx.lineWidth = 2;\n      ctx.strokeStyle = \"rgba(255, 255, 255, 0.66)\";\n      roundedPath(ctx, x + 2, y + 2, width - 4, height - 4, Math.max(2, radius - 2));\n      ctx.stroke();\n      ctx.restore();\n    }\n\n    function drawText(ctx, text, x, y, size, color, weight, align) {\n      ctx.save();\n      ctx.font = (weight || 500) + \" \" + size + 'px \"Poppins\", \"Inter\", sans-serif';\n      ctx.textAlign = align || \"left\";\n      ctx.textBaseline = \"middle\";\n      ctx.fillStyle = color;\n      ctx.fillText(text, x, y);\n      ctx.restore();\n    }\n\n    function drawCheck(ctx, x, y, size, color) {\n      ctx.save();\n      ctx.lineWidth = Math.max(4, size * 0.16);\n      ctx.lineCap = \"round\";\n      ctx.lineJoin = \"round\";\n      ctx.strokeStyle = color;\n      ctx.beginPath();\n      ctx.moveTo(x - size * 0.45, y);\n      ctx.lineTo(x - size * 0.12, y + size * 0.32);\n      ctx.lineTo(x + size * 0.48, y - size * 0.38);\n      ctx.stroke();\n      ctx.restore();\n    }\n\n    /* Little celebratory sparkles make careful practice feel rewarding, without scores. */\n    function burstFeedback(state, x, y, color) {\n      if (!state.feedbackParticles) state.feedbackParticles = [];\n      for (let i = 0; i < 7; i++) {\n        const angle = Math.PI * 2 * i / 7 + Math.random() * 0.22;\n        const speed = 28 + Math.random() * 42;\n        state.feedbackParticles.push({ x: x, y: y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 18, age: 0, life: 0.65 + Math.random() * 0.35, size: 4 + Math.random() * 5, color: color || \"#f3a83d\" });\n      }\n      if (state.feedbackParticles.length > 84) state.feedbackParticles.splice(0, state.feedbackParticles.length - 84);\n    }\n\n    function drawFeedbackEffects(ctx, state) {\n      (state.feedbackParticles || []).forEach(function (particle) {\n        const alpha = Math.max(0, 1 - particle.age / particle.life);\n        const x = particle.x + particle.vx * particle.age;\n        const y = particle.y + particle.vy * particle.age + 25 * particle.age * particle.age;\n        ctx.save(); ctx.globalAlpha = alpha; ctx.shadowColor = particle.color; ctx.shadowBlur = 10; ctx.fillStyle = particle.color;\n        ctx.beginPath(); ctx.moveTo(x, y - particle.size); ctx.lineTo(x + particle.size * .38, y - particle.size * .38); ctx.lineTo(x + particle.size, y); ctx.lineTo(x + particle.size * .38, y + particle.size * .38); ctx.lineTo(x, y + particle.size); ctx.lineTo(x - particle.size * .38, y + particle.size * .38); ctx.lineTo(x - particle.size, y); ctx.lineTo(x - particle.size * .38, y - particle.size * .38); ctx.closePath(); ctx.fill(); ctx.restore();\n      });\n    }\n\n    function drawBackground(ctx) {\n      const gradient = ctx.createLinearGradient(0, 0, 0, GAME_HEIGHT);\n      gradient.addColorStop(0, \"#eff8f6\");\n      gradient.addColorStop(1, \"#e6f1f0\");\n      ctx.fillStyle = gradient;\n      ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);\n\n      ctx.save();\n      ctx.globalAlpha = 0.42;\n      ctx.fillStyle = \"#d4e9e5\";\n      [\n        [16, 370, 7], [704, 415, 8], [20, 832, 6], [698, 790, 6],\n        [86, 902, 4], [640, 906, 5]\n      ].forEach(function (dot) {\n        ctx.beginPath();\n        ctx.arc(dot[0], dot[1], dot[2], 0, Math.PI * 2);\n        ctx.fill();\n      });\n      ctx.restore();\n    }\n\n    function drawHalloweenDecorations(ctx, clock) {\n      ctx.save();\n      /* Moon, tiny stars, and friendly bats dress the mirror without covering the lesson. */\n      ctx.globalAlpha = 0.92;\n      const moon = ctx.createLinearGradient(612, 36, 666, 96);\n      moon.addColorStop(0, \"#fff8da\"); moon.addColorStop(1, \"#f4bd64\");\n      ctx.fillStyle = moon;\n      ctx.shadowColor = \"rgba(227, 153, 65, .35)\"; ctx.shadowBlur = 15;\n      ctx.beginPath(); ctx.arc(649, 72, 26, 0, Math.PI * 2); ctx.fill();\n      ctx.shadowBlur = 0; ctx.globalCompositeOperation = \"destination-out\";\n      ctx.beginPath(); ctx.arc(660, 62, 24, 0, Math.PI * 2); ctx.fill();\n      ctx.globalCompositeOperation = \"source-over\";\n      ctx.fillStyle = \"#f0a83f\";\n      [[600, 48, 3], [683, 91, 3], [589, 91, 2.5], [626, 112, 2]].forEach(function (star, i) {\n        const twinkle = 0.65 + 0.35 * Math.sin(clock * 2 + i);\n        ctx.globalAlpha = twinkle; ctx.beginPath(); ctx.arc(star[0], star[1], star[2], 0, Math.PI * 2); ctx.fill();\n      });\n      function bat(x, y, scale, phase) {\n        const flap = Math.sin(clock * 3 + phase) * 4;\n        ctx.save(); ctx.translate(x, y); ctx.scale(scale, scale); ctx.fillStyle = \"#594365\";\n        ctx.beginPath(); ctx.ellipse(0, 0, 7, 10, 0, 0, Math.PI * 2); ctx.fill();\n        ctx.beginPath(); ctx.moveTo(-4, -1); ctx.quadraticCurveTo(-14, -14 - flap, -23, -7); ctx.quadraticCurveTo(-20, 1, -11, 5); ctx.lineTo(-6, 10); ctx.closePath(); ctx.fill();\n        ctx.beginPath(); ctx.moveTo(4, -1); ctx.quadraticCurveTo(14, -14 - flap, 23, -7); ctx.quadraticCurveTo(20, 1, 11, 5); ctx.lineTo(6, 10); ctx.closePath(); ctx.fill();\n        ctx.restore();\n      }\n      bat(552, 93, 0.75, 0); bat(682, 292, 0.62, 1.7);\n      function pumpkin(x, y, scale) {\n        ctx.save(); ctx.translate(x, y); ctx.scale(scale, scale);\n        ctx.shadowColor = \"rgba(76, 48, 29, .22)\"; ctx.shadowBlur = 8; ctx.shadowOffsetY = 4;\n        const g = ctx.createRadialGradient(-8, -11, 2, 0, 0, 34);\n        g.addColorStop(0, \"#ffbe63\"); g.addColorStop(1, \"#e8782f\"); ctx.fillStyle = g;\n        [-15, 0, 15].forEach(function (dx, i) { ctx.beginPath(); ctx.ellipse(dx, 0, i === 1 ? 13 : 12, 23, 0, 0, Math.PI * 2); ctx.fill(); });\n        ctx.shadowColor = \"transparent\"; ctx.fillStyle = \"#557a50\"; roundedPath(ctx, -4, -30, 8, 13, 4); ctx.fill();\n        ctx.fillStyle = \"#54324e\"; ctx.beginPath(); ctx.moveTo(-12, -4); ctx.lineTo(-6, 4); ctx.lineTo(0, -4); ctx.lineTo(6, 4); ctx.lineTo(12, -4); ctx.quadraticCurveTo(8, 11, 0, 12); ctx.quadraticCurveTo(-9, 10, -12, -4); ctx.fill();\n        ctx.restore();\n      }\n      pumpkin(82, 811, 0.78); pumpkin(638, 811, 0.78);\n      ctx.restore();\n    }\n\n    function drawSpookyBathroomAccents(ctx, clock) {\n      ctx.save();\n      /* Festive framing: keep the central face, prompts, and touch controls clear. */\n      function cobweb(x, y, direction) {\n        ctx.save(); ctx.translate(x, y); ctx.scale(direction, 1);\n        ctx.strokeStyle = \"rgba(111, 78, 128, 0.46)\"; ctx.lineWidth = 2;\n        for (let ray = 0; ray < 5; ray++) {\n          const angle = ray * Math.PI / 8;\n          ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.cos(angle) * 56, Math.sin(angle) * 56); ctx.stroke();\n        }\n        for (let ring = 1; ring <= 4; ring++) {\n          const radius = ring * 12;\n          ctx.beginPath();\n          for (let ray = 0; ray <= 4; ray++) {\n            const angle = ray * Math.PI / 8;\n            const px = Math.cos(angle) * radius;\n            const py = Math.sin(angle) * radius;\n            if (ray === 0) ctx.moveTo(px, py); else ctx.quadraticCurveTo(px - 2, py - 2, px, py);\n          }\n          ctx.stroke();\n        }\n        ctx.restore();\n      }\n      cobweb(38, 344, 1); cobweb(682, 344, -1);\n\n      function tinyGhost(x, y, scale, phase) {\n        const bob = Math.sin(clock * 1.4 + phase) * 4;\n        ctx.save(); ctx.translate(x, y + bob); ctx.scale(scale, scale);\n        ctx.shadowColor = \"rgba(91, 61, 112, .24)\"; ctx.shadowBlur = 10; ctx.shadowOffsetY = 4;\n        ctx.fillStyle = \"rgba(255, 250, 242, .96)\";\n        ctx.beginPath(); ctx.moveTo(-24, 22); ctx.lineTo(-24, -4);\n        ctx.bezierCurveTo(-24, -31, 24, -31, 24, -4); ctx.lineTo(24, 22);\n        ctx.quadraticCurveTo(16, 14, 8, 22); ctx.quadraticCurveTo(0, 14, -8, 22);\n        ctx.quadraticCurveTo(-16, 14, -24, 22); ctx.closePath(); ctx.fill();\n        ctx.shadowColor = \"transparent\"; ctx.fillStyle = \"#594365\";\n        ctx.beginPath(); ctx.arc(-8, -5, 2.7, 0, Math.PI * 2); ctx.arc(8, -5, 2.7, 0, Math.PI * 2); ctx.fill();\n        ctx.beginPath(); ctx.arc(0, 4, 2, 0, Math.PI * 2); ctx.fill();\n        ctx.restore();\n      }\n      tinyGhost(80, 438, .78, 0.4); tinyGhost(640, 443, .72, 2.1);\n\n      function autumnLeaf(x, y, rotation, color) {\n        ctx.save(); ctx.translate(x, y); ctx.rotate(rotation);\n        ctx.fillStyle = color; ctx.globalAlpha = .82;\n        ctx.beginPath(); ctx.moveTo(0, -10); ctx.quadraticCurveTo(16, -8, 12, 6);\n        ctx.quadraticCurveTo(4, 12, 0, 18); ctx.quadraticCurveTo(-4, 10, -12, 6);\n        ctx.quadraticCurveTo(-16, -8, 0, -10); ctx.fill();\n        ctx.strokeStyle = \"rgba(103, 67, 52, .48)\"; ctx.lineWidth = 1.5;\n        ctx.beginPath(); ctx.moveTo(0, -7); ctx.lineTo(0, 15); ctx.stroke(); ctx.restore();\n      }\n      autumnLeaf(113, 356, -.65, \"#d9843c\"); autumnLeaf(607, 356, .7, \"#b6769a\");\n      autumnLeaf(58, 850, .35, \"#e8a34c\"); autumnLeaf(663, 846, -.45, \"#9a6fb0\");\n\n      /* A softly glowing tiny pumpkin lantern at each mirror-foot corner. */\n      function lantern(x, y, scale) {\n        ctx.save(); ctx.translate(x, y); ctx.scale(scale, scale);\n        ctx.shadowColor = \"rgba(247, 145, 47, .45)\"; ctx.shadowBlur = 17;\n        const glow = ctx.createRadialGradient(-5, -6, 2, 0, 0, 25);\n        glow.addColorStop(0, \"#ffd37a\"); glow.addColorStop(1, \"#e8782f\"); ctx.fillStyle = glow;\n        [-9, 0, 9].forEach(function (dx, i) { ctx.beginPath(); ctx.ellipse(dx, 0, i === 1 ? 10 : 8, 15, 0, 0, Math.PI * 2); ctx.fill(); });\n        ctx.shadowBlur = 0; ctx.fillStyle = \"#52794f\"; roundedPath(ctx, -3, -21, 6, 9, 3); ctx.fill();\n        ctx.fillStyle = \"#54324e\"; ctx.beginPath(); ctx.moveTo(-9, -2); ctx.lineTo(-4, 3); ctx.lineTo(0, -2); ctx.lineTo(4, 3); ctx.lineTo(9, -2); ctx.quadraticCurveTo(6, 8, 0, 8); ctx.quadraticCurveTo(-7, 7, -9, -2); ctx.fill();\n        ctx.restore();\n      }\n      lantern(42, 857, .72); lantern(678, 857, .72);\n      ctx.restore();\n    }\n\n    function drawHeader(ctx, state) {\n      const titles = [\"Warm-water prep\", \"Apply gentle cream\", \"Practice light strokes\", \"Gentle aftercare\", \"Put tools away\"];\n      const subtitles = [\"A gentle, no-blade face-care practice.\", \"Cover the cheeks and chin with pretend lather.\", \"Trace short arrows with a rounded practice tool.\", \"Rinse, pat dry, then moisturize at your own pace.\", \"Store the practice tool safely, then revisit any step.\"];\n      const stageTitle = state.stage >= 0 && state.stage <= 4 ? titles[state.stage] : \"A calm routine recap\";\n      const stageSubtitle = state.stage >= 0 && state.stage <= 4 ? subtitles[state.stage] : \"Practice any step again whenever you like.\";\n      drawText(ctx, \"SHAVE TUTOR  \u2022  PRETEND PRACTICE\", 44, 34, 18, \"#347477\", 700);\n      drawText(ctx, stageTitle, 42, 77, 42, \"#1E4047\", 700);\n      drawText(ctx, stageSubtitle, 44, 116, 21, \"#49686D\", 500);\n    }\n\n    function drawSafetyCard(ctx) {\n      drawPanel(ctx, 32, 142, 656, 178, 24, \"#fffdf8\", \"#d8e8df\");\n\n      ctx.save();\n      ctx.fillStyle = \"#e2f2e9\";\n      ctx.beginPath();\n      ctx.arc(55, 169, 13, 0, Math.PI * 2);\n      ctx.fill();\n      drawCheck(ctx, 55, 169, 12, \"#28787a\");\n      ctx.restore();\n\n      drawText(ctx, \"SAFETY FIRST\", 78, 169, 18, \"#28787a\", 700);\n      drawText(ctx, \"Shaving is optional; hair grows at different times.\", 54, 205, 20, \"#29464d\", 600);\n      drawText(ctx, \"For any real shave, ask an adult and use clean tools.\", 54, 241, 20, \"#29464d\", 600);\n      drawText(ctx, \"Stop if skin feels sore or irritated; tell an adult.\", 54, 277, 20, \"#29464d\", 600);\n    }\n\n    function drawMirrorBase(ctx) {\n      drawPanel(ctx, 32, 336, 656, 532, 30, \"#d4e8e5\", \"#b8d2cf\");\n\n      ctx.save();\n      roundedPath(ctx, 48, 352, 624, 500, 24);\n      ctx.clip();\n\n      const mirrorGradient = ctx.createLinearGradient(48, 352, 672, 852);\n      mirrorGradient.addColorStop(0, \"#f9fffc\");\n      mirrorGradient.addColorStop(0.5, \"#e8f5f1\");\n      mirrorGradient.addColorStop(1, \"#d9ece8\");\n      ctx.fillStyle = mirrorGradient;\n      ctx.fillRect(48, 352, 624, 500);\n\n      ctx.globalAlpha = 0.28;\n      ctx.fillStyle = \"#ffffff\";\n      ctx.beginPath();\n      ctx.moveTo(72, 372);\n      ctx.lineTo(112, 372);\n      ctx.lineTo(244, 842);\n      ctx.lineTo(204, 842);\n      ctx.closePath();\n      ctx.fill();\n      ctx.restore();\n\n      ctx.save();\n      ctx.fillStyle = \"#7eaaa5\";\n      ctx.beginPath();\n      ctx.arc(632, 376, 5, 0, Math.PI * 2);\n      ctx.arc(650, 376, 5, 0, Math.PI * 2);\n      ctx.fill();\n      ctx.restore();\n    }\n\n    function drawFacialHair(ctx, centerX, centerY, visibility) {\n      if (visibility === \"none\") return;\n\n      const sparseDots = [\n        [-31, 119, 2.1], [-18, 126, 2.3], [-5, 122, 2.0], [10, 127, 2.2],\n        [24, 120, 2.1], [-42, 132, 1.8], [-29, 139, 2.0], [-14, 145, 2.2],\n        [2, 141, 2.0], [18, 145, 2.0], [34, 136, 1.8], [-37, 151, 1.8],\n        [-21, 157, 2.0], [-4, 153, 2.1], [13, 158, 2.0], [30, 151, 1.8],\n        [-51, 141, 1.6], [47, 143, 1.6], [-9, 169, 1.8], [8, 168, 1.8],\n        [-27, 169, 1.6], [27, 167, 1.6]\n      ];\n      const lightDots = sparseDots.filter(function (_, index) {\n        return index % 2 === 0;\n      });\n\n      ctx.save();\n      ctx.fillStyle = \"#67483f\";\n      ctx.globalAlpha = visibility === \"sparse\" ? 0.42 : 0.25;\n      (visibility === \"sparse\" ? sparseDots : lightDots).forEach(function (dot) {\n        ctx.beginPath();\n        ctx.arc(centerX + dot[0], centerY + dot[1], dot[2], 0, Math.PI * 2);\n        ctx.fill();\n      });\n      ctx.restore();\n    }\n\n    function drawCheekZone(ctx, zone, cleaned, handsClean, clock) {\n      ctx.save();\n\n      if (cleaned) {\n        const pulse = 0.88 + 0.12 * Math.sin(clock * 2.2);\n        ctx.globalAlpha = pulse;\n        ctx.shadowColor = \"rgba(62, 171, 128, 0.62)\";\n        ctx.shadowBlur = 24;\n        const cleanSurface = ctx.createRadialGradient(zone.x - 14, zone.y - 12, 3, zone.x, zone.y, 66);\n        cleanSurface.addColorStop(0, \"rgba(232,255,239,0.86)\");\n        cleanSurface.addColorStop(0.68, \"rgba(127,220,177,0.54)\");\n        cleanSurface.addColorStop(1, \"rgba(67,160,120,0.22)\");\n        ctx.fillStyle = cleanSurface;\n        ctx.beginPath();\n        ctx.ellipse(zone.x, zone.y, 56, 41, 0, 0, Math.PI * 2);\n        ctx.fill();\n\n        ctx.shadowBlur = 0;\n        ctx.lineWidth = 4;\n        ctx.strokeStyle = \"#278768\";\n        ctx.beginPath();\n        ctx.ellipse(zone.x, zone.y, 55, 40, 0, 0, Math.PI * 2);\n        ctx.stroke();\n        ctx.lineWidth = 2;\n        ctx.strokeStyle = \"rgba(255,255,255,0.78)\";\n        ctx.beginPath();\n        ctx.ellipse(zone.x - 1, zone.y - 2, 51, 36, 0, Math.PI * 1.12, Math.PI * 1.88);\n        ctx.stroke();\n        drawCheck(ctx, zone.x + 29, zone.y - 20, 20, \"#176e53\");\n      } else if (handsClean) {\n        const targetSurface = ctx.createRadialGradient(zone.x - 13, zone.y - 12, 2, zone.x, zone.y, 66);\n        targetSurface.addColorStop(0, \"rgba(231,255,242,0.50)\");\n        targetSurface.addColorStop(1, \"rgba(112,201,162,0.14)\");\n        ctx.fillStyle = targetSurface;\n        ctx.strokeStyle = \"#438f78\";\n        ctx.lineWidth = 3;\n        ctx.beginPath();\n        ctx.ellipse(zone.x, zone.y, 55, 40, 0, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.setLineDash([]);\n        ctx.stroke();\n        ctx.lineWidth = 2;\n        ctx.strokeStyle = \"rgba(255,255,255,0.72)\";\n        ctx.beginPath();\n        ctx.ellipse(zone.x - 1, zone.y - 2, 51, 36, 0, Math.PI * 1.12, Math.PI * 1.88);\n        ctx.stroke();\n      } else {\n        const targetSurface = ctx.createRadialGradient(zone.x - 13, zone.y - 12, 2, zone.x, zone.y, 66);\n        targetSurface.addColorStop(0, \"rgba(255,249,226,0.50)\");\n        targetSurface.addColorStop(1, \"rgba(255,194,103,0.17)\");\n        ctx.fillStyle = targetSurface;\n        ctx.strokeStyle = \"#a66c32\";\n        ctx.lineWidth = 3;\n        ctx.setLineDash([8, 7]);\n        ctx.beginPath();\n        ctx.ellipse(zone.x, zone.y, 55, 40, 0, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.stroke();\n      }\n\n      ctx.restore();\n    }\n\n    function drawCharacter(ctx, state, clock, assets) {\n      const offset = getCharacterOffset();\n      const centerX = 360 + offset.x;\n      const centerY = 565 + offset.y;\n      const skin = SKIN_PRESETS[window.gameConfig.appearance.skinTone] || SKIN_PRESETS.light;\n      const zones = getCheekZones();\n      const portrait = assets && assets.characters && assets.characters[window.gameConfig.appearance.skinTone];\n\n      /* Use the matching realistic 3D portrait when available; retain the original\n         canvas portrait as a resilient fallback while assets load or are unavailable. */\n      if (portrait && portrait.complete && portrait.naturalWidth > 0) {\n        ctx.save();\n        const portraitBoxWidth = 408;\n        const portraitBoxHeight = 482;\n        const portraitScale = Math.min(portraitBoxWidth / portrait.naturalWidth, portraitBoxHeight / portrait.naturalHeight);\n        const portraitWidth = portrait.naturalWidth * portraitScale;\n        const portraitHeight = portrait.naturalHeight * portraitScale;\n        /* The portrait art is a real 3D render; a soft cast shadow seats it in the mirror. */\n        ctx.shadowColor = \"rgba(21, 39, 43, 0.28)\";\n        ctx.shadowBlur = 19;\n        ctx.shadowOffsetX = 0;\n        ctx.shadowOffsetY = 11;\n        ctx.drawImage(\n          portrait,\n          centerX - portraitWidth / 2,\n          centerY - 226 + (portraitBoxHeight - portraitHeight) / 2,\n          portraitWidth,\n          portraitHeight\n        );\n        ctx.shadowColor = \"transparent\";\n        ctx.shadowBlur = 0;\n        ctx.shadowOffsetY = 0;\n\n        drawFacialHair(ctx, centerX, centerY, window.gameConfig.appearance.facialHair);\n        if (state.stage === 0) {\n          drawCheekZone(ctx, zones.left, state.cleaned.left, state.handsClean, clock);\n          drawCheekZone(ctx, zones.right, state.cleaned.right, state.handsClean, clock);\n        } else {\n          drawPracticeFaceOverlays(ctx, state, centerX, centerY);\n        }\n        ctx.restore();\n        return { centerX: centerX, centerY: centerY };\n      }\n\n      ctx.save();\n\n      /* Neck and fully clothed shoulders */\n      ctx.fillStyle = skin.shade;\n      roundedPath(ctx, centerX - 43, centerY + 126, 86, 105, 26);\n      ctx.fill();\n\n      ctx.fillStyle = \"#75b6b2\";\n      ctx.beginPath();\n      ctx.moveTo(centerX - 186, 852);\n      ctx.lineTo(centerX - 174, 808);\n      ctx.quadraticCurveTo(centerX - 155, 763, centerX - 93, 752);\n      ctx.lineTo(centerX - 49, 768);\n      ctx.lineTo(centerX + 49, 768);\n      ctx.lineTo(centerX + 93, 752);\n      ctx.quadraticCurveTo(centerX + 155, 763, centerX + 174, 808);\n      ctx.lineTo(centerX + 186, 852);\n      ctx.closePath();\n      ctx.fill();\n\n      ctx.strokeStyle = \"rgba(35, 105, 105, 0.30)\";\n      ctx.lineWidth = 3;\n      ctx.beginPath();\n      ctx.moveTo(centerX - 50, 773);\n      ctx.lineTo(centerX, 816);\n      ctx.lineTo(centerX + 50, 773);\n      ctx.stroke();\n\n      /* Hair silhouette, ears, and face */\n      ctx.fillStyle = \"#51434a\";\n      ctx.beginPath();\n      ctx.ellipse(centerX, centerY - 1, 157, 194, 0, 0, Math.PI * 2);\n      ctx.fill();\n\n      ctx.fillStyle = skin.base;\n      ctx.beginPath();\n      ctx.ellipse(centerX - 139, centerY + 13, 25, 38, 0, 0, Math.PI * 2);\n      ctx.ellipse(centerX + 139, centerY + 13, 25, 38, 0, 0, Math.PI * 2);\n      ctx.fill();\n\n      ctx.fillStyle = skin.base;\n      ctx.strokeStyle = skin.outline;\n      ctx.lineWidth = 2;\n      ctx.beginPath();\n      ctx.ellipse(centerX, centerY + 3, 142, 183, 0, 0, Math.PI * 2);\n      ctx.fill();\n      ctx.stroke();\n\n      /* Soft, code-drawn hair cap */\n      ctx.fillStyle = \"#51434a\";\n      ctx.beginPath();\n      ctx.moveTo(centerX - 144, centerY - 74);\n      ctx.bezierCurveTo(centerX - 158, centerY - 154, centerX - 102, centerY - 196, centerX - 34, centerY - 192);\n      ctx.bezierCurveTo(centerX + 34, centerY - 210, centerX + 125, centerY - 177, centerX + 143, centerY - 94);\n      ctx.lineTo(centerX + 122, centerY - 80);\n      ctx.quadraticCurveTo(centerX + 83, centerY - 108, centerX + 48, centerY - 87);\n      ctx.quadraticCurveTo(centerX + 6, centerY - 63, centerX - 31, centerY - 91);\n      ctx.quadraticCurveTo(centerX - 82, centerY - 59, centerX - 115, centerY - 84);\n      ctx.closePath();\n      ctx.fill();\n\n      ctx.strokeStyle = \"rgba(255, 255, 255, 0.20)\";\n      ctx.lineWidth = 3;\n      ctx.lineCap = \"round\";\n      ctx.beginPath();\n      ctx.moveTo(centerX - 96, centerY - 155);\n      ctx.quadraticCurveTo(centerX - 54, centerY - 184, centerX - 22, centerY - 179);\n      ctx.moveTo(centerX + 27, centerY - 183);\n      ctx.quadraticCurveTo(centerX + 76, centerY - 176, centerX + 98, centerY - 145);\n      ctx.stroke();\n\n      /* Brows and friendly eyes */\n      ctx.strokeStyle = \"#604840\";\n      ctx.lineWidth = 5;\n      ctx.lineCap = \"round\";\n      ctx.beginPath();\n      ctx.moveTo(centerX - 65, centerY - 48);\n      ctx.quadraticCurveTo(centerX - 45, centerY - 58, centerX - 25, centerY - 49);\n      ctx.moveTo(centerX + 25, centerY - 49);\n      ctx.quadraticCurveTo(centerX + 45, centerY - 58, centerX + 65, centerY - 48);\n      ctx.stroke();\n\n      [-43, 43].forEach(function (eyeOffset) {\n        ctx.fillStyle = \"#fffdf8\";\n        ctx.beginPath();\n        ctx.ellipse(centerX + eyeOffset, centerY - 27, 13, 15, 0, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.fillStyle = \"#33454b\";\n        ctx.beginPath();\n        ctx.arc(centerX + eyeOffset, centerY - 25, 6, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.fillStyle = \"#ffffff\";\n        ctx.beginPath();\n        ctx.arc(centerX + eyeOffset + 2, centerY - 28, 2, 0, Math.PI * 2);\n        ctx.fill();\n      });\n\n      /* Nose and calm smile */\n      ctx.strokeStyle = skin.outline;\n      ctx.lineWidth = 3;\n      ctx.lineCap = \"round\";\n      ctx.beginPath();\n      ctx.moveTo(centerX + 1, centerY - 3);\n      ctx.quadraticCurveTo(centerX + 13, centerY + 22, centerX + 2, centerY + 31);\n      ctx.stroke();\n\n      ctx.strokeStyle = \"#8c514e\";\n      ctx.lineWidth = 4;\n      ctx.beginPath();\n      ctx.moveTo(centerX - 35, centerY + 91);\n      ctx.quadraticCurveTo(centerX, centerY + 121, centerX + 35, centerY + 91);\n      ctx.stroke();\n\n      drawFacialHair(ctx, centerX, centerY, window.gameConfig.appearance.facialHair);\n\n      if (state.stage === 0) {\n        drawCheekZone(ctx, zones.left, state.cleaned.left, state.handsClean, clock);\n        drawCheekZone(ctx, zones.right, state.cleaned.right, state.handsClean, clock);\n      } else {\n        drawPracticeFaceOverlays(ctx, state, centerX, centerY);\n      }\n\n      ctx.restore();\n      return { centerX: centerX, centerY: centerY };\n    }\n\n    function drawPracticeFaceOverlays(ctx, state, centerX, centerY) {\n      const zones = getFaceZones();\n      if (state.stage >= 3) {\n        [\"left\", \"right\", \"chin\"].forEach(function (name) {\n          const zone = zones[name];\n          const isDry = state.dried[name];\n          const isMoisturized = state.moisturized[name];\n          ctx.save();\n          if (state.stage === 3 && !state.rinsed) {\n            ctx.globalAlpha = 0.62;\n            ctx.fillStyle = \"#fffdf4\";\n            ctx.beginPath(); ctx.ellipse(zone.x, zone.y, zone.radius * 0.76, zone.radius * 0.52, 0, 0, Math.PI * 2); ctx.fill();\n            ctx.globalAlpha = 1;\n          } else if (isMoisturized) {\n            ctx.fillStyle = \"rgba(145, 213, 179, 0.32)\";\n            ctx.strokeStyle = \"#64a982\"; ctx.lineWidth = 3;\n            ctx.beginPath(); ctx.ellipse(zone.x, zone.y, zone.radius * 0.76, zone.radius * 0.52, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();\n            drawCheck(ctx, zone.x + 18, zone.y - 9, 16, \"#28785b\");\n          } else if (isDry) {\n            ctx.strokeStyle = \"#72a99a\"; ctx.lineWidth = 3;\n            ctx.beginPath(); ctx.arc(zone.x, zone.y, 14, 0.2, Math.PI * 1.75); ctx.stroke();\n          }\n          if (state.stage === 3 && state.rinsed && !state.dried[name]) {\n            ctx.strokeStyle = \"#4b9bb1\"; ctx.lineWidth = 3; ctx.setLineDash([5, 6]);\n            ctx.beginPath(); ctx.ellipse(zone.x, zone.y, zone.radius * 0.78, zone.radius * 0.54, 0, 0, Math.PI * 2); ctx.stroke();\n          }\n          if (state.stage === 3 && Object.values(state.dried).every(Boolean) && !state.moisturized[name]) {\n            ctx.strokeStyle = \"#7db993\"; ctx.lineWidth = 3; ctx.setLineDash([5, 6]);\n            ctx.beginPath(); ctx.ellipse(zone.x, zone.y, zone.radius * 0.78, zone.radius * 0.54, 0, 0, Math.PI * 2); ctx.stroke();\n          }\n          ctx.restore();\n        });\n        if (state.stage === 3 && state.rinseFlash > 0) {\n          ctx.save(); ctx.globalAlpha = Math.min(0.75, state.rinseFlash * 0.72); ctx.lineCap = \"round\";\n          [[-83, 25], [-61, 54], [-20, 12], [18, 38], [58, 18], [83, 49], [2, 104]].forEach(function (drop, index) {\n            const x = centerX + drop[0], y = centerY + drop[1] + ((state.clock * 95 + index * 31) % 94);\n            const gradient = ctx.createLinearGradient(x - 5, y - 12, x + 4, y + 12);\n            gradient.addColorStop(0, \"#e8ffff\"); gradient.addColorStop(1, \"#58b8d2\"); ctx.strokeStyle = gradient; ctx.lineWidth = 5;\n            ctx.beginPath(); ctx.moveTo(x, y - 9); ctx.quadraticCurveTo(x + 5, y, x, y + 8); ctx.stroke();\n          });\n          ctx.restore();\n        }\n        return;\n      }\n      const opacityValue = Number(window.gameConfig.settings.creamOpacity);\n      const opacity = clamp(Number.isFinite(opacityValue) ? opacityValue : 0.72, 0.35, 0.9);\n      const assistanceValue = Number(window.gameConfig.settings.gestureAssistance);\n      const assistance = clamp(Number.isFinite(assistanceValue) ? assistanceValue : 0.65, 0, 1);\n      const isPractice = state.stage === 2;\n      const foamColor = \"#fffdf4\";\n\n      [\"left\", \"right\", \"chin\"].forEach(function (name) {\n        const zone = zones[name];\n        const applied = state.cream[name];\n        ctx.save();\n        if (!applied && !isPractice) {\n          ctx.fillStyle = \"rgba(112, 201, 162, 0.13)\";\n          ctx.strokeStyle = \"#438f78\";\n          ctx.lineWidth = 3;\n          ctx.setLineDash([8, 7]);\n          ctx.beginPath();\n          ctx.ellipse(zone.x, zone.y, zone.radius * 0.88, zone.radius * 0.65, 0, 0, Math.PI * 2);\n          ctx.fill();\n          ctx.stroke();\n        }\n        if (applied) {\n          ctx.globalAlpha = opacity;\n          const lather = ctx.createRadialGradient(zone.x - 17, zone.y - 18, 4, zone.x, zone.y, zone.radius * 0.9);\n          lather.addColorStop(0, \"#ffffff\"); lather.addColorStop(0.72, foamColor); lather.addColorStop(1, \"#e7eee8\");\n          ctx.fillStyle = lather;\n          ctx.strokeStyle = \"#d9e8dd\";\n          ctx.lineWidth = 2;\n          ctx.beginPath();\n          ctx.ellipse(zone.x, zone.y, zone.radius * 0.76, zone.radius * 0.52, 0, 0, Math.PI * 2);\n          ctx.fill(); ctx.stroke();\n          ctx.globalAlpha = opacity * 0.76;\n          [[-24, -7, 9], [-12, -16, 12], [2, -12, 10], [17, -2, 12], [23, 11, 8], [4, 16, 11], [-13, 13, 9]].forEach(function (bubble) {\n            const bx = zone.x + bubble[0], by = zone.y + bubble[1];\n            const shine = ctx.createRadialGradient(bx - 3, by - 4, 1, bx, by, bubble[2]);\n            shine.addColorStop(0, \"rgba(255,255,255,.98)\"); shine.addColorStop(1, \"rgba(224,238,232,.82)\");\n            ctx.fillStyle = shine; ctx.beginPath(); ctx.arc(bx, by, bubble[2], 0, Math.PI * 2); ctx.fill();\n          });\n        }\n        ctx.restore();\n      });\n\n      if (!isPractice) return;\n      const paths = getGuidedPaths();\n      ctx.save();\n      paths.forEach(function (path, index) {\n        const done = state.strokes[index];\n        ctx.lineCap = \"round\";\n        ctx.lineJoin = \"round\";\n        if (done) {\n          const skin = SKIN_PRESETS[window.gameConfig.appearance.skinTone] || SKIN_PRESETS.light;\n          ctx.strokeStyle = skin.base;\n          ctx.lineWidth = 27;\n          ctx.beginPath();\n          ctx.moveTo(path.start.x, path.start.y);\n          ctx.lineTo(path.end.x, path.end.y);\n          ctx.stroke();\n          ctx.strokeStyle = \"rgba(255, 255, 255, 0.72)\";\n          ctx.lineWidth = 2;\n          ctx.beginPath();\n          ctx.moveTo(path.start.x, path.start.y);\n          ctx.lineTo(path.end.x, path.end.y);\n          ctx.stroke();\n        } else {\n          ctx.globalAlpha = 0.48 + assistance * 0.35;\n          ctx.strokeStyle = \"#287e78\";\n          ctx.lineWidth = 4;\n          ctx.setLineDash([7, 8]);\n          ctx.beginPath();\n          ctx.moveTo(path.start.x, path.start.y);\n          ctx.lineTo(path.end.x, path.end.y);\n          ctx.stroke();\n          ctx.setLineDash([]);\n          drawArrowHead(ctx, path.start, path.end, 14, \"#287e78\");\n          ctx.globalAlpha = 1;\n        }\n      });\n      ctx.restore();\n    }\n\n    function drawArrowHead(ctx, start, end, size, color) {\n      const angle = Math.atan2(end.y - start.y, end.x - start.x);\n      ctx.save();\n      ctx.fillStyle = color;\n      ctx.beginPath();\n      ctx.moveTo(end.x, end.y);\n      ctx.lineTo(end.x - size * Math.cos(angle - 0.58), end.y - size * Math.sin(angle - 0.58));\n      ctx.lineTo(end.x - size * Math.cos(angle + 0.58), end.y - size * Math.sin(angle + 0.58));\n      ctx.closePath();\n      ctx.fill();\n      ctx.restore();\n    }\n\n    function drawEditorSelection(ctx, centerX, centerY) {\n      ctx.save();\n      ctx.setLineDash([10, 7]);\n      ctx.lineWidth = 3;\n      ctx.strokeStyle = \"#267d79\";\n      roundedPath(ctx, centerX - 180, centerY - 211, 360, 443, 26);\n      ctx.stroke();\n      ctx.setLineDash([]);\n\n      ctx.fillStyle = \"#267d79\";\n      [\n        [centerX - 180, centerY - 211],\n        [centerX + 180, centerY - 211],\n        [centerX - 180, centerY + 232],\n        [centerX + 180, centerY + 232]\n      ].forEach(function (point) {\n        ctx.beginPath();\n        ctx.arc(point[0], point[1], 6, 0, Math.PI * 2);\n        ctx.fill();\n      });\n\n      drawText(ctx, \"SELECTED  \u2022  DRAG TO NUDGE\", centerX, centerY - 222, 18, \"#236f70\", 700, \"center\");\n      ctx.restore();\n    }\n\n    function drawInstructionCard(ctx, mode, state) {\n      let fill = \"#fffdf8\";\n      let stroke = \"#d6e6df\";\n      let title = \"STEP 1  \u2022  WARM CLOTH\";\n      let message = window.gameConfig.settings.captions.prep;\n      let icon = \"1\";\n      let complete = false;\n\n      if (mode === \"edit\") {\n        title = \"EDITOR PREVIEW\";\n        message = \"Set cream opacity and gesture assistance in Game Settings.\";\n        icon = \"E\";\n      } else if (state.stage === 1) {\n        title = \"STEP 2  \u2022  APPLY PRETEND CREAM\";\n        message = state.cream.left && state.cream.right && state.cream.chin\n          ? \"Soft lather covers the marked zones. Tap Next to practice.\"\n          : window.gameConfig.settings.captions.cream;\n        icon = \"2\";\n        complete = state.cream.left && state.cream.right && state.cream.chin;\n      } else if (state.stage === 2) {\n        title = \"STEP 3  \u2022  SHORT, LIGHT PRACTICE STROKES\";\n        message = state.strokes.every(Boolean)\n          ? \"Guided paths complete. Real growth varies; ask an adult for guidance.\"\n          : window.gameConfig.settings.captions.practice;\n        icon = \"3\";\n        complete = state.strokes.every(Boolean);\n      } else if (state.stage === 3) {\n        title = \"STEP 4  \u2022  GENTLE AFTERCARE\";\n        message = !state.rinsed ? window.gameConfig.settings.captions.rinse : !Object.values(state.dried).every(Boolean) ? window.gameConfig.settings.captions.dry : window.gameConfig.settings.captions.moisturize;\n        icon = \"4\";\n        complete = state.rinsed && Object.values(state.dried).every(Boolean) && Object.values(state.moisturized).every(Boolean);\n      } else if (state.stage === 5 && state.routineComplete) {\n        title = \"TASK COMPLETE!\";\n        message = \"You finished the gentle practice routine!\";\n        icon = \"\u2713\";\n        complete = true;\n      } else if (state.stage === 4) {\n        title = \"STEP 5  \u2022  PUT THE PRACTICE TOOL AWAY\";\n        message = window.gameConfig.settings.captions.store;\n        icon = \"5\";\n      } else if (state.cleaned.left && state.cleaned.right) {\n        title = \"BOTH CHEEKS GENTLY CLEAN\";\n        message = \"Warm\u2014not hot. Keep the cloth comfortable to touch.\";\n        icon = \"\u2713\";\n        complete = true;\n      } else if (state.cleaned.left || state.cleaned.right) {\n        title = \"ONE CHEEK CLEAN\";\n        message = \"Nice and gentle. Now sweep the other highlighted cheek.\";\n        icon = \"1\";\n      } else if (state.handsClean) {\n        title = \"HANDS CLEAN  \u2022  YOUR TURN\";\n        message = \"Drag the cloth over both highlighted cheeks.\";\n        icon = \"2\";\n      }\n\n      if (complete) { fill = \"#e8f6ee\"; stroke = \"#b8dfca\"; }\n      if (state.stage === 5 && state.routineComplete) { fill = \"#fff0dc\"; stroke = \"#edaa55\"; }\n      if (window.gameConfig.settings.showExplanationCards !== false || mode === \"edit\" || state.stage === 5) drawPanel(ctx, 32, 876, 656, 92, 22, fill, stroke);\n      else return;\n\n      ctx.save();\n      ctx.fillStyle = complete ? \"#cdebd9\" : \"#e3f1eb\";\n      ctx.beginPath();\n      ctx.arc(73, 922, 24, 0, Math.PI * 2);\n      ctx.fill();\n      ctx.restore();\n\n      if (icon === \"\u2713\") drawCheck(ctx, 73, 922, 20, \"#247b5d\");\n      else drawText(ctx, icon, 73, 922, 21, \"#28787a\", 700, \"center\");\n      drawText(ctx, title, 110, 899, state.stage === 5 && state.routineComplete ? 25 : 18, state.stage === 5 && state.routineComplete ? \"#a95022\" : \"#28787a\", 800);\n      drawText(ctx, message, 110, 936, 20, \"#29464d\", 700);\n\n      if (state.stage === 2 && mode === \"play\") {\n        drawText(ctx, \"Growth direction varies from person to person.\", 42, 978, 16, \"#4f696d\", 600);\n      }\n      if (state.stage === 5 && mode === \"play\") {\n        drawText(ctx, \"Keep tools clean. Stop if skin feels sore or irritated.\", 110, 959, 17, \"#29464d\", 600);\n      }\n    }\n\n    function drawTray(ctx, mode, state) {\n      drawPanel(ctx, 24, 984, 672, 272, 28, \"#fffdf8\", \"#d8e6df\");\n\n      if (mode !== \"play\") {\n        drawText(ctx, \"Appearance preview \u2022 cream opacity and gesture help are adjustable.\", 360, 1000, 17, \"#526b6f\", 500, \"center\");\n      } else if (state.stage === 0) {\n        drawText(ctx, \"Soft cloth only  \u2022  No real razor in this practice.\", 360, 1000, 17, \"#526b6f\", 500, \"center\");\n      } else if (state.stage === 1) {\n        drawText(ctx, \"Pretend cream only  \u2022  This is a no-blade activity.\", 360, 1000, 17, \"#526b6f\", 500, \"center\");\n      } else if (state.stage === 2) {\n        drawText(ctx, \"Rounded practice tool \u2022 no blade \u2022 ask an adult before real shaving.\", 360, 1000, 16, \"#526b6f\", 500, \"center\");\n      } else if (state.stage === 3) {\n        drawText(ctx, \"Aftercare is gentle and self-paced \u2022 tap rinse, then pat, then moisturize.\", 360, 1000, 15, \"#526b6f\", 500, \"center\");\n      } else {\n        drawText(ctx, \"No score or penalties \u2022 take your time and revisit steps; finish after 01:00.\", 360, 1000, 15, \"#526b6f\", 500, \"center\");\n      }\n    }\n\n    function drawCloth(ctx, x, y, clothImage, kind) {\n      if (clothImage && clothImage.complete && clothImage.naturalWidth > 0) {\n        ctx.save();\n        ctx.translate(x, y);\n        ctx.rotate(-0.045);\n        const maxWidth = 142;\n        const maxHeight = 118;\n        const ratio = Math.min(maxWidth / clothImage.naturalWidth, maxHeight / clothImage.naturalHeight);\n        const width = clothImage.naturalWidth * ratio;\n        const height = clothImage.naturalHeight * ratio;\n        ctx.shadowColor = \"rgba(38, 72, 86, 0.24)\";\n        ctx.shadowBlur = 14;\n        ctx.shadowOffsetY = 7;\n        ctx.drawImage(clothImage, -width / 2, -height / 2, width, height);\n        ctx.restore();\n        return;\n      }\n\n      /* Friendly fallback while generated art is unavailable. */\n      ctx.save();\n      ctx.translate(x, y);\n      ctx.rotate(-0.045);\n      ctx.shadowColor = \"rgba(48, 72, 67, 0.24)\";\n      ctx.shadowBlur = 14;\n      ctx.shadowOffsetY = 7;\n      if (kind === \"razor\") {\n        roundedPath(ctx, -17, -10, 34, 78, 15);\n        ctx.fillStyle = \"#9bc9bd\";\n        ctx.fill();\n        roundedPath(ctx, -52, -47, 104, 39, 18);\n        ctx.fillStyle = \"#d5ebe4\";\n        ctx.fill();\n        ctx.strokeStyle = \"#78a99e\";\n        ctx.lineWidth = 3;\n        ctx.stroke();\n      } else if (kind === \"towel\") {\n        roundedPath(ctx, -52, -38, 104, 76, 17);\n        ctx.fillStyle = \"#f7f0df\"; ctx.fill();\n        ctx.fillStyle = \"#b9dce5\"; roundedPath(ctx, -43, -18, 86, 36, 12); ctx.fill();\n        ctx.strokeStyle = \"#9fc4ce\"; ctx.lineWidth = 3; ctx.stroke();\n      } else if (kind === \"moisturizer\") {\n        roundedPath(ctx, -32, -35, 64, 70, 13); ctx.fillStyle = \"#e9f4dc\"; ctx.fill();\n        roundedPath(ctx, -20, -50, 40, 18, 5); ctx.fillStyle = \"#9fc9aa\"; ctx.fill();\n        ctx.strokeStyle = \"#a8c9ab\"; ctx.lineWidth = 3; ctx.stroke();\n      } else if (kind === \"cream\") {\n        roundedPath(ctx, -39, -46, 78, 91, 19);\n        ctx.fillStyle = \"#f7fff9\";\n        ctx.fill();\n        ctx.fillStyle = \"#c9e8dc\";\n        roundedPath(ctx, -39, 5, 78, 30, 10);\n        ctx.fill();\n        roundedPath(ctx, -25, 41, 50, 13, 5);\n        ctx.fillStyle = \"#aed6c7\";\n        ctx.fill();\n        ctx.strokeStyle = \"#92bdb0\";\n        ctx.lineWidth = 3;\n        ctx.stroke();\n      } else {\n        roundedPath(ctx, -55, -38, 110, 76, 17);\n        ctx.fillStyle = \"#dceef5\";\n        ctx.fill();\n        ctx.shadowColor = \"transparent\";\n        ctx.shadowBlur = 0;\n        ctx.shadowOffsetY = 0;\n        roundedPath(ctx, -55, -38, 110, 76, 17);\n        ctx.strokeStyle = \"#8db4c7\";\n        ctx.lineWidth = 3;\n        ctx.stroke();\n      }\n      ctx.restore();\n    }\n\n    function drawScene(ctx, mode, state, clock, scaleX, scaleY, assets) {\n      ctx.setTransform(scaleX, 0, 0, scaleY, 0, 0);\n      ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);\n\n      if (assets.background && assets.background.complete && assets.background.naturalWidth > 0) {\n        ctx.drawImage(assets.background, 0, 0, GAME_WIDTH, GAME_HEIGHT);\n      } else {\n        drawBackground(ctx);\n        drawMirrorBase(ctx);\n      }\n      /* Seasonal dressing is part of the polished 3D bathroom background. */\n      drawHeader(ctx, state);\n      drawSafetyCard(ctx);\n\n      ctx.save();\n      roundedPath(ctx, 48, 352, 624, 500, 24);\n      ctx.clip();\n      const character = drawCharacter(ctx, state, clock, assets);\n      drawFeedbackEffects(ctx, state);\n      if (mode === \"edit\" && state.editSelected) {\n        drawEditorSelection(ctx, character.centerX, character.centerY);\n      }\n      ctx.restore();\n\n      drawText(\n        ctx,\n        mode === \"edit\" ? \"MIRROR PREVIEW\" : \"BATHROOM MIRROR\",\n        70,\n        375,\n        18,\n        \"#537976\",\n        700\n      );\n\n      drawInstructionCard(ctx, mode, state);\n      drawTray(ctx, mode, state);\n\n      if (mode === \"play\" && state.activePointer !== null && state.dragPosition) {\n        drawActiveTool(ctx, state.dragPosition.x, state.dragPosition.y, state.dragTool, assets);\n      } else if (mode === \"play\" && state.keyboardToolSelected && state.keyboardPosition) {\n        drawActiveTool(ctx, state.keyboardPosition.x, state.keyboardPosition.y, state.keyboardToolType, assets);\n      }\n    }\n\n    function drawActiveTool(ctx, x, y, toolType, assets) {\n      if (toolType === \"cream\") return drawCloth(ctx, x, y, assets.cream, \"cream\");\n      if (toolType === \"razor\") return drawCloth(ctx, x, y, assets.razor, \"razor\");\n      if (toolType === \"towel\") return drawCloth(ctx, x, y, assets.towel, \"towel\");\n      if (toolType === \"moisturizer\") return drawCloth(ctx, x, y, assets.moisturizer, \"moisturizer\");\n      drawCloth(ctx, x, y, assets.cloth, \"cloth\");\n    }\n\n    function distanceToSegment(point, start, end) {\n      const dx = end.x - start.x;\n      const dy = end.y - start.y;\n      const lengthSquared = dx * dx + dy * dy;\n\n      if (lengthSquared === 0) {\n        return Math.hypot(point.x - start.x, point.y - start.y);\n      }\n\n      const amount = clamp(\n        ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared,\n        0,\n        1\n      );\n      const nearestX = start.x + amount * dx;\n      const nearestY = start.y + amount * dy;\n      return Math.hypot(point.x - nearestX, point.y - nearestY);\n    }\n\n    function showGameParameters(platformApi, syncAppearanceInputs) {\n      if (!platformApi || typeof platformApi.showGameParameters !== \"function\") return;\n\n      const skinOptions = Object.keys(SKIN_PRESETS).map(function (value) {\n        return { label: SKIN_PRESETS[value].label, value: value };\n      });\n\n      try {\n        const result = platformApi.showGameParameters({\n          name: \"Gentle Shaving Practice\",\n          params: {\n            \"Cream opacity\": {\n              key: \"settings.creamOpacity\",\n              type: \"slider\",\n              min: 0.35,\n              max: 0.9,\n              step: 0.05\n            },\n            \"Gesture assistance\": {\n              key: \"settings.gestureAssistance\",\n              type: \"slider\",\n              min: 0,\n              max: 1,\n              step: 0.05\n            },\n            \"Show explanation cards\": { key: \"settings.showExplanationCards\", type: \"checkbox\" },\n            \"Prep caption\": { key: \"settings.captions.prep\", type: \"string\" },\n            \"Cream caption\": { key: \"settings.captions.cream\", type: \"string\" },\n            \"Practice caption\": { key: \"settings.captions.practice\", type: \"string\" },\n            \"Rinse caption\": { key: \"settings.captions.rinse\", type: \"string\" },\n            \"Drying caption\": { key: \"settings.captions.dry\", type: \"string\" },\n            \"Moisturizer caption\": { key: \"settings.captions.moisturize\", type: \"string\" },\n            \"Put-away caption\": { key: \"settings.captions.store\", type: \"string\" },\n            \"Skin tone\": {\n              key: \"appearance.skinTone\",\n              type: \"dropdown\",\n              options: skinOptions,\n              onChange: function (value) {\n                if (SKIN_PRESETS[value]) {\n                  window.gameConfig.appearance.skinTone = value;\n                  syncAppearanceInputs();\n                }\n              }\n            },\n            \"Facial hair\": {\n              key: \"appearance.facialHair\",\n              type: \"dropdown\",\n              options: [\n                { label: \"None\", value: \"none\" },\n                { label: \"Light\", value: \"light\" },\n                { label: \"Sparse\", value: \"sparse\" }\n              ],\n              onChange: function (value) {\n                if (FACIAL_HAIR_PRESETS.includes(value)) {\n                  window.gameConfig.appearance.facialHair = value;\n                  syncAppearanceInputs();\n                }\n              }\n            }\n          }\n        });\n\n        if (result && typeof result.catch === \"function\") {\n          result.catch(function () {});\n        }\n      } catch (error) {\n        if (typeof platformApi.log === \"function\") {\n          platformApi.log(\"Game parameters could not be opened: \" + String(error));\n        }\n      }\n    }\n\n    function run(mode) {\n      if (activeSession) {\n        activeSession.destroy();\n      }\n\n      const activeMode = mode === \"edit\" ? \"edit\" : \"play\";\n      const config = prepareGameConfig();\n\n      const gameWorld = document.getElementById(\"game-world\");\n      const canvas = document.getElementById(\"game-canvas\");\n      const ctx = canvas.getContext(\"2d\");\n      const cleanButton = document.getElementById(\"clean-button\");\n      const cleanTitle = document.getElementById(\"clean-title\");\n      const cleanCaption = document.getElementById(\"clean-caption\");\n      const clothButton = document.getElementById(\"cloth-button\");\n      const creamButton = document.getElementById(\"cream-button\");\n      const razorButton = document.getElementById(\"razor-button\");\n      const rinseButton = document.getElementById(\"rinse-button\");\n      const towelButton = document.getElementById(\"towel-button\");\n      const moisturizerButton = document.getElementById(\"moisturizer-button\");\n      const storeButton = document.getElementById(\"store-button\");\n      const stepRibbon = document.getElementById(\"step-ribbon\");\n      const muteButton = document.getElementById(\"mute-button\");\n      const backButton = document.getElementById(\"back-button\");\n      const nextButton = document.getElementById(\"next-button\");\n      const editorPanel = document.getElementById(\"editor-panel\");\n      const skinToneSelect = document.getElementById(\"skin-tone-select\");\n      const facialHairSelect = document.getElementById(\"facial-hair-select\");\n      const liveRegion = document.getElementById(\"live-region\");\n      const clothPreviewImage = document.getElementById(\"cloth-preview-image\");\n      const platformApi = window.lib || null;\n\n      function loadImageAsset(assetId) {\n        if (!platformApi || typeof platformApi.getAsset !== \"function\") return null;\n        const asset = platformApi.getAsset(assetId);\n        if (!asset || !asset.url) return null;\n        const image = new Image();\n        image.src = asset.url;\n        return image;\n      }\n\n      const sceneAssets = {\n        background: loadImageAsset(\"mirror_bathroom_background\"),\n        cloth: loadImageAsset(\"warm_washcloth_icon\"),\n        cream: loadImageAsset(\"shaving_cream_tube_icon\"),\n        razor: loadImageAsset(\"rounded_practice_razor_icon\"),\n        towel: loadImageAsset(\"dry_towel_icon\"),\n        moisturizer: loadImageAsset(\"gentle_moisturizer_icon\"),\n        characters: {\n          fair: loadImageAsset(\"character_fair_3d\"),\n          light: loadImageAsset(\"character_light_3d\"),\n          medium: loadImageAsset(\"character_medium_3d\"),\n          tan: loadImageAsset(\"character_tan_3d\"),\n          deep: loadImageAsset(\"character_deep_3d\")\n        }\n      };\n      activeCharacterPortrait = sceneAssets.characters[config.appearance.skinTone] || null;\n      if (sceneAssets.cloth && clothPreviewImage) clothPreviewImage.src = sceneAssets.cloth.src;\n      const creamPreviewImage = document.getElementById(\"cream-preview-image\");\n      const razorPreviewImage = document.getElementById(\"razor-preview-image\");\n      if (sceneAssets.cream && creamPreviewImage) creamPreviewImage.src = sceneAssets.cream.src;\n      if (sceneAssets.razor && razorPreviewImage) razorPreviewImage.src = sceneAssets.razor.src;\n      const towelPreviewImage = document.getElementById(\"towel-preview-image\");\n      const moisturizerPreviewImage = document.getElementById(\"moisturizer-preview-image\");\n      if (sceneAssets.towel && towelPreviewImage) towelPreviewImage.src = sceneAssets.towel.src;\n      if (sceneAssets.moisturizer && moisturizerPreviewImage) moisturizerPreviewImage.src = sceneAssets.moisturizer.src;\n      let rinseAudioContext = null;\n      let rinseAudioBuffer = null;\n      let audioMuted = false;\n      try {\n        const audioInfo = platformApi && typeof platformApi.getAsset === \"function\" ? platformApi.getAsset(\"rinse_water_sfx\") : null;\n        const AudioContextClass = window.AudioContext || window.webkitAudioContext;\n        if (AudioContextClass) {\n          rinseAudioContext = new AudioContextClass();\n          if (audioInfo && audioInfo.url) {\n            fetch(audioInfo.url).then(function (response) { return response.arrayBuffer(); })\n              .then(function (data) { return rinseAudioContext.decodeAudioData(data); })\n              .then(function (buffer) { rinseAudioBuffer = buffer; })\n              .catch(function () { rinseAudioBuffer = null; });\n          }\n        }\n      } catch (audioError) { rinseAudioContext = null; }\n\n      const state = {\n        feedbackParticles: [],\n        rinseFlash: 0,\n        handsClean: false,\n        cleaned: { left: false, right: false },\n        stage: 0,\n        cream: { left: false, right: false, chin: false },\n        rinsed: false,\n        dried: { left: false, right: false, chin: false },\n        moisturized: { left: false, right: false, chin: false },\n        routineComplete: false,\n        highestStage: 0,\n        strokes: [false, false, false, false, false, false],\n        strokeStarted: [false, false, false, false, false, false],\n        strokeProgress: [0, 0, 0, 0, 0, 0],\n        activePointer: null,\n        dragTool: null,\n        gestureStartPosition: null,\n        dragPosition: null,\n        previousDragPosition: null,\n        dragMoved: false,\n        cheekCountAtDragStart: 0,\n        keyboardToolSelected: false,\n        keyboardPosition: null,\n        editSelected: false,\n        editDrag: null,\n        clock: 0,\n        playElapsed: 0,\n        heldInputs: new Set()\n      };\n\n      let canvasScaleX = 1;\n      let canvasScaleY = 1;\n      let lastTimestamp = null;\n      let lastTimerSecond = -1;\n      let animationFrame = 0;\n      let destroyed = false;\n      const cleanup = [];\n\n      function addListener(target, type, handler, options) {\n        target.addEventListener(type, handler, options);\n        cleanup.push(function () {\n          target.removeEventListener(type, handler, options);\n        });\n      }\n\n      function setStatus(message) {\n        liveRegion.textContent = message;\n      }\n\n      function syncAppearanceInputs() {\n        const appearance = window.gameConfig.appearance;\n        activeCharacterPortrait = sceneAssets.characters[appearance.skinTone] || null;\n        skinToneSelect.value = SKIN_PRESETS[appearance.skinTone]\n          ? appearance.skinTone\n          : \"light\";\n        facialHairSelect.value = FACIAL_HAIR_PRESETS.includes(appearance.facialHair)\n          ? appearance.facialHair\n          : \"none\";\n      }\n\n      function updateButtons() {\n        cleanButton.setAttribute(\"aria-pressed\", state.handsClean ? \"true\" : \"false\");\n        clothButton.setAttribute(\"aria-disabled\", state.handsClean ? \"false\" : \"true\");\n\n        cleanButton.hidden = activeMode !== \"play\" || state.stage !== 0;\n        clothButton.hidden = activeMode !== \"play\" || state.stage !== 0;\n        creamButton.hidden = activeMode !== \"play\" || state.stage !== 1;\n        razorButton.hidden = activeMode !== \"play\" || state.stage !== 2;\n        rinseButton.hidden = activeMode !== \"play\" || state.stage !== 3 || state.rinsed;\n        towelButton.hidden = activeMode !== \"play\" || state.stage !== 3 || !state.rinsed || Object.values(state.dried).every(Boolean);\n        moisturizerButton.hidden = activeMode !== \"play\" || state.stage !== 3 || !Object.values(state.dried).every(Boolean) || Object.values(state.moisturized).every(Boolean);\n        storeButton.hidden = activeMode !== \"play\" || state.stage !== 4;\n        const practiceReady = state.playElapsed >= 60;\n        document.getElementById(\"store-title\").textContent = practiceReady ? \"Put tool away\" : \"Keep practicing\";\n        document.getElementById(\"store-caption\").textContent = practiceReady ? window.gameConfig.settings.captions.store : \"Revisit steps until 01:00\";\n        storeButton.setAttribute(\"aria-label\", practiceReady ? \"Put the clean practice tool away safely\" : \"Keep practicing; revisit the routine until the one-minute practice is complete\");\n        stepRibbon.hidden = activeMode !== \"play\";\n        muteButton.hidden = activeMode !== \"play\";\n        backButton.hidden = activeMode !== \"play\";\n        nextButton.hidden = activeMode !== \"play\" || state.stage >= 4;\n        backButton.disabled = state.stage === 0;\n        const stageReady = state.stage === 0 ? state.cleaned.left && state.cleaned.right\n          : state.stage === 1 ? state.cream.left && state.cream.right && state.cream.chin\n          : state.stage === 2 ? state.strokes.every(Boolean)\n          : state.stage === 3 ? state.rinsed && Object.values(state.dried).every(Boolean) && Object.values(state.moisturized).every(Boolean)\n          : false;\n        nextButton.disabled = !stageReady;\n        nextButton.textContent = \"Next \u2192\";\n        stepRibbon.querySelectorAll(\".ribbon-step\").forEach(function (button) {\n          const index = Number(button.dataset.step);\n          button.disabled = index > state.highestStage;\n          button.classList.toggle(\"is-complete\", index < state.highestStage || (index === 4 && state.routineComplete));\n          if (index === (state.stage === 5 ? 4 : state.stage)) button.setAttribute(\"aria-current\", \"step\");\n          else button.removeAttribute(\"aria-current\");\n        });\n\n        document.getElementById(\"rinse-caption\").textContent = window.gameConfig.settings.captions.rinse;\n        document.getElementById(\"towel-caption\").textContent = window.gameConfig.settings.captions.dry;\n        document.getElementById(\"moisturizer-caption\").textContent = window.gameConfig.settings.captions.moisturize;\n        document.getElementById(\"store-caption\").textContent = window.gameConfig.settings.captions.store;\n        if (state.handsClean) {\n          cleanTitle.textContent = \"Hands are clean\";\n          cleanCaption.textContent = \"Ready for the cloth\";\n          cleanButton.setAttribute(\"aria-label\", \"Hands are clean. Continue with the warm cloth.\");\n        } else {\n          cleanTitle.textContent = \"Clean Hands\";\n          cleanCaption.textContent = \"Tap once to begin\";\n          cleanButton.setAttribute(\"aria-label\", \"Clean Hands. Activate before using the cloth.\");\n        }\n      }\n\n      function completeRoutine() {\n        if (state.routineComplete || state.stage !== 4 || state.playElapsed < 60) return;\n        state.routineComplete = true;\n        state.stage = 5;\n        state.highestStage = 4;\n        [[142, 815], [250, 792], [360, 782], [470, 792], [578, 815], [360, 858]].forEach(function (point, index) {\n          burstFeedback(state, point[0], point[1], [\"#f3a83d\", \"#a578bd\", \"#69bd91\"][index % 3]);\n        });\n        playCompletionSound();\n        updateButtons();\n        setStatus(\"TASK COMPLETE! Cheers and applause! You finished the gentle practice routine. Remember: shaving is optional; ask an adult, keep tools clean, and stop if skin is irritated. Tap any numbered ribbon step to revisit it.\");\n      }\n\n      function advanceStageWhenReady() {\n        if (activeMode !== \"play\" || state.routineComplete || state.stage < 0 || state.stage > 3) return;\n        const ready = state.stage === 0 ? state.cleaned.left && state.cleaned.right\n          : state.stage === 1 ? state.cream.left && state.cream.right && state.cream.chin\n          : state.stage === 2 ? state.strokes.every(Boolean)\n          : state.rinsed && Object.values(state.dried).every(Boolean) && Object.values(state.moisturized).every(Boolean);\n        if (!ready) return;\n\n        const completedStage = state.stage;\n        state.stage += 1;\n        state.highestStage = Math.max(state.highestStage, state.stage);\n        state.keyboardToolSelected = false;\n        state.keyboardPosition = null;\n        state.strokeStarted.forEach(function (_, index) { state.strokeStarted[index] = false; });\n        if (state.activePointer !== null) finishToolPointer(null, true);\n        const messages = [\n          \"Step 2: use pretend cream to cover each highlighted cheek and the chin.\",\n          \"Step 3: trace the short arrows with the rounded pretend practice tool. No real razor is used.\",\n          \"Step 4: rinse away pretend foam, pat the face dry, and add a little pretend moisturizer at your own pace.\",\n          \"Step 5: aftercare is complete. Keep practicing or revisit any step until the one-minute timer finishes; TASK COMPLETE will appear automatically.\"\n        ];\n        setStatus(messages[completedStage]);\n        updateButtons();\n        if (state.stage === 4 && state.playElapsed >= 60) completeRoutine();\n      }\n\n      function resizeCanvas() {\n        const width = gameWorld.clientWidth || GAME_WIDTH;\n        const height = gameWorld.clientHeight || GAME_HEIGHT;\n        const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);\n\n        canvas.width = Math.max(1, Math.round(width * pixelRatio));\n        canvas.height = Math.max(1, Math.round(height * pixelRatio));\n        canvasScaleX = canvas.width / GAME_WIDTH;\n        canvasScaleY = canvas.height / GAME_HEIGHT;\n      }\n\n      function pointFromEvent(event) {\n        const rect = canvas.getBoundingClientRect();\n        const width = rect.width || GAME_WIDTH;\n        const height = rect.height || GAME_HEIGHT;\n\n        return {\n          x: (event.clientX - rect.left) * (GAME_WIDTH / width),\n          y: (event.clientY - rect.top) * (GAME_HEIGHT / height)\n        };\n      }\n\n      function countCleanCheeks() {\n        return Number(state.cleaned.left) + Number(state.cleaned.right);\n      }\n\n      function recordCheek(side) {\n        if (!state.handsClean || state.cleaned[side]) return;\n\n        state.cleaned[side] = true;\n        burstFeedback(state, getCheekZones()[side].x, getCheekZones()[side].y, \"#69bd91\");\n        updateButtons();\n\n        if (state.cleaned.left && state.cleaned.right) {\n          setStatus(\"Both cheek areas have a soft clean glow. Reminder: use a warm\u2014not hot\u2014cloth.\");\n        } else {\n          setStatus(\"One cheek is gently clean. Sweep the cloth over the other highlighted cheek.\");\n        }\n      }\n\n      function registerPracticeSegment(start, end) {\n        const paths = getGuidedPaths();\n        const assistance = clamp(Number(window.gameConfig.settings.gestureAssistance), 0, 1);\n        const tolerance = 13 + assistance * 25;\n        const dx = end.x - start.x;\n        const dy = end.y - start.y;\n        const movement = Math.hypot(dx, dy);\n        if (movement < 2) return;\n\n        paths.forEach(function (path, index) {\n          if (state.strokes[index]) return;\n          const pathX = path.end.x - path.start.x;\n          const pathY = path.end.y - path.start.y;\n          const pathLength = Math.hypot(pathX, pathY) || 1;\n          const nearPath = distanceToSegment({ x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }, path.start, path.end) <= tolerance;\n\n          const direction = (dx * pathX + dy * pathY) / (movement * pathLength);\n          if (!state.strokeStarted[index] && Math.hypot(start.x - path.end.x, start.y - path.end.y) <= tolerance * 1.5 && direction < -0.12) {\n            setStatus(\"That was opposite the illustrated arrow. No problem\u2014try a short, gentle stroke along the arrow.\");\n            return;\n          }\n          if (!state.strokeStarted[index] && distanceToSegment(path.start, start, end) <= tolerance * 1.25 && direction > 0.12) {\n            state.strokeStarted[index] = true;\n            state.strokeProgress[index] = 0;\n          }\n          if (!nearPath || !state.strokeStarted[index]) return;\n\n          if (direction < -0.12) {\n            setStatus(\"That was opposite the illustrated arrow. No problem\u2014try a short, gentle stroke along the arrow.\");\n            return;\n          }\n          if (direction < 0.18) return;\n\n          const progress = ((end.x - path.start.x) * pathX + (end.y - path.start.y) * pathY) / (pathLength * pathLength);\n          state.strokeProgress[index] = Math.max(state.strokeProgress[index], clamp(progress, 0, 1));\n          if (state.strokeProgress[index] >= 0.78) {\n            state.strokes[index] = true;\n            burstFeedback(state, path.end.x, path.end.y, \"#7bc9b5\");\n            state.strokeStarted[index] = false;\n            if (state.strokes.every(Boolean)) {\n              setStatus(\"All short guided paths are complete. Real hair-growth direction varies; ask an adult before any real shave.\");\n            } else {\n              setStatus(\"A gentle path is complete. Follow another short arrow when you are ready.\");\n            }\n          }\n        });\n      }\n\n      function registerToolSegment(start, end, toolType) {\n        if (toolType === \"cloth\") {\n          if (!state.handsClean) return;\n          const zones = getCheekZones();\n          [\"left\", \"right\"].forEach(function (side) {\n            if (!state.cleaned[side] && distanceToSegment(zones[side], start, end) <= zones[side].radius) {\n              recordCheek(side);\n            }\n          });\n        } else if (toolType === \"cream\" && state.stage === 1) {\n          const zones = getFaceZones();\n          const assistance = clamp(Number(window.gameConfig.settings.gestureAssistance), 0, 1);\n          const tolerance = 12 + assistance * 22;\n          [\"left\", \"right\", \"chin\"].forEach(function (name) {\n            if (!state.cream[name] && distanceToSegment(zones[name], start, end) <= zones[name].radius * 0.72 + tolerance) {\n              state.cream[name] = true;\n              burstFeedback(state, zones[name].x, zones[name].y, \"#f2b45e\");\n              setStatus(\"A little pretend cream is on the \" + (name === \"chin\" ? \"chin\" : name + \" cheek\") + \". Cover the remaining highlighted zones.\");\n            }\n          });\n          if (state.cream.left && state.cream.right && state.cream.chin) {\n            setStatus(\"Soft pretend lather covers all three zones. Tap Next to practice.\");\n            updateButtons();\n          }\n        } else if (toolType === \"razor\" && state.stage === 2) {\n          registerPracticeSegment(start, end);\n        } else if (toolType === \"towel\" && state.stage === 3 && state.rinsed) {\n          const zones = getFaceZones();\n          [\"left\", \"right\", \"chin\"].forEach(function (name) {\n            if (!state.dried[name] && distanceToSegment(zones[name], start, end) <= zones[name].radius * 0.76) {\n              state.dried[name] = true;\n              burstFeedback(state, zones[name].x, zones[name].y, \"#9cc9d2\");\n              setStatus(\"A soft pat on the \" + (name === \"chin\" ? \"chin\" : name + \" cheek\") + \". Pat gently rather than rubbing.\");\n            }\n          });\n          updateButtons();\n        } else if (toolType === \"moisturizer\" && state.stage === 3 && Object.values(state.dried).every(Boolean)) {\n          const zones = getFaceZones();\n          [\"left\", \"right\", \"chin\"].forEach(function (name) {\n            if (!state.moisturized[name] && distanceToSegment(zones[name], start, end) <= zones[name].radius * 0.74) {\n              state.moisturized[name] = true;\n              burstFeedback(state, zones[name].x, zones[name].y, \"#8acb9e\");\n              setStatus(\"A little pretend moisturizer on the \" + (name === \"chin\" ? \"chin\" : name + \" cheek\") + \". Keep it gentle.\");\n            }\n          });\n          if (Object.values(state.moisturized).every(Boolean)) setStatus(\"Aftercare is complete. The routine will move to the final step automatically.\");\n          updateButtons();\n        }\n        advanceStageWhenReady();\n      }\n\n      function nextToolTarget(toolType) {\n        if (toolType === \"cloth\") {\n          const zones = getCheekZones();\n          const side = state.cleaned.left ? \"right\" : \"left\";\n          return zones[side];\n        }\n        if (toolType === \"cream\") {\n          const zones = getFaceZones();\n          const name = [\"left\", \"right\", \"chin\"].find(function (zone) { return !state.cream[zone]; }) || \"chin\";\n          return zones[name];\n        }\n        if (toolType === \"towel\" || toolType === \"moisturizer\") {\n          const zones = getFaceZones();\n          const progress = toolType === \"towel\" ? state.dried : state.moisturized;\n          const name = [\"left\", \"right\", \"chin\"].find(function (zone) { return !progress[zone]; }) || \"chin\";\n          return zones[name];\n        }\n        const paths = getGuidedPaths();\n        const index = state.strokes.findIndex(function (done) { return !done; });\n        return paths[Math.max(0, index)].start;\n      }\n\n      function activateKeyboardTool(toolType) {\n        if (toolType === \"cloth\" && !state.handsClean) {\n          setStatus(\"Tap Clean Hands before moving or using the cloth.\");\n          return;\n        }\n        if ((toolType === \"towel\" || toolType === \"moisturizer\") && state.stage !== 3) return;\n        if (!state.keyboardToolSelected || state.keyboardToolType !== toolType) {\n          state.keyboardPosition = Object.assign({}, nextToolTarget(toolType));\n          state.keyboardToolSelected = true;\n          state.keyboardToolType = toolType;\n          state.previousKeyboardPosition = Object.assign({}, state.keyboardPosition);\n          setStatus(\"Tool selected. Use arrow keys to move along the highlighted area; press Enter to apply gently.\");\n          return;\n        }\n        const point = state.keyboardPosition;\n        const previous = state.previousKeyboardPosition || point;\n        if (toolType === \"razor\") {\n          registerPracticeSegment(previous, point, toolType);\n          advanceStageWhenReady();\n        } else {\n          registerToolSegment(point, point, toolType);\n        }\n        state.previousKeyboardPosition = Object.assign({}, point);\n        updateButtons();\n      }\n\n      function finishToolPointer(event, cancelled) {\n        if (state.activePointer === null) return;\n        if (event && event.pointerId !== state.activePointer) return;\n        const toolType = state.dragTool;\n        const moved = state.dragMoved;\n        const startPoint = state.gestureStartPosition;\n        const endPoint = state.dragPosition;\n        state.activePointer = null;\n        state.dragPosition = null;\n        state.previousDragPosition = null;\n        state.gestureStartPosition = null;\n        state.dragMoved = false;\n        state.dragTool = null;\n        [clothButton, creamButton, razorButton, towelButton, moisturizerButton].forEach(function (button) {\n          button.classList.remove(\"is-dragging\");\n        });\n\n        if (cancelled || !startPoint || !endPoint) return;\n        if (toolType === \"cream\" && !moved) registerToolSegment(endPoint, endPoint, toolType);\n        if (toolType === \"razor\") {\n          const completedDuringGesture = state.strokes.some(function (done, index) {\n            return done && !state.strokesAtDragStart[index];\n          });\n          state.strokeStarted.forEach(function (started, index) {\n            if (started && !state.strokes[index]) {\n              state.strokeStarted[index] = false;\n              state.strokeProgress[index] = 0;\n            }\n          });\n          if (!completedDuringGesture && moved) {\n            setStatus(\"Nice and gentle. Start at an arrow's tail and try one short, light stroke toward its tip.\");\n          }\n        } else if (toolType === \"cloth\" && !state.handsClean) {\n          setStatus(\"Tap Clean Hands before using the cloth.\");\n        }\n        updateButtons();\n      }\n\n      function handleToolPointerDown(event, toolType, button) {\n        if (activeMode !== \"play\" || state.activePointer !== null) return;\n        if (event.isPrimary === false) return;\n        if (event.pointerType === \"mouse\" && event.button !== 0) return;\n        event.preventDefault();\n        if (toolType === \"cloth\" && !state.handsClean) {\n          setStatus(\"Tap Clean Hands before using the cloth.\");\n          return;\n        }\n        if ((toolType === \"cream\" && state.stage !== 1) || (toolType === \"razor\" && state.stage !== 2) || ((toolType === \"towel\" || toolType === \"moisturizer\") && state.stage !== 3)) return;\n        state.keyboardToolSelected = false;\n        state.keyboardPosition = null;\n        state.activePointer = event.pointerId;\n        state.dragTool = toolType;\n        state.dragPosition = pointFromEvent(event);\n        state.previousDragPosition = state.dragPosition;\n        state.gestureStartPosition = Object.assign({}, state.dragPosition);\n        state.dragMoved = false;\n        state.strokesAtDragStart = state.strokes.slice();\n        button.classList.add(\"is-dragging\");\n        try { button.setPointerCapture(event.pointerId); } catch (error) { /* optional */ }\n      }\n\n      function handleToolPointerMove(event) {\n        if (state.activePointer !== event.pointerId) return;\n        const point = pointFromEvent(event);\n        const previous = state.previousDragPosition;\n        if (previous && Math.hypot(point.x - previous.x, point.y - previous.y) > 6) state.dragMoved = true;\n        if (previous) registerToolSegment(previous, point, state.dragTool);\n        state.dragPosition = point;\n        state.previousDragPosition = point;\n      }\n\n      function handleEditPointerDown(event) {\n        if (activeMode !== \"edit\" || state.editDrag) return;\n        if (event.isPrimary === false) return;\n        if (event.pointerType === \"mouse\" && event.button !== 0) return;\n\n        const point = pointFromEvent(event);\n        const offset = getCharacterOffset();\n        const centerX = 360 + offset.x;\n        const centerY = 565 + offset.y;\n        const hitCharacter =\n          point.x >= centerX - 180 &&\n          point.x <= centerX + 180 &&\n          point.y >= centerY - 211 &&\n          point.y <= centerY + 250;\n\n        if (!hitCharacter) {\n          state.editSelected = false;\n          canvas.style.cursor = \"default\";\n          return;\n        }\n\n        event.preventDefault();\n        state.editSelected = true;\n        state.editDrag = {\n          pointerId: event.pointerId,\n          pointerStart: point,\n          offsetStart: offset\n        };\n        canvas.style.cursor = \"grabbing\";\n\n        try {\n          canvas.setPointerCapture(event.pointerId);\n        } catch (error) {\n          /* Dragging still works in browsers without pointer capture. */\n        }\n      }\n\n      function handleEditPointerMove(event) {\n        if (activeMode !== \"edit\" || !state.editDrag) return;\n        if (event.pointerId !== state.editDrag.pointerId) return;\n\n        const point = pointFromEvent(event);\n        const dx = point.x - state.editDrag.pointerStart.x;\n        const dy = point.y - state.editDrag.pointerStart.y;\n\n        window.gameConfig.scene.characterOffsetX = Math.round(\n          clamp(state.editDrag.offsetStart.x + dx, -28, 28)\n        );\n        window.gameConfig.scene.characterOffsetY = Math.round(\n          clamp(state.editDrag.offsetStart.y + dy, -18, 18)\n        );\n      }\n\n      function finishEditPointer(event) {\n        if (!state.editDrag) return;\n        if (event && event.pointerId !== state.editDrag.pointerId) return;\n        state.editDrag = null;\n        canvas.style.cursor = state.editSelected ? \"grab\" : \"default\";\n      }\n\n      function handleKeyDown(event) {\n        const key = String(event.key || \"\").toLowerCase();\n        if (activeMode !== \"play\") return;\n\n        if (key === \"escape\") {\n          if (state.keyboardToolSelected || state.activePointer !== null) {\n            event.preventDefault();\n            state.keyboardToolSelected = false;\n            state.keyboardPosition = null;\n            finishToolPointer(null, true);\n            setStatus(\"Tool selection cancelled. You can return to the practice whenever you like.\");\n          }\n          return;\n        }\n\n        if (!state.keyboardToolSelected || !state.keyboardPosition) return;\n        const position = state.keyboardPosition;\n        let moved = true;\n        if (key === \"arrowleft\") position.x -= 24;\n        else if (key === \"arrowright\") position.x += 24;\n        else if (key === \"arrowup\") position.y -= 24;\n        else if (key === \"arrowdown\") position.y += 24;\n        else moved = false;\n\n        if (moved) {\n          event.preventDefault();\n          const previous = Object.assign({}, state.previousKeyboardPosition || position);\n          position.x = clamp(position.x, 90, 630);\n          position.y = clamp(position.y, 360, 900);\n          registerToolSegment(previous, position, state.keyboardToolType);\n          state.previousKeyboardPosition = Object.assign({}, position);\n          updateButtons();\n        }\n      }\n\n      function handleKeyUp(event) {\n        const key = String(event.key || \"\").toLowerCase();\n        state.heldInputs.delete(key);\n      }\n\n      function handleWindowBlur() {\n        state.heldInputs.clear();\n\n        if (state.activePointer !== null) {\n          finishToolPointer(null, true);\n        }\n\n        state.keyboardToolSelected = false;\n        state.keyboardPosition = null;\n\n        if (state.editDrag) {\n          finishEditPointer(null);\n        }\n\n      }\n\n      function frame(timestamp) {\n        if (destroyed) return;\n\n        const deltaTime = lastTimestamp === null\n          ? 0\n          : Math.max(0, Math.min((timestamp - lastTimestamp) / 1000, 0.11));\n\n        lastTimestamp = timestamp;\n        state.clock += deltaTime;\n        if (activeMode === \"play\") {\n          state.playElapsed += deltaTime;\n          state.rinseFlash = Math.max(0, state.rinseFlash - deltaTime);\n          state.feedbackParticles.forEach(function (particle) { particle.age += deltaTime; });\n          state.feedbackParticles = state.feedbackParticles.filter(function (particle) { return particle.age < particle.life; });\n        }\n        const elapsedSecond = Math.floor(state.playElapsed);\n        if (elapsedSecond !== lastTimerSecond) {\n          lastTimerSecond = elapsedSecond;\n          const shownSeconds = Math.min(elapsedSecond, 5999);\n          const minutes = String(Math.floor(shownSeconds / 60)).padStart(2, \"0\");\n          const seconds = String(shownSeconds % 60).padStart(2, \"0\");\n          const ready = state.playElapsed >= 60;\n          document.getElementById(\"practice-timer-label\").textContent = (ready ? \"\ud83c\udf84 Practice complete \u00b7 \" : \"\ud83c\udf84 Cozy practice \u00b7 \") + minutes + \":\" + seconds + \" / 01:00\";\n          document.getElementById(\"timer-fill\").style.width = Math.min(100, state.playElapsed / 60 * 100) + \"%\";\n          if (state.stage === 4) {\n            updateButtons();\n            if (state.playElapsed >= 60) completeRoutine();\n          }\n        }\n\n        drawScene(ctx, activeMode, state, state.clock, canvasScaleX, canvasScaleY, sceneAssets);\n        animationFrame = requestAnimationFrame(frame);\n      }\n\n      editorPanel.hidden = activeMode !== \"edit\";\n      canvas.style.cursor = activeMode === \"edit\" ? \"grab\" : \"default\";\n\n      syncAppearanceInputs();\n      updateButtons();\n\n      addListener(window, \"resize\", resizeCanvas);\n      addListener(window, \"keydown\", handleKeyDown);\n      addListener(window, \"keyup\", handleKeyUp);\n      addListener(window, \"blur\", handleWindowBlur);\n\n      addListener(cleanButton, \"click\", function () {\n        if (activeMode !== \"play\") return;\n\n        if (state.handsClean) {\n          setStatus(\"Your hands are already clean. Continue with the warm cloth.\");\n          return;\n        }\n\n        state.handsClean = true;\n        updateButtons();\n        setStatus(\"Hands are clean. Drag the warm cloth over both highlighted cheeks.\");\n      });\n\n      [\n        { button: clothButton, type: \"cloth\" },\n        { button: creamButton, type: \"cream\" },\n        { button: razorButton, type: \"razor\" },\n        { button: towelButton, type: \"towel\" },\n        { button: moisturizerButton, type: \"moisturizer\" }\n      ].forEach(function (tool) {\n        addListener(tool.button, \"pointerdown\", function (event) {\n          handleToolPointerDown(event, tool.type, tool.button);\n        });\n        addListener(tool.button, \"pointermove\", handleToolPointerMove);\n        addListener(tool.button, \"pointerup\", function (event) { finishToolPointer(event, false); });\n        addListener(tool.button, \"pointercancel\", function (event) { finishToolPointer(event, true); });\n        addListener(tool.button, \"lostpointercapture\", function (event) { finishToolPointer(event, true); });\n        addListener(tool.button, \"click\", function (event) {\n          if (activeMode === \"play\" && event.detail === 0) activateKeyboardTool(tool.type);\n        });\n      });\n\n      stepRibbon.querySelectorAll(\".ribbon-step\").forEach(function (button) {\n        addListener(button, \"click\", function () {\n          if (activeMode !== \"play\") return;\n          const requestedStep = Number(button.dataset.step);\n          if (requestedStep <= state.highestStage && requestedStep <= 4) {\n            state.stage = requestedStep;\n            state.keyboardToolSelected = false;\n            state.keyboardPosition = null;\n            if (state.activePointer !== null) finishToolPointer(null, true);\n            updateButtons();\n            setStatus(\"Revisiting \" + [\"warm-water prep\", \"pretend cream\", \"gentle practice strokes\", \"aftercare\", \"safe put-away\"][requestedStep] + \". Your completed actions remain available.\");\n          }\n        });\n      });\n\n      addListener(muteButton, \"click\", function () {\n        audioMuted = !audioMuted;\n        muteButton.textContent = audioMuted ? \"Sound: Off\" : \"Sound: On\";\n        muteButton.setAttribute(\"aria-pressed\", audioMuted ? \"true\" : \"false\");\n        muteButton.setAttribute(\"aria-label\", audioMuted ? \"Turn game sounds on\" : \"Mute game sounds\");\n      });\n\n      function playRinseSound() {\n        if (audioMuted || !rinseAudioContext || !rinseAudioBuffer) return;\n        try {\n          if (rinseAudioContext.state === \"suspended\") rinseAudioContext.resume().catch(function () {});\n          const source = rinseAudioContext.createBufferSource();\n          source.buffer = rinseAudioBuffer;\n          source.connect(rinseAudioContext.destination);\n          source.start(0);\n        } catch (error) { /* optional sound */ }\n      }\n\n      function playCompletionSound() {\n        if (audioMuted || !rinseAudioContext) return;\n        try {\n          const audio = rinseAudioContext;\n          if (audio.state === \"suspended\") audio.resume().catch(function () {});\n          const now = audio.currentTime + 0.04;\n          const sampleRate = audio.sampleRate;\n          const clapBuffer = audio.createBuffer(1, Math.ceil(sampleRate * 0.16), sampleRate);\n          const samples = clapBuffer.getChannelData(0);\n          for (let i = 0; i < samples.length; i++) {\n            const envelope = Math.pow(1 - i / samples.length, 5);\n            samples[i] = (Math.random() * 2 - 1) * envelope;\n          }\n          for (let i = 0; i < 24; i++) {\n            const when = now + i * 0.085 + Math.random() * 0.045;\n            const source = audio.createBufferSource();\n            const filter = audio.createBiquadFilter();\n            const gain = audio.createGain();\n            source.buffer = clapBuffer;\n            filter.type = \"bandpass\";\n            filter.frequency.value = 1450 + Math.random() * 900;\n            filter.Q.value = 0.8;\n            gain.gain.setValueAtTime(0.0001, when);\n            gain.gain.exponentialRampToValueAtTime(0.12 + Math.random() * 0.08, when + 0.008);\n            gain.gain.exponentialRampToValueAtTime(0.0001, when + 0.15);\n            source.connect(filter);\n            filter.connect(gain);\n            gain.connect(audio.destination);\n            source.start(when);\n            source.stop(when + 0.16);\n          }\n          const cheer = audio.createOscillator();\n          const cheerGain = audio.createGain();\n          cheer.type = \"sine\";\n          cheer.frequency.setValueAtTime(390, now + 0.08);\n          cheer.frequency.exponentialRampToValueAtTime(610, now + 0.48);\n          cheer.frequency.exponentialRampToValueAtTime(500, now + 0.95);\n          cheerGain.gain.setValueAtTime(0.0001, now + 0.08);\n          cheerGain.gain.exponentialRampToValueAtTime(0.035, now + 0.22);\n          cheerGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.98);\n          cheer.connect(cheerGain);\n          cheerGain.connect(audio.destination);\n          cheer.start(now + 0.08);\n          cheer.stop(now + 1.02);\n        } catch (error) { /* optional celebration sound */ }\n      }\n\n      addListener(rinseButton, \"click\", function () {\n        if (activeMode !== \"play\" || state.stage !== 3 || state.rinsed) return;\n        state.rinsed = true;\n        state.rinseFlash = 1.35;\n        const rinseZones = getFaceZones();\n        burstFeedback(state, rinseZones.left.x, rinseZones.left.y, \"#74c6df\");\n        burstFeedback(state, rinseZones.right.x, rinseZones.right.y, \"#74c6df\");\n        playRinseSound();\n        updateButtons();\n        setStatus(\"Pretend foam rinsed away. Use the clean soft towel to pat\u2014never scrub\u2014the cheeks and chin.\");\n      });\n\n      addListener(storeButton, \"click\", function () {\n        if (activeMode !== \"play\" || state.stage !== 4) return;\n        if (state.playElapsed < 60) {\n          setStatus(\"Take your time with the one-minute practice. Revisit any unlocked step on the ribbon; TASK COMPLETE will appear automatically when the timer reaches 01:00.\");\n          return;\n        }\n        completeRoutine();\n      });\n\n      addListener(backButton, \"click\", function () {\n        if (activeMode !== \"play\" || state.stage === 0) return;\n        if (state.stage === 5) state.stage = 4;\n        else state.stage -= 1;\n        state.keyboardToolSelected = false;\n        state.keyboardPosition = null;\n        state.strokeStarted.forEach(function (_, index) { state.strokeStarted[index] = false; });\n        if (state.activePointer !== null) finishToolPointer(null, true);\n        updateButtons();\n        setStatus(state.stage === 1\n          ? \"Back to pretend cream. Lather remains in place; review or repeat any zone.\"\n          : \"Back to warm-water prep. Your cheek prep is still here to revisit.\");\n      });\n\n      addListener(nextButton, \"click\", function () {\n        if (activeMode !== \"play\") return;\n        if (state.stage === 0 && state.cleaned.left && state.cleaned.right) {\n          state.stage = 1;\n          state.highestStage = Math.max(state.highestStage, 1);\n          setStatus(\"Step 2: use pretend cream to cover each highlighted cheek and the chin.\");\n        } else if (state.stage === 1 && state.cream.left && state.cream.right && state.cream.chin) {\n          state.stage = 2;\n          state.highestStage = Math.max(state.highestStage, 2);\n          setStatus(\"Step 3: trace the short arrows with the rounded pretend practice tool. No real razor is used.\");\n        } else if (state.stage === 2 && state.strokes.every(Boolean)) {\n          state.stage = 3;\n          state.highestStage = Math.max(state.highestStage, 3);\n          setStatus(\"Step 4: rinse away pretend foam, pat the face dry, and add a little pretend moisturizer at your own pace.\");\n        } else if (state.stage === 3 && state.rinsed && Object.values(state.dried).every(Boolean) && Object.values(state.moisturized).every(Boolean)) {\n          state.stage = 4;\n          state.highestStage = Math.max(state.highestStage, 4);\n          setStatus(\"Step 5: put the clean practice tool away safely. Then a calm recap will offer step replay.\");\n        }\n        state.keyboardToolSelected = false;\n        state.keyboardPosition = null;\n        state.strokeStarted.forEach(function (_, index) { state.strokeStarted[index] = false; });\n        if (state.activePointer !== null) finishToolPointer(null, true);\n        updateButtons();\n      });\n\n      addListener(skinToneSelect, \"change\", function () {\n        if (SKIN_PRESETS[skinToneSelect.value]) {\n          window.gameConfig.appearance.skinTone = skinToneSelect.value;\n          setStatus(\"Skin-tone preview updated.\");\n        }\n      });\n\n      addListener(facialHairSelect, \"change\", function () {\n        if (FACIAL_HAIR_PRESETS.includes(facialHairSelect.value)) {\n          window.gameConfig.appearance.facialHair = facialHairSelect.value;\n          setStatus(\"Facial-hair preview updated.\");\n        }\n      });\n\n      addListener(canvas, \"pointerdown\", handleEditPointerDown);\n      addListener(canvas, \"pointermove\", handleEditPointerMove);\n      addListener(canvas, \"pointerup\", finishEditPointer);\n      addListener(canvas, \"pointercancel\", finishEditPointer);\n      addListener(canvas, \"lostpointercapture\", finishEditPointer);\n\n      resizeCanvas();\n\n      if (activeMode === \"play\") {\n        setStatus(\"Step 1: tap Clean Hands, then drag the cloth across both highlighted cheeks.\");\n      } else {\n        setStatus(\"Edit mode: choose skin tone and facial-hair visibility, or drag the character preview.\");\n      }\n\n      showGameParameters(platformApi, syncAppearanceInputs);\n\n      const session = {\n        destroy: function () {\n          if (destroyed) return;\n          destroyed = true;\n          cancelAnimationFrame(animationFrame);\n          cleanup.forEach(function (removeListener) {\n            removeListener();\n          });\n          clothButton.classList.remove(\"is-dragging\");\n          [creamButton, razorButton, towelButton, moisturizerButton].forEach(function (button) { button.classList.remove(\"is-dragging\"); });\n          if (rinseAudioContext && rinseAudioContext.state !== \"closed\") rinseAudioContext.close().catch(function () {});\n        }\n      };\n\n      activeSession = session;\n      animationFrame = requestAnimationFrame(frame);\n      return session;\n    }\n\n    /* The host calls run(\"edit\") or run(\"play\"). */\n    window.run = run;\n  </script>\n</body>\n</html>", "game_config": "{\n  \"appearance\": {\n    \"skinTone\": \"light\",\n    \"facialHair\": \"none\"\n  },\n  \"scene\": {\n    \"characterOffsetX\": 0,\n    \"characterOffsetY\": 0\n  },\n  \"settings\": {\n    \"creamOpacity\": 0.72,\n    \"gestureAssistance\": 0.65,\n    \"showExplanationCards\": true,\n    \"captions\": {\n      \"prep\": \"Warm\u2014not hot. Gently prepare both cheeks.\",\n      \"cream\": \"Spread pretend cream over both cheeks and chin.\",\n      \"practice\": \"Follow short arrows with gentle pretend strokes.\",\n      \"rinse\": \"Rinse away the pretend foam with comfortable water.\",\n      \"dry\": \"Pat each area dry; do not rub.\",\n      \"moisturize\": \"Use a small amount and spread it gently.\",\n      \"store\": \"Put the clean practice tool away safely.\"\n    }\n  }\n}", "game_design_document": "**Experience:** An English-language, self-paced grooming simulation for young beginners. Players guide a friendly, cartoon character through a pretend shave in a bathroom mirror. The tool is always presented as a rounded practice prop, not a real blade.\n\n**Core loop:** Read a short cue, make a touch or pointer gesture, see a calm visual response, then continue or revisit a step. The five-step routine is:\n\n1. **Ready & warm wash:** Shaving is optional, facial hair grows at different times, and a first real shave should only happen with a parent or guardian\u2019s guidance. Wash hands; if skin is sore, cut, or irritated, pause and ask an adult. Drag a warm\u2014not hot\u2014cloth across the cheeks.\n2. **Apply cream:** Spread shaving cream over the indicated areas. Explain that real shaving should not be done on dry skin.\n3. **Practice strokes:** Trace short, gentle paths with the rounded virtual razor, following arrows that represent this character\u2019s hair growth. Remind players not to press hard, repeatedly scrape one spot, or shave near the eyes or lips. Real hair grows differently from person to person, so an adult should guide any real shave.\n4. **Rinse & pat dry:** Clear the virtual cream with water, then gently pat the face dry.\n5. **Moisturize & put away:** Apply a gentle, fragrance-free moisturizer if needed, and safely put away the tool. Never share razors. If a real shave causes discomfort, irritation, or a nick, stop and tell an adult.\n\nThe full guided route includes roughly a dozen hands-on actions and short instruction cards, designed for a typical **60\u201390-second** self-paced session. There is no countdown or forced waiting.\n\n**Toy\u2014not a scored game:** No points, timer, lives, difficulty, skin-quality grading, or fail state. A simple step ribbon is for navigation only. Players may revisit steps, and the ending offers a brief care reminder rather than a win screen.\n\n**Perspective and layout:** Fixed, front-facing 2D mirror view in a 720\u00d71280 portrait layout. A code-drawn, fully clothed cartoon character\u2019s face sits in the center, with highlighted cheek and chin zones. Instructions stay near the top; the tool tray and large navigation buttons sit in the lower half. Interactive touch targets are at least 96\u00d796 pixels.\n\n**Controls:**\n- **Touch:** Tap a large tool button, then drag the cloth, cream, or practice razor over the highlighted area. Tap Back or Next to navigate.\n- **Mouse:** Click and drag tools; click the same large buttons.\n- **Keyboard:** Tab to focus controls; Enter or Space activates a selected tool or button. Arrow keys move a selected practice tool; Enter or Space applies the action. Escape cancels a selection.\n- Track the active pointer so extra touches do not interfere. Prevent default behavior for game keys, normalize key names to lowercase, and clear held input on window blur.\n\n**Edit Mode:** A compact editor exposes safe, bounded options in the game config:\n- Choose from five skin-tone presets and facial-hair visibility presets: none, light, or sparse.\n- Adjust theme accents and visual cream opacity.\n- Toggle optional explanation cards and adjust gesture assistance.\n- Customize non-safety captions within a short character limit. Keep the adult-supervision, optional-shaving, hygiene, and stop-if-irritated reminders visible and uneditable; keep the lesson sequence fixed.\n\n**Art direction:** Calm, clean flat-vector illustration with warm cream, soft blue, mint, and peach. The character is friendly and age-appropriate, with no realistic blade, injury, blood, or distress. Draw the face and interaction zones in code so appearance presets remain editable. Use generated images for the bathroom setting and draggable objects; render water, arrows, foam, and feedback effects with code.\n\n**Runtime:** Self-contained HTML/JS using only bundled code and approved static assets. Use fixed English copy; no runtime text generation, external APIs, leaderboard, or player-data storage is needed.", "persistent_storage_config": null, "game_metadata": {"game_id": "01M644CC5FKB8H8NK4J317FC7W", "title": "Shave Tutor", "description": "**Description:** Practice a gentle, pretend first shave\u2014from skin prep and careful strokes to soothing aftercare.", "thumbnail_url": "https://d2oir5eh8rty2e.cloudfront.net/assets/images/thumbnail_84d61798-36d2-4457-ae8e-6b73d388d968.webp", "created_at": 1793288012, "updated_at": 1793590577963.9592, "art_style": null}, "task_plan": null}