[object Object]

← back to Games Agentabrams

Add Baguette Brawl to the arcade (games/baguette-brawl, single-file inlined build + manifest)

63c4a5259918259e7dcef0ff678771212041f043 · 2026-08-31 13:02:45 -0700 · Steve Abrams

Files touched

Diff

commit 63c4a5259918259e7dcef0ff678771212041f043
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 13:02:45 2026 -0700

    Add Baguette Brawl to the arcade (games/baguette-brawl, single-file inlined build + manifest)
---
 games-data.js                           |   8 +
 games.json                              |  88 +--
 games/baguette-brawl/BaguetteBrawl.aa   | 915 ++++++++++++++++++++++++++++++++
 games/baguette-brawl/index.html         | 915 ++++++++++++++++++++++++++++++++
 games/particle-text-starling/index.html |   5 +
 5 files changed, 1891 insertions(+), 40 deletions(-)

diff --git a/games-data.js b/games-data.js
index 56ea4e7..2594164 100644
--- a/games-data.js
+++ b/games-data.js
@@ -2,6 +2,14 @@
    file:// fallback: <script src> works from a local file, fetch() does not. */
 window.__GAMES_DATA__ = {
   "games": [
+    {
+      "id": "baguette-brawl",
+      "file": "BaguetteBrawl.aa",
+      "title": "Baguette Brawl",
+      "desc": "Two crusty baguette duelists brawl beneath the Eiffel Tower — jab, heavy, guard, and charge the meter for an unblockable Crust Crusher. Solo vs. 3-tier AI or local 2-player, best of 3. Pure Canvas 2D, zero dependencies.",
+      "path": "games/baguette-brawl/",
+      "added": "2026-08-31"
+    },
     {
       "id": "asteroids",
       "file": "AbramsAsteroids.aa",
diff --git a/games.json b/games.json
index dda0b89..3fc56d2 100644
--- a/games.json
+++ b/games.json
@@ -1,5 +1,13 @@
 {
   "games": [
+    {
+      "id": "baguette-brawl",
+      "file": "BaguetteBrawl.aa",
+      "title": "Baguette Brawl",
+      "desc": "Two crusty baguette duelists brawl beneath the Eiffel Tower \u2014 jab, heavy, guard, and charge the meter for an unblockable Crust Crusher. Solo vs. 3-tier AI or local 2-player, best of 3. Pure Canvas 2D, zero dependencies.",
+      "path": "games/baguette-brawl/",
+      "added": "2026-08-31"
+    },
     {
       "id": "asteroids",
       "file": "AbramsAsteroids.aa",
@@ -12,7 +20,7 @@
       "id": "minesweeper",
       "file": "AbramsMines.aa",
       "title": "Abrams Minesweeper",
-      "desc": "Classic Minesweeper reborn in neon-on-dark — flood-fill sweeps, tap-to-flag, first-click-safe, best-time chasing across three difficulties. Pure vanilla JS, zero dependencies.",
+      "desc": "Classic Minesweeper reborn in neon-on-dark \u2014 flood-fill sweeps, tap-to-flag, first-click-safe, best-time chasing across three difficulties. Pure vanilla JS, zero dependencies.",
       "path": "games/minesweeper/",
       "added": "2026-07-24"
     },
@@ -36,7 +44,7 @@
       "id": "tower-stack",
       "file": "AbramsStack.aa",
       "title": "Abrams Stack",
-      "desc": "Time your taps to stack neon blocks into a sky-high tower — nail a perfect drop for combo bonuses before the overhang slices you down. Pure Canvas 2D, zero dependencies.",
+      "desc": "Time your taps to stack neon blocks into a sky-high tower \u2014 nail a perfect drop for combo bonuses before the overhang slices you down. Pure Canvas 2D, zero dependencies.",
       "path": "games/tower-stack/",
       "added": "2026-07-24"
     },
@@ -60,15 +68,15 @@
       "id": "whack-a-mole",
       "file": "AbramsWhack.aa",
       "title": "Abrams Whack",
-      "desc": "A neon whack-a-mole arcade sprint — smash glowing moles for combo multipliers, snag gold jackpots, dodge bombs that cost lives. Pure vanilla JS, zero dependencies.",
+      "desc": "A neon whack-a-mole arcade sprint \u2014 smash glowing moles for combo multipliers, snag gold jackpots, dodge bombs that cost lives. Pure vanilla JS, zero dependencies.",
       "path": "games/whack-a-mole/",
       "added": "2026-07-24"
     },
     {
       "id": "dvd-bounce",
       "file": "DVDBounce.aa",
-      "title": "DVD Bounce — Elevated",
-      "desc": "The classic bouncing-logo screensaver, elevated — glowing trails, corner-hit celebration, color shifts on every wall bounce. An ambient toy, not a scored game. Model Arena winner (Claude). Pure Canvas 2D, zero dependencies.",
+      "title": "DVD Bounce \u2014 Elevated",
+      "desc": "The classic bouncing-logo screensaver, elevated \u2014 glowing trails, corner-hit celebration, color shifts on every wall bounce. An ambient toy, not a scored game. Model Arena winner (Claude). Pure Canvas 2D, zero dependencies.",
       "path": "games/dvd-bounce/",
       "added": "2026-07-24"
     },
@@ -76,16 +84,16 @@
       "id": "rope-physics",
       "file": "VerletRope.aa",
       "title": "Verlet Rope",
-      "desc": "A physics toy — drag to swing a Verlet-integrated rope, watch it settle and whip under gravity. A generative-physics playground, not a scored game. Model Arena winner (Claude). Pure Canvas 2D, zero dependencies.",
+      "desc": "A physics toy \u2014 drag to swing a Verlet-integrated rope, watch it settle and whip under gravity. A generative-physics playground, not a scored game. Model Arena winner (Claude). Pure Canvas 2D, zero dependencies.",
       "path": "games/rope-physics/",
       "added": "2026-07-24"
     },
     {
       "id": "particle-text-starling",
       "file": "Starling.aa",
-      "icon": "🐦",
+      "icon": "\ud83d\udc26",
       "title": "Starling",
-      "desc": "Type a word or phrase and thousands of particles murmurate into it — sweep the mouse to scatter, watch them reform, click for a burst. 5 color themes, live typing, multi-line wrapping. Pure Canvas 2D, zero dependencies.",
+      "desc": "Type a word or phrase and thousands of particles murmurate into it \u2014 sweep the mouse to scatter, watch them reform, click for a burst. 5 color themes, live typing, multi-line wrapping. Pure Canvas 2D, zero dependencies.",
       "path": "games/particle-text-starling/",
       "added": "2026-07-24"
     },
@@ -93,15 +101,15 @@
       "id": "rubiks-cube",
       "file": "AbramsCube.aa",
       "title": "Abrams Cube",
-      "desc": "Pure CSS-3D twist cube — drag to orbit, scramble, solve, undo, timer.",
+      "desc": "Pure CSS-3D twist cube \u2014 drag to orbit, scramble, solve, undo, timer.",
       "path": "games/rubiks-cube/",
       "added": "2026-07-23"
     },
     {
       "id": "atmos-skyfire",
       "file": "AtmosSkyfire.aa",
-      "title": "ATMOS — Skyfire",
-      "desc": "Three.js arcade flyer — bank through a gradient sky, fire twin missiles (SPACE/click), dodge or shoot incoming birds. Steer with arrows/WASD/mouse.",
+      "title": "ATMOS \u2014 Skyfire",
+      "desc": "Three.js arcade flyer \u2014 bank through a gradient sky, fire twin missiles (SPACE/click), dodge or shoot incoming birds. Steer with arrows/WASD/mouse.",
       "path": "games/atmos-skyfire/",
       "added": "2026-07-23"
     },
@@ -109,7 +117,7 @@
       "id": "flappy-clone",
       "file": "FlappyClone.aa",
       "title": "Flappy Clone",
-      "desc": "Polished Flappy-Bird clone — Space/click/tap to flap through scrolling pipes. Parallax daytime sky, medals, persistent best, sound (M), pause (P). Pure Canvas 2D, zero dependencies.",
+      "desc": "Polished Flappy-Bird clone \u2014 Space/click/tap to flap through scrolling pipes. Parallax daytime sky, medals, persistent best, sound (M), pause (P). Pure Canvas 2D, zero dependencies.",
       "path": "games/flappy-clone/",
       "added": "2026-07-23"
     },
@@ -117,7 +125,7 @@
       "id": "flappy-sky",
       "file": "FlappySky.aa",
       "title": "Flappy Sky",
-      "desc": "Flap through the neon gates against a twinkling night sky — how far can you fly? Medals, persistent best, sound (M), pause (P/Esc). Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Flap through the neon gates against a twinkling night sky \u2014 how far can you fly? Medals, persistent best, sound (M), pause (P/Esc). Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/flappy-sky/",
       "added": "2026-07-24"
     },
@@ -125,7 +133,7 @@
       "id": "tetris",
       "file": "AbramsBlocks.aa",
       "title": "Abrams Blocks",
-      "desc": "Full-featured Tetris — SRS rotation with wall kicks, 7-bag randomizer, ghost piece, hold, next-queue, level speed-up, line-clear scoring. Arrows move, ↑/Z rotate, Space hard-drop, C hold. Touch: swipe to move, tap to rotate, fling down to drop. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Full-featured Tetris \u2014 SRS rotation with wall kicks, 7-bag randomizer, ghost piece, hold, next-queue, level speed-up, line-clear scoring. Arrows move, \u2191/Z rotate, Space hard-drop, C hold. Touch: swipe to move, tap to rotate, fling down to drop. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/tetris/",
       "added": "2026-07-24"
     },
@@ -133,7 +141,7 @@
       "id": "flappy-flight",
       "file": "FlappyFlight.aa",
       "title": "Flappy Flight",
-      "desc": "Polished Flappy-Bird-style flyer — Space/click/tap to flap through scrolling pipes. Twinkling parallax night sky, moon, medals, persistent best. True pause (P/Esc, auto-pauses off-focus), mute (M or speaker icon). Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Polished Flappy-Bird-style flyer \u2014 Space/click/tap to flap through scrolling pipes. Twinkling parallax night sky, moon, medals, persistent best. True pause (P/Esc, auto-pauses off-focus), mute (M or speaker icon). Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/flappy-flight/",
       "added": "2026-07-23"
     },
@@ -141,7 +149,7 @@
       "id": "snake",
       "file": "AbramsSnake.aa",
       "title": "Abrams Snake",
-      "desc": "Classic grid snake — eat the pink, grow, and speed up as you go. Walls and your own tail are fatal. Arrows/WASD to steer, swipe on touch, P pause, M mute. Persistent best. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Classic grid snake \u2014 eat the pink, grow, and speed up as you go. Walls and your own tail are fatal. Arrows/WASD to steer, swipe on touch, P pause, M mute. Persistent best. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/snake/",
       "added": "2026-07-24"
     },
@@ -149,7 +157,7 @@
       "id": "breakout",
       "file": "AbramsBreak.aa",
       "title": "Abrams Break",
-      "desc": "Brick-breaker — steer the paddle with mouse/arrows/drag, launch with Space or click, clear every brick, ride the ball's contact angle. Multi-row levels that get tougher, lives, tough top row, persistent best. P pause, M mute. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Brick-breaker \u2014 steer the paddle with mouse/arrows/drag, launch with Space or click, clear every brick, ride the ball's contact angle. Multi-row levels that get tougher, lives, tough top row, persistent best. P pause, M mute. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/breakout/",
       "added": "2026-07-24"
     },
@@ -157,7 +165,7 @@
       "id": "2048",
       "file": "Abrams2048.aa",
       "title": "Abrams 2048",
-      "desc": "Sliding-tile puzzle — merge equal tiles to reach 2048 and beyond. Arrows/WASD or swipe to slide the whole board, merges pop and chime, persistent best, keep-playing after the win. Pure DOM grid, procedural Web Audio, zero dependencies.",
+      "desc": "Sliding-tile puzzle \u2014 merge equal tiles to reach 2048 and beyond. Arrows/WASD or swipe to slide the whole board, merges pop and chime, persistent best, keep-playing after the win. Pure DOM grid, procedural Web Audio, zero dependencies.",
       "path": "games/2048/",
       "added": "2026-07-24"
     },
@@ -165,7 +173,7 @@
       "id": "pong",
       "file": "AbramsPong.aa",
       "title": "Abrams Pong 2P",
-      "desc": "Two-player table tennis — Left: W/S, Right: ↑/↓, first to 11. Contact-angle english off the paddle, speed-up rallies, procedural blips. Toggle 'vs CPU' to play solo. Touch: drag your own half of the screen. P pause, M mute. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Two-player table tennis \u2014 Left: W/S, Right: \u2191/\u2193, first to 11. Contact-angle english off the paddle, speed-up rallies, procedural blips. Toggle 'vs CPU' to play solo. Touch: drag your own half of the screen. P pause, M mute. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/pong/",
       "added": "2026-07-24"
     },
@@ -173,7 +181,7 @@
       "id": "emoji-memory",
       "file": "EmojiMemory.aa",
       "title": "Abrams Emoji Memory",
-      "desc": "Flip-and-match memory game — find every emoji pair in the fewest moves. 3D card flips, move counter + timer, persistent best-per-size, three difficulties (4×4 / 6×6 / 8×8), and a light/dark theme toggle. Click/tap to flip, N new game. Pure DOM grid, procedural Web Audio, zero dependencies.",
+      "desc": "Flip-and-match memory game \u2014 find every emoji pair in the fewest moves. 3D card flips, move counter + timer, persistent best-per-size, three difficulties (4\u00d74 / 6\u00d76 / 8\u00d78), and a light/dark theme toggle. Click/tap to flip, N new game. Pure DOM grid, procedural Web Audio, zero dependencies.",
       "path": "games/emoji-memory/",
       "added": "2026-07-24"
     },
@@ -181,7 +189,7 @@
       "id": "spirograph",
       "file": "Spirograph.aa",
       "title": "Spirograph Studio",
-      "desc": "Hypotrochoid/epitrochoid pattern toy — tune the fixed & rolling gear radii and pen offset to draw endless looping curves, layer colors, and export the art. A generative-art playground, not a scored game. Pure Canvas 2D, zero dependencies.",
+      "desc": "Hypotrochoid/epitrochoid pattern toy \u2014 tune the fixed & rolling gear radii and pen offset to draw endless looping curves, layer colors, and export the art. A generative-art playground, not a scored game. Pure Canvas 2D, zero dependencies.",
       "path": "games/spirograph/",
       "added": "2026-07-24"
     },
@@ -189,7 +197,7 @@
       "id": "constellation-clock",
       "file": "ConstellationClock.aa",
       "title": "Constellation Clock",
-      "desc": "A live clock rendered as a drifting star-field constellation — the current time drawn in connected stars against a twinkling night sky. An ambient visualizer, not a scored game. Pure Canvas 2D, zero dependencies.",
+      "desc": "A live clock rendered as a drifting star-field constellation \u2014 the current time drawn in connected stars against a twinkling night sky. An ambient visualizer, not a scored game. Pure Canvas 2D, zero dependencies.",
       "path": "games/constellation-clock/",
       "added": "2026-07-24"
     },
@@ -197,8 +205,8 @@
       "id": "backgammon",
       "file": "90210Backgammon.aa",
       "title": "90210 Backgammon",
-      "icon": "🎲",
-      "desc": "A lavish Beverly Hills take on classic backgammon — roll the dice, bear off your checkers, and hit the bar against an opponent on a gold-and-marble 90210 board. Self-contained HTML, zero runtime dependencies.",
+      "icon": "\ud83c\udfb2",
+      "desc": "A lavish Beverly Hills take on classic backgammon \u2014 roll the dice, bear off your checkers, and hit the bar against an opponent on a gold-and-marble 90210 board. Self-contained HTML, zero runtime dependencies.",
       "path": "games/backgammon/",
       "added": "2026-07-25"
     },
@@ -206,7 +214,7 @@
       "id": "chomp",
       "file": "AbramsChomp.aa",
       "title": "Abrams Chomp",
-      "desc": "Neon maze-chomper — weave through the maze eating every pellet, grab power pellets to turn the tables and chase the ghosts, and clear the board. Arrows/WASD to steer, swipe on touch. Pure Canvas 2D, zero dependencies.",
+      "desc": "Neon maze-chomper \u2014 weave through the maze eating every pellet, grab power pellets to turn the tables and chase the ghosts, and clear the board. Arrows/WASD to steer, swipe on touch. Pure Canvas 2D, zero dependencies.",
       "path": "games/chomp/",
       "added": "2026-07-25"
     },
@@ -214,7 +222,7 @@
       "id": "gems",
       "file": "NeonGems.aa",
       "title": "Neon Gems",
-      "desc": "Match-3 puzzle — swap adjacent neon gems to line up three or more, trigger cascading chain combos, and rack up the score. Click/tap or drag to swap. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Match-3 puzzle \u2014 swap adjacent neon gems to line up three or more, trigger cascading chain combos, and rack up the score. Click/tap or drag to swap. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/gems/",
       "added": "2026-07-25"
     },
@@ -222,7 +230,7 @@
       "id": "hopper",
       "file": "AbramsHopper.aa",
       "title": "Abrams Hopper",
-      "desc": "Frogger-style road-and-river crosser — hop across busy neon traffic lanes and ride logs over the water to reach the home slots without getting squashed or wet. Arrows/WASD to hop, swipe on touch. Pure Canvas 2D, zero dependencies.",
+      "desc": "Frogger-style road-and-river crosser \u2014 hop across busy neon traffic lanes and ride logs over the water to reach the home slots without getting squashed or wet. Arrows/WASD to hop, swipe on touch. Pure Canvas 2D, zero dependencies.",
       "path": "games/hopper/",
       "added": "2026-07-25"
     },
@@ -230,7 +238,7 @@
       "id": "invaders",
       "file": "NeonInvaders.aa",
       "title": "Neon Invaders",
-      "desc": "Space-Invaders arcade shooter — slide your cannon, blast the descending grid of neon aliens before they reach the ground, and dodge their return fire. ←/→ move, Space fire, P pause. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Space-Invaders arcade shooter \u2014 slide your cannon, blast the descending grid of neon aliens before they reach the ground, and dodge their return fire. \u2190/\u2192 move, Space fire, P pause. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/invaders/",
       "added": "2026-07-25"
     },
@@ -238,7 +246,7 @@
       "id": "missile",
       "file": "MissileCommand.aa",
       "title": "Missile Command",
-      "desc": "City-defense classic — aim and detonate interceptor missiles to destroy incoming salvos before they wipe out your cities, across escalating waves. Click/tap to fire at a point. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "City-defense classic \u2014 aim and detonate interceptor missiles to destroy incoming salvos before they wipe out your cities, across escalating waves. Click/tap to fire at a point. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/missile/",
       "added": "2026-07-25"
     },
@@ -246,7 +254,7 @@
       "id": "racer",
       "file": "NeonDrifter.aa",
       "title": "Neon Drifter",
-      "desc": "Endless top-down racer — drift a neon car down a scrolling track, weave through traffic, and chase distance as the speed ramps up. Arrows/WASD to steer, swipe on touch. Pure Canvas 2D, zero dependencies.",
+      "desc": "Endless top-down racer \u2014 drift a neon car down a scrolling track, weave through traffic, and chase distance as the speed ramps up. Arrows/WASD to steer, swipe on touch. Pure Canvas 2D, zero dependencies.",
       "path": "games/racer/",
       "added": "2026-07-25"
     },
@@ -254,7 +262,7 @@
       "id": "sokoban",
       "file": "NeonSokoban.aa",
       "title": "Neon Sokoban",
-      "desc": "Crate-pushing puzzle — push every glowing crate onto its target pad in the fewest moves; you can only push, never pull, so plan ahead. Arrows/WASD to push, U undo, R restart. Pure vanilla JS, zero dependencies.",
+      "desc": "Crate-pushing puzzle \u2014 push every glowing crate onto its target pad in the fewest moves; you can only push, never pull, so plan ahead. Arrows/WASD to push, U undo, R restart. Pure vanilla JS, zero dependencies.",
       "path": "games/sokoban/",
       "added": "2026-07-25"
     },
@@ -262,7 +270,7 @@
       "id": "sudoku",
       "file": "NeonSudoku.aa",
       "title": "Neon Sudoku",
-      "desc": "Classic 9×9 Sudoku reborn in neon — fill the grid so every row, column, and box holds 1–9, with pencil marks, mistake highlighting, and multiple difficulties. Click a cell then type, or tap the number pad. Pure vanilla JS, zero dependencies.",
+      "desc": "Classic 9\u00d79 Sudoku reborn in neon \u2014 fill the grid so every row, column, and box holds 1\u20139, with pencil marks, mistake highlighting, and multiple difficulties. Click a cell then type, or tap the number pad. Pure vanilla JS, zero dependencies.",
       "path": "games/sudoku/",
       "added": "2026-07-25"
     },
@@ -270,7 +278,7 @@
       "id": "tron",
       "file": "AbramsTron.aa",
       "title": "Abrams Tron",
-      "desc": "Light-cycle duel — race a neon trail across the grid, box out the AI rider, and force it to crash while you thread your own wall. Arrows/WASD to turn, P pause. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
+      "desc": "Light-cycle duel \u2014 race a neon trail across the grid, box out the AI rider, and force it to crash while you thread your own wall. Arrows/WASD to turn, P pause. Pure Canvas 2D, procedural Web Audio, zero dependencies.",
       "path": "games/tron/",
       "added": "2026-07-25"
     },
@@ -278,7 +286,7 @@
       "id": "wordle",
       "file": "AbramsWordle.aa",
       "title": "Abrams Wordle",
-      "desc": "Guess the hidden five-letter word in six tries — green marks the right letter in the right spot, yellow the right letter misplaced, gray the misses. On-screen + physical keyboard, persistent stats. Pure vanilla JS, zero dependencies.",
+      "desc": "Guess the hidden five-letter word in six tries \u2014 green marks the right letter in the right spot, yellow the right letter misplaced, gray the misses. On-screen + physical keyboard, persistent stats. Pure vanilla JS, zero dependencies.",
       "path": "games/wordle/",
       "added": "2026-07-25"
     },
@@ -286,8 +294,8 @@
       "id": "trivia-90210",
       "file": "90210Trivia.aa",
       "title": "90210 Trivia",
-      "icon": "💎",
-      "desc": "A glam Beverly Hills pop-culture trivia game — beat the clock on 90210, Rodeo Drive, and Pink Palace questions, stack a streak multiplier, and climb from Peach Pit Local to Rodeo Drive Royalty. Self-contained HTML, zero runtime dependencies.",
+      "icon": "\ud83d\udc8e",
+      "desc": "A glam Beverly Hills pop-culture trivia game \u2014 beat the clock on 90210, Rodeo Drive, and Pink Palace questions, stack a streak multiplier, and climb from Peach Pit Local to Rodeo Drive Royalty. Self-contained HTML, zero runtime dependencies.",
       "path": "games/trivia-90210/",
       "added": "2026-07-25"
     },
@@ -295,8 +303,8 @@
       "id": "rodeo-runner",
       "file": "RodeoDriveRunner.aa",
       "title": "Rodeo Drive Runner",
-      "icon": "🏎️",
-      "desc": "A glam Beverly Hills endless runner — cruise a gold convertible down Rodeo Drive, swerve the traffic, and snag champagne and diamonds to stack a combo multiplier. Speed ramps the longer you last. Pure Canvas 2D, zero runtime dependencies.",
+      "icon": "\ud83c\udfce\ufe0f",
+      "desc": "A glam Beverly Hills endless runner \u2014 cruise a gold convertible down Rodeo Drive, swerve the traffic, and snag champagne and diamonds to stack a combo multiplier. Speed ramps the longer you last. Pure Canvas 2D, zero runtime dependencies.",
       "path": "games/rodeo-runner/",
       "added": "2026-07-25"
     },
@@ -304,8 +312,8 @@
       "id": "beverly-hills-slots",
       "file": "PinkPalaceSlots.aa",
       "title": "Beverly Hills Slots",
-      "icon": "🎰",
-      "desc": "A glam 90210 slot machine — spin a gold-and-marble 3-reel cabinet, chase the Pink Palace diamond jackpot, set your bet, and ride a persistent best-bank. Self-contained HTML, zero runtime dependencies.",
+      "icon": "\ud83c\udfb0",
+      "desc": "A glam 90210 slot machine \u2014 spin a gold-and-marble 3-reel cabinet, chase the Pink Palace diamond jackpot, set your bet, and ride a persistent best-bank. Self-contained HTML, zero runtime dependencies.",
       "path": "games/beverly-hills-slots/",
       "added": "2026-07-25"
     }
diff --git a/games/baguette-brawl/BaguetteBrawl.aa b/games/baguette-brawl/BaguetteBrawl.aa
new file mode 100644
index 0000000..e8ac114
--- /dev/null
+++ b/games/baguette-brawl/BaguetteBrawl.aa
@@ -0,0 +1,915 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<title>Baguette Brawl — Duel de Paris</title>
+<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🥖</text></svg>">
+<style>
+:root{
+  --bg:#f7efe6; --surface:#fff9f1; --text:#3a2c22; --muted:#a08c7a;
+  --accent:#c65f38; --accent2:#e0a458;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%}
+body{
+  background:radial-gradient(120% 120% at 50% 0%,#20222e 0%,#12131a 55%,#0a0a0f 100%);
+  color:var(--text);
+  font-family:-apple-system,'Segoe UI','Helvetica Neue',Arial,sans-serif;
+  display:flex;align-items:center;justify-content:center;
+  min-height:100%;padding:20px;overflow:hidden;
+}
+#wrap{
+  position:relative;width:960px;max-width:100%;
+  aspect-ratio:960/540;border-radius:14px;overflow:hidden;
+  box-shadow:0 16px 48px rgba(0,0,0,.55), 0 0 0 1px rgba(224,164,88,.25);
+  background:#1a1720;
+}
+canvas{display:block;width:100%;height:100%;image-rendering:auto}
+
+/* overlays */
+.overlay{
+  position:absolute;inset:0;display:flex;flex-direction:column;
+  align-items:center;justify-content:center;text-align:center;
+  background:linear-gradient(160deg,rgba(18,15,22,.86),rgba(30,20,14,.92));
+  backdrop-filter:blur(2px);gap:16px;padding:32px;
+  color:var(--bg);z-index:10;
+}
+.overlay.hide{display:none}
+.kicker{
+  font-size:12px;letter-spacing:.28em;text-transform:uppercase;
+  color:var(--accent2);font-weight:600;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;
+}
+h1{
+  font-family:'Playfair Display',Georgia,'Bodoni MT',serif;
+  font-weight:800;font-size:clamp(38px,7vw,67px);line-height:.98;
+  color:var(--surface);letter-spacing:-.01em;
+  text-shadow:0 2px 24px rgba(224,164,88,.25);
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.09s;
+}
+h1 .em{color:var(--accent2);font-style:italic}
+.sub{
+  max-width:520px;color:#e8dfd2;font-size:16px;line-height:1.5;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.18s;
+}
+.btns{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.27s;}
+button.play{
+  font-family:inherit;cursor:pointer;border:none;border-radius:12px;
+  padding:15px 26px;font-size:15px;font-weight:700;letter-spacing:.02em;
+  color:#fff;background:linear-gradient(135deg,var(--accent),#a8471f);
+  box-shadow:0 8px 24px rgba(198,95,56,.4);transition:transform .16s ease,box-shadow .16s ease;
+}
+button.play.alt{background:linear-gradient(135deg,#5c7d8f,#38505d);box-shadow:0 8px 24px rgba(75,107,122,.4)}
+button.play:hover{transform:translateY(-2px);box-shadow:0 16px 40px rgba(198,95,56,.5)}
+button.play.alt:hover{box-shadow:0 16px 40px rgba(75,107,122,.5)}
+
+.diffrow{display:flex;align-items:center;gap:10px;font-size:13px;color:#e0d7c9;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.22s;}
+.diffrow label{letter-spacing:.12em;text-transform:uppercase;font-size:11px;color:var(--accent2);font-weight:700}
+select#difficulty{
+  font-family:inherit;font-size:13px;color:#f2e9dd;background:#2a2530;
+  border:1px solid #4a4150;border-radius:8px;padding:7px 12px;cursor:pointer;
+}
+
+.controls{
+  display:flex;gap:32px;flex-wrap:wrap;justify-content:center;margin-top:6px;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.36s;
+}
+.ctrlcard{
+  background:rgba(255,249,241,.06);border:1px solid rgba(224,164,88,.22);
+  border-radius:12px;padding:14px 18px;min-width:200px;text-align:left;
+}
+.ctrlcard h4{font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:var(--accent2);margin-bottom:8px}
+.ctrlcard .row{display:flex;justify-content:space-between;font-size:13px;color:#e0d7c9;margin:3px 0;gap:12px}
+kbd{
+  background:#2a2530;border:1px solid #4a4150;border-bottom-width:2px;border-radius:5px;
+  padding:1px 7px;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#f2e9dd;
+}
+.result-name{font-family:'Playfair Display',Georgia,serif;font-style:italic}
+.foot{position:absolute;bottom:12px;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:var(--muted)}
+@keyframes fadeUp{from{opacity:0;transform:translateY(28px)}to{opacity:1;transform:none}}
+
+/* pause overlay is lighter */
+#pause{background:linear-gradient(160deg,rgba(18,15,22,.72),rgba(30,20,14,.8))}
+
+/* ---- touch controls (mobile only) ---- */
+#touch{
+  position:absolute;inset:auto 0 0 0;z-index:8;display:none;
+  justify-content:space-between;padding:0 14px 14px;pointer-events:none;
+  user-select:none;-webkit-user-select:none;touch-action:none;
+}
+#touch .pad,#touch .acts{display:flex;gap:12px;pointer-events:auto}
+#touch button{
+  width:58px;height:58px;border-radius:50%;border:1px solid rgba(224,164,88,.4);
+  background:rgba(30,25,32,.55);color:#f2e9dd;font-size:20px;font-weight:800;
+  backdrop-filter:blur(3px);cursor:pointer;
+}
+#touch button.down{background:rgba(198,95,56,.75);transform:scale(.94)}
+#touch button.sup{background:rgba(224,161,42,.35);font-size:15px}
+@media (hover:none) and (pointer:coarse){ #touch{display:flex} .controls{display:none} }
+
+</style>
+</head>
+<body>
+<div id="wrap">
+  <canvas id="c" width="960" height="540"></canvas>
+
+  <!-- MENU -->
+  <div id="menu" class="overlay">
+    <div class="kicker">Sous la lune de Montmartre</div>
+    <h1>Baguette <span class="em">Brawl</span></h1>
+    <p class="sub">Two crusty duelists face off beneath the Eiffel Tower. Swing your loaf, guard your crust, charge the meter and unleash the <b>Crust Crusher</b>. First to <b>2 rounds</b> wins the boulangerie.</p>
+    <div class="diffrow">
+      <label for="difficulty">Chef IA</label>
+      <select id="difficulty">
+        <option value="easy">Apprenti (facile)</option>
+        <option value="normal" selected>Boulanger (normal)</option>
+        <option value="hard">Maître (difficile)</option>
+      </select>
+    </div>
+    <div class="btns">
+      <button class="play" data-mode="1">Solo vs. Chef IA</button>
+      <button class="play alt" data-mode="2">Deux Joueurs</button>
+    </div>
+    <div class="controls">
+      <div class="ctrlcard">
+        <h4>Pierre · Joueur 1</h4>
+        <div class="row"><span>Move / Jump</span><span><kbd>A</kbd><kbd>D</kbd><kbd>W</kbd></span></div>
+        <div class="row"><span>Jab · Heavy</span><span><kbd>J</kbd><kbd>K</kbd></span></div>
+        <div class="row"><span>Guard · Super</span><span><kbd>L</kbd><kbd>U</kbd></span></div>
+      </div>
+      <div class="ctrlcard">
+        <h4>Gaston · Joueur 2</h4>
+        <div class="row"><span>Move / Jump</span><span><kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd></span></div>
+        <div class="row"><span>Jab · Heavy</span><span><kbd>,</kbd><kbd>.</kbd></span></div>
+        <div class="row"><span>Guard · Super</span><span><kbd>/</kbd><kbd>;</kbd></span></div>
+      </div>
+    </div>
+    <div class="foot">Pause <kbd>P</kbd> · Mute <kbd>M</kbd> · Un jeu de pain · Paris</div>
+  </div>
+
+  <!-- PAUSE -->
+  <div id="pause" class="overlay hide">
+    <div class="kicker">Pause</div>
+    <h1>En pause</h1>
+    <div class="btns">
+      <button class="play" data-pause="resume">Reprendre</button>
+      <button class="play alt" data-pause="menu">Menu</button>
+    </div>
+    <div class="foot"><kbd>P</kbd> pour reprendre</div>
+  </div>
+
+  <!-- ROUND / RESULT -->
+  <div id="result" class="overlay hide">
+    <div class="kicker" id="resKicker">Manche terminée</div>
+    <h1 id="resTitle">—</h1>
+    <p class="sub" id="resSub"></p>
+    <div class="btns" id="resBtns"></div>
+  </div>
+
+  <!-- TOUCH CONTROLS (mobile) -->
+  <div id="touch">
+    <div class="pad">
+      <button data-key="a">◀</button>
+      <button data-key="w">▲</button>
+      <button data-key="d">▶</button>
+    </div>
+    <div class="acts">
+      <button data-key="l">🛡</button>
+      <button data-key="j">J</button>
+      <button data-key="k">K</button>
+      <button data-key="u" class="sup">SUP</button>
+    </div>
+  </div>
+</div>
+
+<script>
+/* ============================================================================
+   Baguette Brawl — Duel de Paris
+   A 2D fighting game where two crusty baguette duelists brawl beneath the
+   Eiffel Tower. Vanilla JS + Canvas, no dependencies, file:// friendly.
+
+   Rebuilt & extended from the Model Arena starting artifact:
+     - press-edge input (attacks no longer auto-repeat while held)
+     - clean round state machine (intro → fight → KO → result), no dt*0 hack
+     - super meter + unblockable "Crust Crusher" special
+     - selectable AI difficulty with real spacing / anti-air / whiff punish
+     - on-screen touch controls for mobile
+     - pause (P / Esc), rematch (R), slow-mo KO, "FIGHT!" / "K.O." banners
+   ========================================================================== */
+(() => {
+'use strict';
+
+const cv  = document.getElementById('c');
+const ctx = cv.getContext('2d');
+const W = 960, H = 540;
+const GROUND = 470;
+
+const menuEl   = document.getElementById('menu');
+const resEl    = document.getElementById('result');
+const pauseEl  = document.getElementById('pause');
+const resTitle = document.getElementById('resTitle');
+const resSub   = document.getElementById('resSub');
+const resKicker= document.getElementById('resKicker');
+const resBtns  = document.getElementById('resBtns');
+const touchEl  = document.getElementById('touch');
+
+// ---------------------------------------------------------------------------
+// Audio (tiny synth, no external files)
+// ---------------------------------------------------------------------------
+let AC = null, muted = false;
+function actx(){ if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } return AC; }
+function tone(freq, dur, type='sine', vol=.15, slideTo=null){
+  if(muted) return; const a=actx(); if(!a) return;
+  const o=a.createOscillator(), g=a.createGain();
+  o.type=type; o.frequency.setValueAtTime(freq,a.currentTime);
+  if(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo,a.currentTime+dur);
+  g.gain.setValueAtTime(vol,a.currentTime);
+  g.gain.exponentialRampToValueAtTime(.0001,a.currentTime+dur);
+  o.connect(g); g.connect(a.destination); o.start(); o.stop(a.currentTime+dur+.02);
+}
+function noise(dur, vol=.2, hp=800){
+  if(muted) return; const a=actx(); if(!a) return;
+  const n=Math.floor(a.sampleRate*dur), buf=a.createBuffer(1,n,a.sampleRate), d=buf.getChannelData(0);
+  for(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*(1-i/n);
+  const s=a.createBufferSource(); s.buffer=buf;
+  const f=a.createBiquadFilter(); f.type='highpass'; f.frequency.value=hp;
+  const g=a.createGain(); g.gain.value=vol;
+  s.connect(f); f.connect(g); g.connect(a.destination); s.start();
+}
+const sfx = {
+  swing(){ noise(.12,.12,1200); },
+  hit(){ noise(.16,.32,500); tone(120,.1,'square',.14,70); },
+  crush(){ noise(.26,.4,300); tone(90,.28,'sawtooth',.22,50); },
+  block(){ tone(880,.09,'triangle',.16,660); noise(.05,.08,2500); },
+  jump(){ tone(300,.12,'sine',.1,520); },
+  super(){ tone(440,.18,'square',.16,880); setTimeout(()=>tone(660,.2,'square',.16,1200),90); },
+  ko(){ tone(400,.5,'sawtooth',.2,80); },
+  count(){ tone(660,.08,'triangle',.12); },
+  fight(){ tone(523,.1,'triangle',.16); setTimeout(()=>tone(784,.18,'triangle',.18),100); },
+  win(){ tone(523,.12,'triangle',.16); setTimeout(()=>tone(659,.12,'triangle',.16),120); setTimeout(()=>tone(784,.2,'triangle',.18),240); }
+};
+
+// ---------------------------------------------------------------------------
+// Input — held keys + press-edge (justPressed) so attacks fire once per tap
+// ---------------------------------------------------------------------------
+const keys = new Set();
+const justPressed = new Set();
+function normKey(e){ return e.key.length===1 ? e.key.toLowerCase() : e.key; }
+addEventListener('keydown', e=>{
+  const k=normKey(e);
+  if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' ','/'].includes(e.key)) e.preventDefault();
+  if(!keys.has(k)) justPressed.add(k);   // edge only on the first frame held
+  keys.add(k);
+  handleGlobalKeys(k);
+});
+addEventListener('keyup', e=>{ keys.delete(normKey(e)); });
+function clearEdges(){ justPressed.clear(); }
+
+function handleGlobalKeys(k){
+  if(k==='m'){ muted=!muted; }
+  if(state==='fight'){
+    if(k==='p'||k==='Escape'){ togglePause(); }
+  } else if(state==='paused'){
+    if(k==='p'||k==='Escape'){ togglePause(); }
+  }
+  if(state==='result' && (k==='r'||k==='Enter')){ const b=resBtns.querySelector('button'); if(b) b.click(); }
+}
+
+// Touch controls write into the same `keys`/`justPressed` sets as P1.
+function bindTouch(){
+  if(!touchEl) return;
+  touchEl.querySelectorAll('[data-key]').forEach(btn=>{
+    const key = btn.getAttribute('data-key');
+    const press = (e)=>{ e.preventDefault(); if(!keys.has(key)) justPressed.add(key); keys.add(key); btn.classList.add('down'); };
+    const release= (e)=>{ e.preventDefault(); keys.delete(key); btn.classList.remove('down'); };
+    btn.addEventListener('touchstart',press,{passive:false});
+    btn.addEventListener('touchend',release,{passive:false});
+    btn.addEventListener('touchcancel',release,{passive:false});
+    btn.addEventListener('mousedown',press);
+    btn.addEventListener('mouseup',release);
+    btn.addEventListener('mouseleave',release);
+  });
+}
+
+// ---------------------------------------------------------------------------
+// Fighter
+// ---------------------------------------------------------------------------
+const P1CTRL = {left:'a',right:'d',jump:'w',light:'j',heavy:'k',block:'l',super:'u'};
+const P2CTRL = {left:'ArrowLeft',right:'ArrowRight',jump:'ArrowUp',light:',',heavy:'.',block:'/',super:';'};
+
+function makeFighter(x, facing, cfg){
+  return {
+    x, y:GROUND, vx:0, vy:0, facing, onGround:true,
+    hp:100, maxhp:100, meter:0, maxMeter:100,
+    state:'idle',        // idle | attack | hurt | ko
+    atk:null, atkT:0, atkDur:0, atkHit:false,
+    hurtT:0, blockT:0, blocking:false,
+    swing:0, walkPhase:0,
+    color:cfg.color, beret:cfg.beret, name:cfg.name,
+    isAI:cfg.isAI||false, ctrl:cfg.ctrl,
+    hitFlash:0, wins:0, _t:0, _think:0
+  };
+}
+
+let f1, f2, mode=1, difficulty='normal', state='menu';
+let roundNum=1, roundTime=60, timeLeft=60, roundPhase='intro';
+let freeze=0, shake=0, particles=[], clouds=[], stars=[], koTimer=null;
+let showAnnounce='', announceT=0, countdownT=0, lastCount=99;
+
+const AI_TUNE = {
+  easy:   {react:.55, aggr:.35, blockCh:.30, superCh:.15, whiff:.25, jumpCh:.006, speed:2.6},
+  normal: {react:.30, aggr:.55, blockCh:.55, superCh:.40, whiff:.55, jumpCh:.010, speed:3.0},
+  hard:   {react:.14, aggr:.75, blockCh:.80, superCh:.70, whiff:.85, jumpCh:.016, speed:3.3}
+};
+
+function initClouds(){
+  clouds=[]; for(let i=0;i<5;i++) clouds.push({x:Math.random()*W,y:40+Math.random()*120,s:.15+Math.random()*.25,w:80+Math.random()*90});
+  stars=[]; for(let i=0;i<70;i++) stars.push({x:Math.random()*W,y:Math.random()*300,r:Math.random()*1.4+.3,t:Math.random()*6});
+}
+initClouds();
+
+function startGame(m){
+  mode=m; roundNum=1;
+  f1=makeFighter(300, 1,{color:'#e0a458',beret:'#c62f2f',name:'Pierre',ctrl:P1CTRL});
+  f2=makeFighter(660,-1,{color:'#d9974a',beret:'#2f4b6b',name:'Gaston',ctrl:P2CTRL,isAI:(m===1)});
+  f1.wins=0; f2.wins=0;
+  actx();
+  startRound();
+}
+function resetFighter(f,x,facing){
+  Object.assign(f,{x,y:GROUND,vx:0,vy:0,hp:100,meter:f.meter*0.5|0,
+    state:'idle',atk:null,atkT:0,atkDur:0,atkHit:false,hurtT:0,blockT:0,
+    blocking:false,swing:0,facing,hitFlash:0,onGround:true});
+}
+function startRound(){
+  clearTimeout(koTimer); koTimer=null;
+  resEl.classList.add('hide'); menuEl.classList.add('hide'); pauseEl.classList.add('hide');
+  resetFighter(f1,300,1);
+  resetFighter(f2,660,-1);
+  timeLeft=roundTime; particles=[]; freeze=0; shake=0;
+  showAnnounce=`MANCHE ${roundNum}`; announceT=1.6; countdownT=3.2; lastCount=99;
+  roundPhase='intro';
+  state='fight';
+}
+
+// ---------------------------------------------------------------------------
+// Combat tuning
+// ---------------------------------------------------------------------------
+const ATK = {
+  light:{dur:.34, active:[.08,.18], reach:78, dmg:7,  kb:3.2, stun:.28, meter:6,  cost:0},
+  heavy:{dur:.62, active:[.22,.36], reach:96, dmg:16, kb:8.5, stun:.5,  meter:10, cost:0},
+  // Crust Crusher — unblockable overhead super, costs a full meter
+  super:{dur:.72, active:[.30,.46], reach:112,dmg:30, kb:12,  stun:.7,  meter:0,  cost:100, unblock:true}
+};
+
+function tryAttack(f, type){
+  if(f.state==='attack'||f.state==='hurt'||f.state==='ko') return;
+  const a = ATK[type];
+  if(type==='super' && f.meter < a.cost) return;
+  if(!f.onGround && type!=='light') return;    // only jabs in the air
+  if(type==='super'){ f.meter=0; sfx.super(); spawnParticles(f.x,f.y-70,20,'#ffe7b0',7); }
+  else sfx.swing();
+  f.state='attack'; f.atk=type; f.atkT=0; f.atkDur=a.dur; f.atkHit=false;
+}
+
+function addMeter(f, amt){ f.meter=Math.min(f.maxMeter, f.meter+amt); }
+
+function updateFighter(f, other, dt){
+  const c=f.ctrl;
+  const canAct = f.state!=='hurt' && f.state!=='ko';
+  f.blocking=false;
+
+  if(f.onGround && f.state!=='attack'){ f.facing = other.x < f.x ? -1 : 1; }
+
+  if(!f.isAI){
+    let mv=0;
+    if(canAct && f.state!=='attack'){
+      if(keys.has(c.left))  mv-=1;
+      if(keys.has(c.right)) mv+=1;
+      if(keys.has(c.block)){ f.blocking=true; mv=0; }
+    }
+    f.vx = mv*3.4;
+    if(canAct && justPressed.has(c.jump) && f.onGround){ f.vy=-13.4; f.onGround=false; sfx.jump(); }
+    if(canAct){
+      if(justPressed.has(c.super))      tryAttack(f,'super');
+      else if(justPressed.has(c.light)) tryAttack(f,'light');
+      else if(justPressed.has(c.heavy)) tryAttack(f,'heavy');
+    }
+  } else {
+    aiControl(f, other, dt);
+  }
+
+  if(f.blocking) f.blockT=Math.min(f.blockT+dt,.2);
+
+  // physics
+  f.vy += 42*dt;
+  f.y  += f.vy;
+  f.x  += f.vx;
+  if(f.y>=GROUND){ f.y=GROUND; f.vy=0; f.onGround=true; } else f.onGround=false;
+  f.x = Math.max(60, Math.min(W-60, f.x));
+
+  // simple body separation so they don't overlap
+  const gap=Math.abs(f.x-other.x);
+  if(gap<64 && Math.abs(f.y-other.y)<90){
+    const push=(64-gap)/2, dir=f.x<other.x?-1:1;
+    f.x+=dir*push*0.5; other.x-=dir*push*0.5;
+    f.x=Math.max(60,Math.min(W-60,f.x)); other.x=Math.max(60,Math.min(W-60,other.x));
+  }
+
+  if(Math.abs(f.vx)>.2 && f.onGround) f.walkPhase+=dt*12; else f.walkPhase*=.8;
+
+  if(f.state==='attack'){
+    f.atkT+=dt;
+    const a=ATK[f.atk];
+    const p=f.atkT/f.atkDur;
+    f.swing = swingCurve(p);
+    if(!f.atkHit && p>=a.active[0] && p<=a.active[1]){
+      const dx=other.x-f.x;
+      const inFront = (f.facing>0 && dx>0)||(f.facing<0 && dx<0);
+      const dist=Math.abs(dx);
+      const vgap=Math.abs(other.y-f.y);
+      if(inFront && dist < a.reach+40 && dist>10 && vgap<120){
+        f.atkHit=true; addMeter(f,a.meter); landHit(f, other, a);
+      }
+    }
+    if(f.atkT>=f.atkDur){ f.state='idle'; f.atk=null; f.swing=0; }
+  } else {
+    f.swing*=.7;
+  }
+
+  if(f.state==='hurt'){ f.hurtT-=dt; if(f.hurtT<=0) f.state='idle'; }
+  if(f.hitFlash>0) f.hitFlash-=dt;
+  if(f.blockT>0 && !f.blocking) f.blockT-=dt*.5;
+}
+
+function swingCurve(p){
+  if(p<.35) return -(p/.35)*.5;
+  const q=(p-.35)/.65;
+  return -0.5 + q*q*(3-2*q)*1.9;
+}
+
+function landHit(attacker, target, a){
+  if(target.state==='ko') return;
+  let dmg=a.dmg, kb=a.kb;
+  const facingRight = target.facing>0;
+  const attackerOnGuardSide = (facingRight && attacker.x>target.x)||(!facingRight && attacker.x<target.x);
+  const blocked = !a.unblock && target.blocking && target.onGround && attackerOnGuardSide;
+
+  if(blocked){
+    dmg*=0.18; kb*=.5;
+    addMeter(target,4);                    // reward defense a little
+    sfx.block();
+    spawnParticles(target.x+target.facing*-20, target.y-70, 6, '#ffe7b0', 3);
+    shake=Math.max(shake,4); target.blockT=.2;
+  } else {
+    a.unblock ? sfx.crush() : sfx.hit();
+    addMeter(target, a.unblock?0:6);
+    target.state='hurt'; target.hurtT=a.stun;
+    spawnParticles(target.x, target.y-70, a.unblock?26:16, attacker.color, a.unblock?9:6);
+    spawnParticles(target.x, target.y-80, 8, '#fff3dd', 5);
+    target.hitFlash=.18;
+    shake=Math.max(shake, a.dmg>10?11:6);
+    freeze=a.unblock?.11:(a.dmg>10?.07:.035);
+  }
+  target.hp=Math.max(0,target.hp-dmg);
+  target.vx = attacker.facing*kb;
+  if(!blocked && a.dmg>10){ target.vy=-4-(a.unblock?3:0); target.onGround=false; }
+  if(target.hp<=0){ target.state='ko'; target.vx=attacker.facing*5; target.vy=-8; target.onGround=false; sfx.ko(); }
+}
+
+// ---------------------------------------------------------------------------
+// AI
+// ---------------------------------------------------------------------------
+function aiControl(f, o, dt){
+  if(roundPhase!=='fight'){ f.vx*=.85; return; }   // no thinking during intro / ko slow-mo
+  const T=AI_TUNE[difficulty]||AI_TUNE.normal;
+  f._t=(f._t||0)+dt; f._think=(f._think||0)-dt;
+  const dist=Math.abs(o.x-f.x);
+  const dir=o.x<f.x?-1:1;
+  let mv=0;
+
+  if(f.state==='attack'||f.state==='hurt'){ f.vx*=.85; return; }
+
+  // anti-air: opponent airborne & close → block or heavy up-swing
+  if(!o.onGround && dist<120 && Math.random()<T.blockCh*0.5){ f.blocking=true; f.vx=0; return; }
+
+  // block when opponent commits an attack in range
+  if(o.state==='attack' && dist<130 && Math.random()<T.blockCh*0.18){ f.blocking=true; f.vx=0; return; }
+
+  // fire super when charged & in range
+  if(f.meter>=100 && dist<110 && Math.random()<T.superCh*0.1){ tryAttack(f,'super'); return; }
+
+  const ideal=90;
+  if(dist>ideal+30) mv=dir;
+  else if(dist<ideal-30) mv=-dir;
+  else {
+    if(f._think<=0){
+      f._think = T.react + Math.random()*.4;
+      if(Math.random()<T.aggr){
+        if(dist<100 && Math.random()<.5) tryAttack(f,'heavy');
+        else tryAttack(f,'light');
+      }
+    }
+    mv = (Math.random()<.3 ? -dir : 0);
+  }
+
+  // punish a whiffed opponent attack: dash in
+  if(o.state==='attack' && o.atkHit===false && dist<160 && dist>90 && Math.random()<T.whiff*0.1){ mv=dir; }
+
+  if(f.onGround && Math.random()<T.jumpCh && dist<180){ f.vy=-13; f.onGround=false; }
+  f.vx=mv*T.speed;
+}
+
+// ---------------------------------------------------------------------------
+// Particles
+// ---------------------------------------------------------------------------
+function spawnParticles(x,y,n,color,spd){
+  for(let i=0;i<n;i++){
+    const ang=Math.random()*Math.PI*2, s=Math.random()*spd+1;
+    particles.push({x,y,vx:Math.cos(ang)*s,vy:Math.sin(ang)*s-2,life:1,color,r:Math.random()*3+1.5});
+  }
+}
+function updateParticles(dt){
+  for(let i=particles.length-1;i>=0;i--){
+    const p=particles[i];
+    p.vy+=18*dt; p.x+=p.vx; p.y+=p.vy; p.life-=dt*1.7;
+    if(p.life<=0||p.y>GROUND+10) particles.splice(i,1);
+  }
+}
+
+// ---------------------------------------------------------------------------
+// Round flow
+// ---------------------------------------------------------------------------
+function checkRoundEnd(){
+  if(roundPhase!=='fight') return;
+  let winner=null;
+  if(f1.hp<=0 && f2.hp<=0) winner='draw';
+  else if(f1.hp<=0) winner=f2;
+  else if(f2.hp<=0) winner=f1;
+  else if(timeLeft<=0){
+    if(f1.hp>f2.hp) winner=f1; else if(f2.hp>f1.hp) winner=f2; else winner='draw';
+  }
+  if(winner!==null){
+    roundPhase='ko';
+    if(winner!=='draw'){ showAnnounce='K.O.'; announceT=1.4; }
+    clearTimeout(koTimer);
+    koTimer=setTimeout(()=>{ koTimer=null; endRound(winner); }, 1100);
+  }
+}
+function endRound(winner){
+  if(winner!=='draw' && winner) winner.wins++;
+  const matchOver = f1.wins>=2 || f2.wins>=2;
+  state='result';
+  if(matchOver){
+    const champ = f1.wins>f2.wins? f1 : f2;
+    sfx.win();
+    resKicker.textContent='Vainqueur du duel';
+    resTitle.innerHTML = `<span class="result-name">${champ.name}</span> l'emporte !`;
+    resSub.textContent = `${champ.name} remporte le match ${Math.max(f1.wins,f2.wins)}–${Math.min(f1.wins,f2.wins)} et devient le Roi de la Baguette.`;
+    resBtns.innerHTML='';
+    addBtn('Rejouer', ()=>startGame(mode), false);
+    addBtn('Menu', ()=>toMenu(), true);
+  } else {
+    resKicker.textContent='Manche terminée';
+    if(winner==='draw'){ resTitle.textContent='Égalité !'; resSub.textContent='Personne ne cède une miette.'; }
+    else { resTitle.innerHTML=`<span class="result-name">${winner.name}</span> gagne la manche`; resSub.textContent=`Score — Pierre ${f1.wins} · ${f2.wins} Gaston`; }
+    resBtns.innerHTML='';
+    roundNum++;
+    addBtn(`Manche ${roundNum} →`, ()=>startRound(), false);
+    addBtn('Menu', ()=>toMenu(), true);
+  }
+  resEl.classList.remove('hide');
+}
+function addBtn(label, fn, alt){
+  const b=document.createElement('button'); b.className='play'+(alt?' alt':''); b.textContent=label;
+  b.onclick=fn; resBtns.appendChild(b);
+}
+function toMenu(){ clearTimeout(koTimer); koTimer=null; state='menu'; resEl.classList.add('hide'); pauseEl.classList.add('hide'); menuEl.classList.remove('hide'); }
+function togglePause(){
+  if(state==='fight'){ state='paused'; pauseEl.classList.remove('hide'); }
+  else if(state==='paused'){ state='fight'; pauseEl.classList.add('hide'); }
+}
+
+// ---------------------------------------------------------------------------
+// Drawing
+// ---------------------------------------------------------------------------
+function skyGradient(){
+  const g=ctx.createLinearGradient(0,0,0,H);
+  g.addColorStop(0,'#241a35'); g.addColorStop(.4,'#4a2e46'); g.addColorStop(.7,'#8a4a45'); g.addColorStop(1,'#c9754a');
+  return g;
+}
+function drawScene(){
+  ctx.fillStyle=skyGradient(); ctx.fillRect(0,0,W,H);
+  // moon
+  ctx.save(); ctx.shadowColor='rgba(255,236,200,.7)'; ctx.shadowBlur=40;
+  ctx.fillStyle='#fdf1d6'; ctx.beginPath(); ctx.arc(770,110,46,0,7); ctx.fill(); ctx.restore();
+  // stars
+  for(const s of stars){ s.t+=.03; const tw=.5+.5*Math.sin(s.t);
+    ctx.globalAlpha=.3+tw*.5; ctx.fillStyle='#fff7e6'; ctx.fillRect(s.x,s.y,s.r,s.r); }
+  ctx.globalAlpha=1;
+  // clouds
+  for(const cl of clouds){ cl.x-=cl.s; if(cl.x<-cl.w) cl.x=W+cl.w;
+    ctx.fillStyle='rgba(60,40,55,.35)';
+    ctx.beginPath(); ctx.ellipse(cl.x,cl.y,cl.w,cl.w*.32,0,0,7); ctx.fill(); }
+  drawEiffel(480, GROUND+40, 300);
+  drawSkyline();
+  // ground
+  const gg=ctx.createLinearGradient(0,GROUND+18,0,H);
+  gg.addColorStop(0,'#3a2c22'); gg.addColorStop(1,'#241a14');
+  ctx.fillStyle=gg; ctx.fillRect(0,GROUND+18,W,H-GROUND);
+  ctx.strokeStyle='rgba(224,164,88,.15)'; ctx.lineWidth=2;
+  ctx.beginPath(); ctx.moveTo(0,GROUND+18); ctx.lineTo(W,GROUND+18); ctx.stroke();
+  for(let x=0;x<W;x+=46){ ctx.beginPath(); ctx.moveTo(x,GROUND+22); ctx.lineTo(x+20,H);
+    ctx.strokeStyle='rgba(0,0,0,.18)'; ctx.stroke(); }
+}
+function draw(){
+  drawScene();
+  ctx.save();
+  if(shake>0){ ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake); }
+  if(f1&&f2){
+    drawShadow(f1); drawShadow(f2);
+    // draw the fighter that's further back (higher on screen / smaller x-depth) first
+    if(f1.y<=f2.y){ drawFighter(f1); drawFighter(f2); } else { drawFighter(f2); drawFighter(f1); }
+  }
+  for(const p of particles){ ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.color;
+    ctx.fillRect(p.x-p.r/2,p.y-p.r/2,p.r,p.r); }
+  ctx.globalAlpha=1;
+  ctx.restore();
+
+  if(f1&&f2) drawHUD();
+  if(announceT>0) drawAnnounce();
+  if(countdownT>0 && countdownT<3.2 && roundPhase==='intro') drawCountdown();
+}
+
+function drawShadow(f){
+  const sc=f.onGround?1:0.6;
+  ctx.fillStyle='rgba(0,0,0,.28)';
+  ctx.beginPath(); ctx.ellipse(f.x,GROUND+18,34*sc,9*sc,0,0,7); ctx.fill();
+}
+function drawEiffel(cx,baseY,h){
+  ctx.save();
+  ctx.strokeStyle='rgba(30,18,26,.65)'; ctx.fillStyle='rgba(30,18,26,.55)';
+  ctx.lineWidth=3;
+  const topY=baseY-h, topW=6, baseW=110, midY=baseY-h*.42, midW=52, plY=baseY-h*.14, plW=88;
+  function leg(sx){
+    ctx.beginPath();
+    ctx.moveTo(cx-baseW*sx, baseY);
+    ctx.quadraticCurveTo(cx-plW*sx*.7, plY, cx-midW*sx, midY);
+    ctx.quadraticCurveTo(cx-topW*sx*3, topY+h*.25, cx-topW*sx, topY);
+    ctx.stroke();
+  }
+  leg(1); leg(-1);
+  ctx.beginPath(); ctx.moveTo(cx-baseW*.72,plY); ctx.quadraticCurveTo(cx,plY-38,cx+baseW*.72,plY); ctx.stroke();
+  ctx.fillRect(cx-plW,plY-4,plW*2,8);
+  ctx.fillRect(cx-midW,midY-3,midW*2,6);
+  ctx.lineWidth=1.5; ctx.strokeStyle='rgba(30,18,26,.4)';
+  for(let i=0;i<6;i++){ const y=plY-(plY-midY)*(i/6); const wl=plW-(plW-midW)*(i/6);
+    ctx.beginPath(); ctx.moveTo(cx-wl,y); ctx.lineTo(cx+wl,y); ctx.stroke(); }
+  ctx.strokeStyle='rgba(30,18,26,.65)'; ctx.lineWidth=3;
+  ctx.beginPath(); ctx.moveTo(cx,topY); ctx.lineTo(cx,topY-18); ctx.stroke();
+  ctx.fillStyle='rgba(255,220,150,.9)'; ctx.beginPath(); ctx.arc(cx,topY-20,3,0,7); ctx.fill();
+  ctx.restore();
+}
+function drawSkyline(){
+  ctx.fillStyle='rgba(25,16,24,.5)';
+  const b=[[40,60,120],[130,90,90],[210,40,150],[720,70,120],[810,110,100],[900,50,150]];
+  for(const [x,w,hh] of b){ ctx.fillRect(x,GROUND+18-hh,w,hh);
+    ctx.fillStyle='rgba(255,210,140,.18)';
+    for(let wy=GROUND+18-hh+10;wy<GROUND+8;wy+=18) for(let wx=x+6;wx<x+w-6;wx+=16) if((wx*7+wy*13)%5<2) ctx.fillRect(wx,wy,4,6);
+    ctx.fillStyle='rgba(25,16,24,.5)';
+  }
+}
+function drawFighter(f){
+  ctx.save();
+  ctx.translate(f.x, f.y);
+  ctx.scale(f.facing,1);
+
+  const hurt = f.hitFlash>0 && Math.floor(f.hitFlash*30)%2===0;
+  const kod = f.state==='ko';
+  if(kod) ctx.rotate(f.facing* -0.9);
+
+  const step=Math.sin(f.walkPhase)*8;
+  ctx.strokeStyle='#8a5a2a'; ctx.lineWidth=6; ctx.lineCap='round';
+  ctx.beginPath(); ctx.moveTo(-8,-8); ctx.lineTo(-10-step*0.3, 0); ctx.stroke();
+  ctx.beginPath(); ctx.moveTo(8,-8);  ctx.lineTo(10+step*0.3, 0); ctx.stroke();
+  ctx.fillStyle='#5c3a1a'; ctx.beginPath(); ctx.ellipse(-11,2,7,4,0,0,7); ctx.fill();
+  ctx.beginPath(); ctx.ellipse(11,2,7,4,0,0,7); ctx.fill();
+
+  const bh=96, bw=40;
+  const bg=ctx.createLinearGradient(-bw/2,0,bw/2,0);
+  bg.addColorStop(0,'#a86a2e'); bg.addColorStop(.4,f.color); bg.addColorStop(.6,'#f2c987'); bg.addColorStop(1,'#a86a2e');
+  ctx.fillStyle=hurt?'#fff':bg;
+  roundedLoaf(0,-bh/2-8,bw,bh); ctx.fill();
+  ctx.strokeStyle='rgba(90,50,20,.55)'; ctx.lineWidth=2.5;
+  for(let i=0;i<4;i++){ const yy=-bh-2+ i*22+16;
+    ctx.beginPath(); ctx.moveTo(-6,yy); ctx.lineTo(8,yy-10); ctx.stroke(); }
+  ctx.fillStyle='rgba(255,250,235,.30)';
+  for(let i=0;i<7;i++){ const fx=((i*97)%bw)-bw/2+4, fy=-bh-4+((i*53)%bh); ctx.fillRect(fx,fy,2,2); }
+
+  // beret
+  ctx.save(); ctx.translate(0,-bh-6);
+  ctx.fillStyle=f.beret;
+  ctx.beginPath(); ctx.ellipse(2,-4,24,13,-.15,0,7); ctx.fill();
+  ctx.beginPath(); ctx.ellipse(0,2,20,7,0,0,7); ctx.fill();
+  ctx.fillStyle='#3a2c22'; ctx.beginPath(); ctx.arc(14,-11,2.5,0,7); ctx.fill();
+  ctx.restore();
+
+  // eyes
+  ctx.fillStyle='#3a2c22'; const eyY=-bh+8;
+  if(kod){ ctx.strokeStyle='#3a2c22'; ctx.lineWidth=2.4; xeye(6,eyY); xeye(20,eyY); }
+  else {
+    ctx.beginPath(); ctx.arc(8,eyY,3.4,0,7); ctx.fill();
+    ctx.beginPath(); ctx.arc(20,eyY,3.4,0,7); ctx.fill();
+    ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(9,eyY-1,1.1,0,7); ctx.fill();
+    ctx.beginPath(); ctx.arc(21,eyY-1,1.1,0,7); ctx.fill();
+  }
+  ctx.strokeStyle='#5c3a1a'; ctx.lineWidth=2; ctx.beginPath();
+  if(f.state==='attack'){ ctx.arc(14,eyY+13,4,0,Math.PI); }
+  else { ctx.moveTo(9,eyY+12); ctx.quadraticCurveTo(14,eyY+15,20,eyY+12); }
+  ctx.stroke();
+
+  // arm + weapon baguette
+  ctx.save();
+  ctx.translate(16,-bh/2-4);
+  const isSuper = f.state==='attack' && f.atk==='super';
+  const base=-0.5, ang=base + f.swing*1.5;
+  ctx.rotate(ang);
+  ctx.strokeStyle='#8a5a2a'; ctx.lineWidth=6; ctx.lineCap='round';
+  ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(22,0); ctx.stroke();
+  ctx.translate(22,0);
+  const wlen = isSuper?74:58;
+  const wg=ctx.createLinearGradient(0,-6,0,6);
+  if(isSuper){ wg.addColorStop(0,'#ffe07a'); wg.addColorStop(.5,'#fff1b0'); wg.addColorStop(1,'#e0a12a'); }
+  else { wg.addColorStop(0,'#e8b96e'); wg.addColorStop(.5,'#f2c987'); wg.addColorStop(1,'#b07a34'); }
+  ctx.fillStyle=wg; roundedRect(0,-7,wlen,14,7); ctx.fill();
+  ctx.strokeStyle='rgba(90,50,20,.5)'; ctx.lineWidth=1.8;
+  for(let i=1;i<5;i++){ ctx.beginPath(); ctx.moveTo(6+i*10,-3); ctx.lineTo(10+i*10,3); ctx.stroke(); }
+  if(f.state==='attack' && f.swing>.3){
+    ctx.globalAlpha=isSuper?.4:.25; ctx.strokeStyle=isSuper?'#fff6cf':'#fff3dd';
+    ctx.lineWidth=isSuper?16:10; ctx.lineCap='round';
+    ctx.beginPath(); ctx.arc(-22,0,isSuper?76:64,-0.6,0.5); ctx.stroke(); ctx.globalAlpha=1;
+  }
+  ctx.restore();
+
+  if(f.blocking && f.blockT>0){
+    ctx.globalAlpha=Math.min(.5,f.blockT*2.5);
+    ctx.strokeStyle='#ffe7b0'; ctx.lineWidth=3;
+    ctx.beginPath(); ctx.ellipse(22,-bh/2-6,26,54,0,-1.2,1.2); ctx.stroke();
+    ctx.globalAlpha=1;
+  }
+  ctx.restore();
+}
+function xeye(x,y){ ctx.beginPath(); ctx.moveTo(x-3,y-3); ctx.lineTo(x+3,y+3); ctx.moveTo(x+3,y-3); ctx.lineTo(x-3,y+3); ctx.stroke(); }
+function roundedLoaf(cx,topY,w,h){
+  const r=w/2; ctx.beginPath();
+  ctx.moveTo(cx-r,topY+r); ctx.arc(cx,topY+r,r,Math.PI,0);
+  ctx.lineTo(cx+r,topY+h-r); ctx.arc(cx,topY+h-r,r,0,Math.PI); ctx.closePath();
+}
+function roundedRect(x,y,w,h,r){
+  ctx.beginPath(); ctx.moveTo(x+r,y);
+  ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
+  ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
+}
+
+// ---------------------------------------------------------------------------
+// HUD
+// ---------------------------------------------------------------------------
+function drawHUD(){
+  hpBar(30,26,360,f1,false,'Pierre',f1.wins);
+  hpBar(W-30-360,26,360,f2,true,'Gaston',f2.wins);
+  meterBar(30,52,360,f1,false);
+  meterBar(W-30-360,52,360,f2,true);
+  ctx.save();
+  ctx.fillStyle='rgba(18,15,22,.7)'; roundedRect(W/2-40,20,80,54,10); ctx.fill();
+  ctx.strokeStyle='rgba(224,164,88,.5)'; ctx.lineWidth=1.5; ctx.stroke();
+  ctx.fillStyle='#f5f2ea'; ctx.font='800 30px Georgia,serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
+  ctx.fillText(Math.ceil(Math.max(0,timeLeft)), W/2, 48);
+  ctx.restore();
+  if(muted){ ctx.save(); ctx.fillStyle='rgba(245,242,234,.6)'; ctx.font='600 12px -apple-system,Arial';
+    ctx.textAlign='center'; ctx.fillText('🔇 muet (M)', W/2, 92); ctx.restore(); }
+}
+function hpBar(x,y,w,f,right,name,wins){
+  const h=20; ctx.save();
+  ctx.fillStyle='#f5f2ea'; ctx.font='700 15px -apple-system,Arial'; ctx.textBaseline='alphabetic';
+  ctx.textAlign=right?'right':'left'; ctx.fillText(name, right? x+w : x, y-6);
+  for(let i=0;i<2;i++){ const px=right? x+w-8-i*16 : x+8+i*16;
+    ctx.beginPath(); ctx.arc(px, y-11, 5, 0,7);
+    ctx.fillStyle = i<wins? '#e0a458':'rgba(255,255,255,.22)'; ctx.fill(); }
+  ctx.fillStyle='rgba(18,15,22,.6)'; roundedRect(x-3,y-3,w+6,h+6,7); ctx.fill();
+  ctx.fillStyle='rgba(0,0,0,.4)'; roundedRect(x,y,w,h,5); ctx.fill();
+  const pct=f.hp/f.maxhp, fw=w*pct, fx = right? x+w-fw : x;
+  const grad=ctx.createLinearGradient(0,y,0,y+h);
+  const col = pct>.4? ['#e0a458','#c65f38'] : ['#e07a45','#a8321f'];
+  grad.addColorStop(0,col[0]); grad.addColorStop(1,col[1]);
+  ctx.fillStyle=grad; roundedRect(fx,y,fw,h,5); ctx.fill();
+  ctx.strokeStyle='rgba(224,164,88,.4)'; ctx.lineWidth=1.5; roundedRect(x-3,y-3,w+6,h+6,7); ctx.stroke();
+  ctx.restore();
+}
+function meterBar(x,y,w,f,right){
+  const h=7; ctx.save();
+  ctx.fillStyle='rgba(0,0,0,.4)'; roundedRect(x,y,w,h,3); ctx.fill();
+  const pct=f.meter/f.maxMeter, fw=w*pct, fx=right? x+w-fw : x;
+  const full=f.meter>=f.maxMeter;
+  const grad=ctx.createLinearGradient(0,y,0,y+h);
+  if(full){ const t=.5+.5*Math.sin(performance.now()/120);
+    grad.addColorStop(0,`rgba(255,${200+40*t|0},120,1)`); grad.addColorStop(1,'#e0a12a'); }
+  else { grad.addColorStop(0,'#7fd3e0'); grad.addColorStop(1,'#3a8fb0'); }
+  ctx.fillStyle=grad; roundedRect(fx,y,fw,h,3); ctx.fill();
+  if(full){ ctx.fillStyle='#ffe7b0'; ctx.font='700 9px -apple-system,Arial';
+    ctx.textAlign=right?'right':'left'; ctx.textBaseline='top';
+    ctx.fillText('CRUST CRUSHER PRÊT', right? x+w : x, y+9); }
+  ctx.restore();
+}
+function drawAnnounce(){
+  ctx.save();
+  const a=Math.min(1,announceT*1.4)*Math.min(1,(1.6-announceT)*4+.2);
+  ctx.globalAlpha=Math.max(0,Math.min(1,a));
+  ctx.textAlign='center'; ctx.textBaseline='middle';
+  ctx.fillStyle=showAnnounce==='K.O.'?'#e0603a':'#f5f2ea';
+  ctx.font="italic 800 60px Georgia,'Playfair Display',serif";
+  ctx.shadowColor='rgba(198,95,56,.6)'; ctx.shadowBlur=24;
+  ctx.fillText(showAnnounce, W/2, 200);
+  ctx.restore();
+}
+function drawCountdown(){
+  const n=Math.ceil(countdownT-0.2);
+  ctx.save(); ctx.textAlign='center'; ctx.textBaseline='middle';
+  const frac=(countdownT-0.2)%1;
+  ctx.globalAlpha=Math.min(1,frac*2);
+  const sc=1+ (1-frac)*0.4;
+  ctx.translate(W/2,270); ctx.scale(sc,sc);
+  if(n>0){ ctx.fillStyle='#e0a458'; ctx.font="800 90px Georgia,serif";
+    ctx.shadowColor='rgba(0,0,0,.5)'; ctx.shadowBlur=18; ctx.fillText(String(n),0,0); }
+  ctx.restore();
+}
+
+// ---------------------------------------------------------------------------
+// Main loop
+// ---------------------------------------------------------------------------
+let last=performance.now();
+function loop(now){
+  let dt=(now-last)/1000; last=now;
+  if(dt>0.05) dt=0.05;
+
+  if(state==='fight'){
+    if(freeze>0){ freeze-=dt; }
+    else {
+      if(announceT>0) announceT-=dt;
+
+      if(roundPhase==='intro'){
+        countdownT-=dt;
+        const n=Math.ceil(countdownT-0.2);
+        if(n<lastCount && n>0){ lastCount=n; sfx.count(); }
+        if(countdownT<=0.2){ roundPhase='fight'; showAnnounce='COMBATTEZ !'; announceT=1.0; sfx.fight(); }
+        // idle facing during intro (light physics only)
+        if(f1&&f2){ f1.vx*=.8; f2.vx*=.8; f1.facing=f2.x<f1.x?-1:1; f2.facing=f1.x<f2.x?-1:1; }
+      } else if(roundPhase==='fight'){
+        if(timeLeft>0) timeLeft-=dt;
+        updateFighter(f1,f2,dt);
+        updateFighter(f2,f1,dt);
+        checkRoundEnd();
+      } else if(roundPhase==='ko'){
+        // let bodies settle in slow-mo
+        updateFighter(f1,f2,dt*0.4);
+        updateFighter(f2,f1,dt*0.4);
+      }
+      updateParticles(dt);
+      if(shake>0) shake=Math.max(0,shake-dt*40);
+    }
+  }
+
+  if(f1&&f2) draw(); else drawScene();
+  clearEdges();
+  requestAnimationFrame(loop);
+}
+requestAnimationFrame(loop);
+
+// ---------------------------------------------------------------------------
+// Menu wiring
+// ---------------------------------------------------------------------------
+document.querySelectorAll('.play[data-mode]').forEach(b=>{
+  b.addEventListener('click',()=>{
+    const diffSel=document.getElementById('difficulty');
+    if(diffSel) difficulty=diffSel.value;
+    startGame(parseInt(b.dataset.mode));
+  });
+});
+document.querySelectorAll('#pause [data-pause]').forEach(b=>{
+  b.addEventListener('click',()=>{
+    const act=b.getAttribute('data-pause');
+    if(act==='resume') togglePause();
+    else if(act==='menu'){ togglePause(); toMenu(); }
+  });
+});
+bindTouch();
+
+// expose a tiny hook for automated smoke tests
+window.__BB__ = { get state(){return state;}, get f1(){return f1;}, get f2(){return f2;}, startGame };
+})();
+
+</script>
+</body>
+</html>
diff --git a/games/baguette-brawl/index.html b/games/baguette-brawl/index.html
new file mode 100644
index 0000000..e8ac114
--- /dev/null
+++ b/games/baguette-brawl/index.html
@@ -0,0 +1,915 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<title>Baguette Brawl — Duel de Paris</title>
+<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🥖</text></svg>">
+<style>
+:root{
+  --bg:#f7efe6; --surface:#fff9f1; --text:#3a2c22; --muted:#a08c7a;
+  --accent:#c65f38; --accent2:#e0a458;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{height:100%}
+body{
+  background:radial-gradient(120% 120% at 50% 0%,#20222e 0%,#12131a 55%,#0a0a0f 100%);
+  color:var(--text);
+  font-family:-apple-system,'Segoe UI','Helvetica Neue',Arial,sans-serif;
+  display:flex;align-items:center;justify-content:center;
+  min-height:100%;padding:20px;overflow:hidden;
+}
+#wrap{
+  position:relative;width:960px;max-width:100%;
+  aspect-ratio:960/540;border-radius:14px;overflow:hidden;
+  box-shadow:0 16px 48px rgba(0,0,0,.55), 0 0 0 1px rgba(224,164,88,.25);
+  background:#1a1720;
+}
+canvas{display:block;width:100%;height:100%;image-rendering:auto}
+
+/* overlays */
+.overlay{
+  position:absolute;inset:0;display:flex;flex-direction:column;
+  align-items:center;justify-content:center;text-align:center;
+  background:linear-gradient(160deg,rgba(18,15,22,.86),rgba(30,20,14,.92));
+  backdrop-filter:blur(2px);gap:16px;padding:32px;
+  color:var(--bg);z-index:10;
+}
+.overlay.hide{display:none}
+.kicker{
+  font-size:12px;letter-spacing:.28em;text-transform:uppercase;
+  color:var(--accent2);font-weight:600;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;
+}
+h1{
+  font-family:'Playfair Display',Georgia,'Bodoni MT',serif;
+  font-weight:800;font-size:clamp(38px,7vw,67px);line-height:.98;
+  color:var(--surface);letter-spacing:-.01em;
+  text-shadow:0 2px 24px rgba(224,164,88,.25);
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.09s;
+}
+h1 .em{color:var(--accent2);font-style:italic}
+.sub{
+  max-width:520px;color:#e8dfd2;font-size:16px;line-height:1.5;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.18s;
+}
+.btns{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.27s;}
+button.play{
+  font-family:inherit;cursor:pointer;border:none;border-radius:12px;
+  padding:15px 26px;font-size:15px;font-weight:700;letter-spacing:.02em;
+  color:#fff;background:linear-gradient(135deg,var(--accent),#a8471f);
+  box-shadow:0 8px 24px rgba(198,95,56,.4);transition:transform .16s ease,box-shadow .16s ease;
+}
+button.play.alt{background:linear-gradient(135deg,#5c7d8f,#38505d);box-shadow:0 8px 24px rgba(75,107,122,.4)}
+button.play:hover{transform:translateY(-2px);box-shadow:0 16px 40px rgba(198,95,56,.5)}
+button.play.alt:hover{box-shadow:0 16px 40px rgba(75,107,122,.5)}
+
+.diffrow{display:flex;align-items:center;gap:10px;font-size:13px;color:#e0d7c9;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.22s;}
+.diffrow label{letter-spacing:.12em;text-transform:uppercase;font-size:11px;color:var(--accent2);font-weight:700}
+select#difficulty{
+  font-family:inherit;font-size:13px;color:#f2e9dd;background:#2a2530;
+  border:1px solid #4a4150;border-radius:8px;padding:7px 12px;cursor:pointer;
+}
+
+.controls{
+  display:flex;gap:32px;flex-wrap:wrap;justify-content:center;margin-top:6px;
+  animation:fadeUp .7s cubic-bezier(.22,1,.36,1) both;animation-delay:.36s;
+}
+.ctrlcard{
+  background:rgba(255,249,241,.06);border:1px solid rgba(224,164,88,.22);
+  border-radius:12px;padding:14px 18px;min-width:200px;text-align:left;
+}
+.ctrlcard h4{font-size:12px;letter-spacing:.15em;text-transform:uppercase;color:var(--accent2);margin-bottom:8px}
+.ctrlcard .row{display:flex;justify-content:space-between;font-size:13px;color:#e0d7c9;margin:3px 0;gap:12px}
+kbd{
+  background:#2a2530;border:1px solid #4a4150;border-bottom-width:2px;border-radius:5px;
+  padding:1px 7px;font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#f2e9dd;
+}
+.result-name{font-family:'Playfair Display',Georgia,serif;font-style:italic}
+.foot{position:absolute;bottom:12px;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:var(--muted)}
+@keyframes fadeUp{from{opacity:0;transform:translateY(28px)}to{opacity:1;transform:none}}
+
+/* pause overlay is lighter */
+#pause{background:linear-gradient(160deg,rgba(18,15,22,.72),rgba(30,20,14,.8))}
+
+/* ---- touch controls (mobile only) ---- */
+#touch{
+  position:absolute;inset:auto 0 0 0;z-index:8;display:none;
+  justify-content:space-between;padding:0 14px 14px;pointer-events:none;
+  user-select:none;-webkit-user-select:none;touch-action:none;
+}
+#touch .pad,#touch .acts{display:flex;gap:12px;pointer-events:auto}
+#touch button{
+  width:58px;height:58px;border-radius:50%;border:1px solid rgba(224,164,88,.4);
+  background:rgba(30,25,32,.55);color:#f2e9dd;font-size:20px;font-weight:800;
+  backdrop-filter:blur(3px);cursor:pointer;
+}
+#touch button.down{background:rgba(198,95,56,.75);transform:scale(.94)}
+#touch button.sup{background:rgba(224,161,42,.35);font-size:15px}
+@media (hover:none) and (pointer:coarse){ #touch{display:flex} .controls{display:none} }
+
+</style>
+</head>
+<body>
+<div id="wrap">
+  <canvas id="c" width="960" height="540"></canvas>
+
+  <!-- MENU -->
+  <div id="menu" class="overlay">
+    <div class="kicker">Sous la lune de Montmartre</div>
+    <h1>Baguette <span class="em">Brawl</span></h1>
+    <p class="sub">Two crusty duelists face off beneath the Eiffel Tower. Swing your loaf, guard your crust, charge the meter and unleash the <b>Crust Crusher</b>. First to <b>2 rounds</b> wins the boulangerie.</p>
+    <div class="diffrow">
+      <label for="difficulty">Chef IA</label>
+      <select id="difficulty">
+        <option value="easy">Apprenti (facile)</option>
+        <option value="normal" selected>Boulanger (normal)</option>
+        <option value="hard">Maître (difficile)</option>
+      </select>
+    </div>
+    <div class="btns">
+      <button class="play" data-mode="1">Solo vs. Chef IA</button>
+      <button class="play alt" data-mode="2">Deux Joueurs</button>
+    </div>
+    <div class="controls">
+      <div class="ctrlcard">
+        <h4>Pierre · Joueur 1</h4>
+        <div class="row"><span>Move / Jump</span><span><kbd>A</kbd><kbd>D</kbd><kbd>W</kbd></span></div>
+        <div class="row"><span>Jab · Heavy</span><span><kbd>J</kbd><kbd>K</kbd></span></div>
+        <div class="row"><span>Guard · Super</span><span><kbd>L</kbd><kbd>U</kbd></span></div>
+      </div>
+      <div class="ctrlcard">
+        <h4>Gaston · Joueur 2</h4>
+        <div class="row"><span>Move / Jump</span><span><kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd></span></div>
+        <div class="row"><span>Jab · Heavy</span><span><kbd>,</kbd><kbd>.</kbd></span></div>
+        <div class="row"><span>Guard · Super</span><span><kbd>/</kbd><kbd>;</kbd></span></div>
+      </div>
+    </div>
+    <div class="foot">Pause <kbd>P</kbd> · Mute <kbd>M</kbd> · Un jeu de pain · Paris</div>
+  </div>
+
+  <!-- PAUSE -->
+  <div id="pause" class="overlay hide">
+    <div class="kicker">Pause</div>
+    <h1>En pause</h1>
+    <div class="btns">
+      <button class="play" data-pause="resume">Reprendre</button>
+      <button class="play alt" data-pause="menu">Menu</button>
+    </div>
+    <div class="foot"><kbd>P</kbd> pour reprendre</div>
+  </div>
+
+  <!-- ROUND / RESULT -->
+  <div id="result" class="overlay hide">
+    <div class="kicker" id="resKicker">Manche terminée</div>
+    <h1 id="resTitle">—</h1>
+    <p class="sub" id="resSub"></p>
+    <div class="btns" id="resBtns"></div>
+  </div>
+
+  <!-- TOUCH CONTROLS (mobile) -->
+  <div id="touch">
+    <div class="pad">
+      <button data-key="a">◀</button>
+      <button data-key="w">▲</button>
+      <button data-key="d">▶</button>
+    </div>
+    <div class="acts">
+      <button data-key="l">🛡</button>
+      <button data-key="j">J</button>
+      <button data-key="k">K</button>
+      <button data-key="u" class="sup">SUP</button>
+    </div>
+  </div>
+</div>
+
+<script>
+/* ============================================================================
+   Baguette Brawl — Duel de Paris
+   A 2D fighting game where two crusty baguette duelists brawl beneath the
+   Eiffel Tower. Vanilla JS + Canvas, no dependencies, file:// friendly.
+
+   Rebuilt & extended from the Model Arena starting artifact:
+     - press-edge input (attacks no longer auto-repeat while held)
+     - clean round state machine (intro → fight → KO → result), no dt*0 hack
+     - super meter + unblockable "Crust Crusher" special
+     - selectable AI difficulty with real spacing / anti-air / whiff punish
+     - on-screen touch controls for mobile
+     - pause (P / Esc), rematch (R), slow-mo KO, "FIGHT!" / "K.O." banners
+   ========================================================================== */
+(() => {
+'use strict';
+
+const cv  = document.getElementById('c');
+const ctx = cv.getContext('2d');
+const W = 960, H = 540;
+const GROUND = 470;
+
+const menuEl   = document.getElementById('menu');
+const resEl    = document.getElementById('result');
+const pauseEl  = document.getElementById('pause');
+const resTitle = document.getElementById('resTitle');
+const resSub   = document.getElementById('resSub');
+const resKicker= document.getElementById('resKicker');
+const resBtns  = document.getElementById('resBtns');
+const touchEl  = document.getElementById('touch');
+
+// ---------------------------------------------------------------------------
+// Audio (tiny synth, no external files)
+// ---------------------------------------------------------------------------
+let AC = null, muted = false;
+function actx(){ if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } return AC; }
+function tone(freq, dur, type='sine', vol=.15, slideTo=null){
+  if(muted) return; const a=actx(); if(!a) return;
+  const o=a.createOscillator(), g=a.createGain();
+  o.type=type; o.frequency.setValueAtTime(freq,a.currentTime);
+  if(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo,a.currentTime+dur);
+  g.gain.setValueAtTime(vol,a.currentTime);
+  g.gain.exponentialRampToValueAtTime(.0001,a.currentTime+dur);
+  o.connect(g); g.connect(a.destination); o.start(); o.stop(a.currentTime+dur+.02);
+}
+function noise(dur, vol=.2, hp=800){
+  if(muted) return; const a=actx(); if(!a) return;
+  const n=Math.floor(a.sampleRate*dur), buf=a.createBuffer(1,n,a.sampleRate), d=buf.getChannelData(0);
+  for(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*(1-i/n);
+  const s=a.createBufferSource(); s.buffer=buf;
+  const f=a.createBiquadFilter(); f.type='highpass'; f.frequency.value=hp;
+  const g=a.createGain(); g.gain.value=vol;
+  s.connect(f); f.connect(g); g.connect(a.destination); s.start();
+}
+const sfx = {
+  swing(){ noise(.12,.12,1200); },
+  hit(){ noise(.16,.32,500); tone(120,.1,'square',.14,70); },
+  crush(){ noise(.26,.4,300); tone(90,.28,'sawtooth',.22,50); },
+  block(){ tone(880,.09,'triangle',.16,660); noise(.05,.08,2500); },
+  jump(){ tone(300,.12,'sine',.1,520); },
+  super(){ tone(440,.18,'square',.16,880); setTimeout(()=>tone(660,.2,'square',.16,1200),90); },
+  ko(){ tone(400,.5,'sawtooth',.2,80); },
+  count(){ tone(660,.08,'triangle',.12); },
+  fight(){ tone(523,.1,'triangle',.16); setTimeout(()=>tone(784,.18,'triangle',.18),100); },
+  win(){ tone(523,.12,'triangle',.16); setTimeout(()=>tone(659,.12,'triangle',.16),120); setTimeout(()=>tone(784,.2,'triangle',.18),240); }
+};
+
+// ---------------------------------------------------------------------------
+// Input — held keys + press-edge (justPressed) so attacks fire once per tap
+// ---------------------------------------------------------------------------
+const keys = new Set();
+const justPressed = new Set();
+function normKey(e){ return e.key.length===1 ? e.key.toLowerCase() : e.key; }
+addEventListener('keydown', e=>{
+  const k=normKey(e);
+  if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' ','/'].includes(e.key)) e.preventDefault();
+  if(!keys.has(k)) justPressed.add(k);   // edge only on the first frame held
+  keys.add(k);
+  handleGlobalKeys(k);
+});
+addEventListener('keyup', e=>{ keys.delete(normKey(e)); });
+function clearEdges(){ justPressed.clear(); }
+
+function handleGlobalKeys(k){
+  if(k==='m'){ muted=!muted; }
+  if(state==='fight'){
+    if(k==='p'||k==='Escape'){ togglePause(); }
+  } else if(state==='paused'){
+    if(k==='p'||k==='Escape'){ togglePause(); }
+  }
+  if(state==='result' && (k==='r'||k==='Enter')){ const b=resBtns.querySelector('button'); if(b) b.click(); }
+}
+
+// Touch controls write into the same `keys`/`justPressed` sets as P1.
+function bindTouch(){
+  if(!touchEl) return;
+  touchEl.querySelectorAll('[data-key]').forEach(btn=>{
+    const key = btn.getAttribute('data-key');
+    const press = (e)=>{ e.preventDefault(); if(!keys.has(key)) justPressed.add(key); keys.add(key); btn.classList.add('down'); };
+    const release= (e)=>{ e.preventDefault(); keys.delete(key); btn.classList.remove('down'); };
+    btn.addEventListener('touchstart',press,{passive:false});
+    btn.addEventListener('touchend',release,{passive:false});
+    btn.addEventListener('touchcancel',release,{passive:false});
+    btn.addEventListener('mousedown',press);
+    btn.addEventListener('mouseup',release);
+    btn.addEventListener('mouseleave',release);
+  });
+}
+
+// ---------------------------------------------------------------------------
+// Fighter
+// ---------------------------------------------------------------------------
+const P1CTRL = {left:'a',right:'d',jump:'w',light:'j',heavy:'k',block:'l',super:'u'};
+const P2CTRL = {left:'ArrowLeft',right:'ArrowRight',jump:'ArrowUp',light:',',heavy:'.',block:'/',super:';'};
+
+function makeFighter(x, facing, cfg){
+  return {
+    x, y:GROUND, vx:0, vy:0, facing, onGround:true,
+    hp:100, maxhp:100, meter:0, maxMeter:100,
+    state:'idle',        // idle | attack | hurt | ko
+    atk:null, atkT:0, atkDur:0, atkHit:false,
+    hurtT:0, blockT:0, blocking:false,
+    swing:0, walkPhase:0,
+    color:cfg.color, beret:cfg.beret, name:cfg.name,
+    isAI:cfg.isAI||false, ctrl:cfg.ctrl,
+    hitFlash:0, wins:0, _t:0, _think:0
+  };
+}
+
+let f1, f2, mode=1, difficulty='normal', state='menu';
+let roundNum=1, roundTime=60, timeLeft=60, roundPhase='intro';
+let freeze=0, shake=0, particles=[], clouds=[], stars=[], koTimer=null;
+let showAnnounce='', announceT=0, countdownT=0, lastCount=99;
+
+const AI_TUNE = {
+  easy:   {react:.55, aggr:.35, blockCh:.30, superCh:.15, whiff:.25, jumpCh:.006, speed:2.6},
+  normal: {react:.30, aggr:.55, blockCh:.55, superCh:.40, whiff:.55, jumpCh:.010, speed:3.0},
+  hard:   {react:.14, aggr:.75, blockCh:.80, superCh:.70, whiff:.85, jumpCh:.016, speed:3.3}
+};
+
+function initClouds(){
+  clouds=[]; for(let i=0;i<5;i++) clouds.push({x:Math.random()*W,y:40+Math.random()*120,s:.15+Math.random()*.25,w:80+Math.random()*90});
+  stars=[]; for(let i=0;i<70;i++) stars.push({x:Math.random()*W,y:Math.random()*300,r:Math.random()*1.4+.3,t:Math.random()*6});
+}
+initClouds();
+
+function startGame(m){
+  mode=m; roundNum=1;
+  f1=makeFighter(300, 1,{color:'#e0a458',beret:'#c62f2f',name:'Pierre',ctrl:P1CTRL});
+  f2=makeFighter(660,-1,{color:'#d9974a',beret:'#2f4b6b',name:'Gaston',ctrl:P2CTRL,isAI:(m===1)});
+  f1.wins=0; f2.wins=0;
+  actx();
+  startRound();
+}
+function resetFighter(f,x,facing){
+  Object.assign(f,{x,y:GROUND,vx:0,vy:0,hp:100,meter:f.meter*0.5|0,
+    state:'idle',atk:null,atkT:0,atkDur:0,atkHit:false,hurtT:0,blockT:0,
+    blocking:false,swing:0,facing,hitFlash:0,onGround:true});
+}
+function startRound(){
+  clearTimeout(koTimer); koTimer=null;
+  resEl.classList.add('hide'); menuEl.classList.add('hide'); pauseEl.classList.add('hide');
+  resetFighter(f1,300,1);
+  resetFighter(f2,660,-1);
+  timeLeft=roundTime; particles=[]; freeze=0; shake=0;
+  showAnnounce=`MANCHE ${roundNum}`; announceT=1.6; countdownT=3.2; lastCount=99;
+  roundPhase='intro';
+  state='fight';
+}
+
+// ---------------------------------------------------------------------------
+// Combat tuning
+// ---------------------------------------------------------------------------
+const ATK = {
+  light:{dur:.34, active:[.08,.18], reach:78, dmg:7,  kb:3.2, stun:.28, meter:6,  cost:0},
+  heavy:{dur:.62, active:[.22,.36], reach:96, dmg:16, kb:8.5, stun:.5,  meter:10, cost:0},
+  // Crust Crusher — unblockable overhead super, costs a full meter
+  super:{dur:.72, active:[.30,.46], reach:112,dmg:30, kb:12,  stun:.7,  meter:0,  cost:100, unblock:true}
+};
+
+function tryAttack(f, type){
+  if(f.state==='attack'||f.state==='hurt'||f.state==='ko') return;
+  const a = ATK[type];
+  if(type==='super' && f.meter < a.cost) return;
+  if(!f.onGround && type!=='light') return;    // only jabs in the air
+  if(type==='super'){ f.meter=0; sfx.super(); spawnParticles(f.x,f.y-70,20,'#ffe7b0',7); }
+  else sfx.swing();
+  f.state='attack'; f.atk=type; f.atkT=0; f.atkDur=a.dur; f.atkHit=false;
+}
+
+function addMeter(f, amt){ f.meter=Math.min(f.maxMeter, f.meter+amt); }
+
+function updateFighter(f, other, dt){
+  const c=f.ctrl;
+  const canAct = f.state!=='hurt' && f.state!=='ko';
+  f.blocking=false;
+
+  if(f.onGround && f.state!=='attack'){ f.facing = other.x < f.x ? -1 : 1; }
+
+  if(!f.isAI){
+    let mv=0;
+    if(canAct && f.state!=='attack'){
+      if(keys.has(c.left))  mv-=1;
+      if(keys.has(c.right)) mv+=1;
+      if(keys.has(c.block)){ f.blocking=true; mv=0; }
+    }
+    f.vx = mv*3.4;
+    if(canAct && justPressed.has(c.jump) && f.onGround){ f.vy=-13.4; f.onGround=false; sfx.jump(); }
+    if(canAct){
+      if(justPressed.has(c.super))      tryAttack(f,'super');
+      else if(justPressed.has(c.light)) tryAttack(f,'light');
+      else if(justPressed.has(c.heavy)) tryAttack(f,'heavy');
+    }
+  } else {
+    aiControl(f, other, dt);
+  }
+
+  if(f.blocking) f.blockT=Math.min(f.blockT+dt,.2);
+
+  // physics
+  f.vy += 42*dt;
+  f.y  += f.vy;
+  f.x  += f.vx;
+  if(f.y>=GROUND){ f.y=GROUND; f.vy=0; f.onGround=true; } else f.onGround=false;
+  f.x = Math.max(60, Math.min(W-60, f.x));
+
+  // simple body separation so they don't overlap
+  const gap=Math.abs(f.x-other.x);
+  if(gap<64 && Math.abs(f.y-other.y)<90){
+    const push=(64-gap)/2, dir=f.x<other.x?-1:1;
+    f.x+=dir*push*0.5; other.x-=dir*push*0.5;
+    f.x=Math.max(60,Math.min(W-60,f.x)); other.x=Math.max(60,Math.min(W-60,other.x));
+  }
+
+  if(Math.abs(f.vx)>.2 && f.onGround) f.walkPhase+=dt*12; else f.walkPhase*=.8;
+
+  if(f.state==='attack'){
+    f.atkT+=dt;
+    const a=ATK[f.atk];
+    const p=f.atkT/f.atkDur;
+    f.swing = swingCurve(p);
+    if(!f.atkHit && p>=a.active[0] && p<=a.active[1]){
+      const dx=other.x-f.x;
+      const inFront = (f.facing>0 && dx>0)||(f.facing<0 && dx<0);
+      const dist=Math.abs(dx);
+      const vgap=Math.abs(other.y-f.y);
+      if(inFront && dist < a.reach+40 && dist>10 && vgap<120){
+        f.atkHit=true; addMeter(f,a.meter); landHit(f, other, a);
+      }
+    }
+    if(f.atkT>=f.atkDur){ f.state='idle'; f.atk=null; f.swing=0; }
+  } else {
+    f.swing*=.7;
+  }
+
+  if(f.state==='hurt'){ f.hurtT-=dt; if(f.hurtT<=0) f.state='idle'; }
+  if(f.hitFlash>0) f.hitFlash-=dt;
+  if(f.blockT>0 && !f.blocking) f.blockT-=dt*.5;
+}
+
+function swingCurve(p){
+  if(p<.35) return -(p/.35)*.5;
+  const q=(p-.35)/.65;
+  return -0.5 + q*q*(3-2*q)*1.9;
+}
+
+function landHit(attacker, target, a){
+  if(target.state==='ko') return;
+  let dmg=a.dmg, kb=a.kb;
+  const facingRight = target.facing>0;
+  const attackerOnGuardSide = (facingRight && attacker.x>target.x)||(!facingRight && attacker.x<target.x);
+  const blocked = !a.unblock && target.blocking && target.onGround && attackerOnGuardSide;
+
+  if(blocked){
+    dmg*=0.18; kb*=.5;
+    addMeter(target,4);                    // reward defense a little
+    sfx.block();
+    spawnParticles(target.x+target.facing*-20, target.y-70, 6, '#ffe7b0', 3);
+    shake=Math.max(shake,4); target.blockT=.2;
+  } else {
+    a.unblock ? sfx.crush() : sfx.hit();
+    addMeter(target, a.unblock?0:6);
+    target.state='hurt'; target.hurtT=a.stun;
+    spawnParticles(target.x, target.y-70, a.unblock?26:16, attacker.color, a.unblock?9:6);
+    spawnParticles(target.x, target.y-80, 8, '#fff3dd', 5);
+    target.hitFlash=.18;
+    shake=Math.max(shake, a.dmg>10?11:6);
+    freeze=a.unblock?.11:(a.dmg>10?.07:.035);
+  }
+  target.hp=Math.max(0,target.hp-dmg);
+  target.vx = attacker.facing*kb;
+  if(!blocked && a.dmg>10){ target.vy=-4-(a.unblock?3:0); target.onGround=false; }
+  if(target.hp<=0){ target.state='ko'; target.vx=attacker.facing*5; target.vy=-8; target.onGround=false; sfx.ko(); }
+}
+
+// ---------------------------------------------------------------------------
+// AI
+// ---------------------------------------------------------------------------
+function aiControl(f, o, dt){
+  if(roundPhase!=='fight'){ f.vx*=.85; return; }   // no thinking during intro / ko slow-mo
+  const T=AI_TUNE[difficulty]||AI_TUNE.normal;
+  f._t=(f._t||0)+dt; f._think=(f._think||0)-dt;
+  const dist=Math.abs(o.x-f.x);
+  const dir=o.x<f.x?-1:1;
+  let mv=0;
+
+  if(f.state==='attack'||f.state==='hurt'){ f.vx*=.85; return; }
+
+  // anti-air: opponent airborne & close → block or heavy up-swing
+  if(!o.onGround && dist<120 && Math.random()<T.blockCh*0.5){ f.blocking=true; f.vx=0; return; }
+
+  // block when opponent commits an attack in range
+  if(o.state==='attack' && dist<130 && Math.random()<T.blockCh*0.18){ f.blocking=true; f.vx=0; return; }
+
+  // fire super when charged & in range
+  if(f.meter>=100 && dist<110 && Math.random()<T.superCh*0.1){ tryAttack(f,'super'); return; }
+
+  const ideal=90;
+  if(dist>ideal+30) mv=dir;
+  else if(dist<ideal-30) mv=-dir;
+  else {
+    if(f._think<=0){
+      f._think = T.react + Math.random()*.4;
+      if(Math.random()<T.aggr){
+        if(dist<100 && Math.random()<.5) tryAttack(f,'heavy');
+        else tryAttack(f,'light');
+      }
+    }
+    mv = (Math.random()<.3 ? -dir : 0);
+  }
+
+  // punish a whiffed opponent attack: dash in
+  if(o.state==='attack' && o.atkHit===false && dist<160 && dist>90 && Math.random()<T.whiff*0.1){ mv=dir; }
+
+  if(f.onGround && Math.random()<T.jumpCh && dist<180){ f.vy=-13; f.onGround=false; }
+  f.vx=mv*T.speed;
+}
+
+// ---------------------------------------------------------------------------
+// Particles
+// ---------------------------------------------------------------------------
+function spawnParticles(x,y,n,color,spd){
+  for(let i=0;i<n;i++){
+    const ang=Math.random()*Math.PI*2, s=Math.random()*spd+1;
+    particles.push({x,y,vx:Math.cos(ang)*s,vy:Math.sin(ang)*s-2,life:1,color,r:Math.random()*3+1.5});
+  }
+}
+function updateParticles(dt){
+  for(let i=particles.length-1;i>=0;i--){
+    const p=particles[i];
+    p.vy+=18*dt; p.x+=p.vx; p.y+=p.vy; p.life-=dt*1.7;
+    if(p.life<=0||p.y>GROUND+10) particles.splice(i,1);
+  }
+}
+
+// ---------------------------------------------------------------------------
+// Round flow
+// ---------------------------------------------------------------------------
+function checkRoundEnd(){
+  if(roundPhase!=='fight') return;
+  let winner=null;
+  if(f1.hp<=0 && f2.hp<=0) winner='draw';
+  else if(f1.hp<=0) winner=f2;
+  else if(f2.hp<=0) winner=f1;
+  else if(timeLeft<=0){
+    if(f1.hp>f2.hp) winner=f1; else if(f2.hp>f1.hp) winner=f2; else winner='draw';
+  }
+  if(winner!==null){
+    roundPhase='ko';
+    if(winner!=='draw'){ showAnnounce='K.O.'; announceT=1.4; }
+    clearTimeout(koTimer);
+    koTimer=setTimeout(()=>{ koTimer=null; endRound(winner); }, 1100);
+  }
+}
+function endRound(winner){
+  if(winner!=='draw' && winner) winner.wins++;
+  const matchOver = f1.wins>=2 || f2.wins>=2;
+  state='result';
+  if(matchOver){
+    const champ = f1.wins>f2.wins? f1 : f2;
+    sfx.win();
+    resKicker.textContent='Vainqueur du duel';
+    resTitle.innerHTML = `<span class="result-name">${champ.name}</span> l'emporte !`;
+    resSub.textContent = `${champ.name} remporte le match ${Math.max(f1.wins,f2.wins)}–${Math.min(f1.wins,f2.wins)} et devient le Roi de la Baguette.`;
+    resBtns.innerHTML='';
+    addBtn('Rejouer', ()=>startGame(mode), false);
+    addBtn('Menu', ()=>toMenu(), true);
+  } else {
+    resKicker.textContent='Manche terminée';
+    if(winner==='draw'){ resTitle.textContent='Égalité !'; resSub.textContent='Personne ne cède une miette.'; }
+    else { resTitle.innerHTML=`<span class="result-name">${winner.name}</span> gagne la manche`; resSub.textContent=`Score — Pierre ${f1.wins} · ${f2.wins} Gaston`; }
+    resBtns.innerHTML='';
+    roundNum++;
+    addBtn(`Manche ${roundNum} →`, ()=>startRound(), false);
+    addBtn('Menu', ()=>toMenu(), true);
+  }
+  resEl.classList.remove('hide');
+}
+function addBtn(label, fn, alt){
+  const b=document.createElement('button'); b.className='play'+(alt?' alt':''); b.textContent=label;
+  b.onclick=fn; resBtns.appendChild(b);
+}
+function toMenu(){ clearTimeout(koTimer); koTimer=null; state='menu'; resEl.classList.add('hide'); pauseEl.classList.add('hide'); menuEl.classList.remove('hide'); }
+function togglePause(){
+  if(state==='fight'){ state='paused'; pauseEl.classList.remove('hide'); }
+  else if(state==='paused'){ state='fight'; pauseEl.classList.add('hide'); }
+}
+
+// ---------------------------------------------------------------------------
+// Drawing
+// ---------------------------------------------------------------------------
+function skyGradient(){
+  const g=ctx.createLinearGradient(0,0,0,H);
+  g.addColorStop(0,'#241a35'); g.addColorStop(.4,'#4a2e46'); g.addColorStop(.7,'#8a4a45'); g.addColorStop(1,'#c9754a');
+  return g;
+}
+function drawScene(){
+  ctx.fillStyle=skyGradient(); ctx.fillRect(0,0,W,H);
+  // moon
+  ctx.save(); ctx.shadowColor='rgba(255,236,200,.7)'; ctx.shadowBlur=40;
+  ctx.fillStyle='#fdf1d6'; ctx.beginPath(); ctx.arc(770,110,46,0,7); ctx.fill(); ctx.restore();
+  // stars
+  for(const s of stars){ s.t+=.03; const tw=.5+.5*Math.sin(s.t);
+    ctx.globalAlpha=.3+tw*.5; ctx.fillStyle='#fff7e6'; ctx.fillRect(s.x,s.y,s.r,s.r); }
+  ctx.globalAlpha=1;
+  // clouds
+  for(const cl of clouds){ cl.x-=cl.s; if(cl.x<-cl.w) cl.x=W+cl.w;
+    ctx.fillStyle='rgba(60,40,55,.35)';
+    ctx.beginPath(); ctx.ellipse(cl.x,cl.y,cl.w,cl.w*.32,0,0,7); ctx.fill(); }
+  drawEiffel(480, GROUND+40, 300);
+  drawSkyline();
+  // ground
+  const gg=ctx.createLinearGradient(0,GROUND+18,0,H);
+  gg.addColorStop(0,'#3a2c22'); gg.addColorStop(1,'#241a14');
+  ctx.fillStyle=gg; ctx.fillRect(0,GROUND+18,W,H-GROUND);
+  ctx.strokeStyle='rgba(224,164,88,.15)'; ctx.lineWidth=2;
+  ctx.beginPath(); ctx.moveTo(0,GROUND+18); ctx.lineTo(W,GROUND+18); ctx.stroke();
+  for(let x=0;x<W;x+=46){ ctx.beginPath(); ctx.moveTo(x,GROUND+22); ctx.lineTo(x+20,H);
+    ctx.strokeStyle='rgba(0,0,0,.18)'; ctx.stroke(); }
+}
+function draw(){
+  drawScene();
+  ctx.save();
+  if(shake>0){ ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake); }
+  if(f1&&f2){
+    drawShadow(f1); drawShadow(f2);
+    // draw the fighter that's further back (higher on screen / smaller x-depth) first
+    if(f1.y<=f2.y){ drawFighter(f1); drawFighter(f2); } else { drawFighter(f2); drawFighter(f1); }
+  }
+  for(const p of particles){ ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.color;
+    ctx.fillRect(p.x-p.r/2,p.y-p.r/2,p.r,p.r); }
+  ctx.globalAlpha=1;
+  ctx.restore();
+
+  if(f1&&f2) drawHUD();
+  if(announceT>0) drawAnnounce();
+  if(countdownT>0 && countdownT<3.2 && roundPhase==='intro') drawCountdown();
+}
+
+function drawShadow(f){
+  const sc=f.onGround?1:0.6;
+  ctx.fillStyle='rgba(0,0,0,.28)';
+  ctx.beginPath(); ctx.ellipse(f.x,GROUND+18,34*sc,9*sc,0,0,7); ctx.fill();
+}
+function drawEiffel(cx,baseY,h){
+  ctx.save();
+  ctx.strokeStyle='rgba(30,18,26,.65)'; ctx.fillStyle='rgba(30,18,26,.55)';
+  ctx.lineWidth=3;
+  const topY=baseY-h, topW=6, baseW=110, midY=baseY-h*.42, midW=52, plY=baseY-h*.14, plW=88;
+  function leg(sx){
+    ctx.beginPath();
+    ctx.moveTo(cx-baseW*sx, baseY);
+    ctx.quadraticCurveTo(cx-plW*sx*.7, plY, cx-midW*sx, midY);
+    ctx.quadraticCurveTo(cx-topW*sx*3, topY+h*.25, cx-topW*sx, topY);
+    ctx.stroke();
+  }
+  leg(1); leg(-1);
+  ctx.beginPath(); ctx.moveTo(cx-baseW*.72,plY); ctx.quadraticCurveTo(cx,plY-38,cx+baseW*.72,plY); ctx.stroke();
+  ctx.fillRect(cx-plW,plY-4,plW*2,8);
+  ctx.fillRect(cx-midW,midY-3,midW*2,6);
+  ctx.lineWidth=1.5; ctx.strokeStyle='rgba(30,18,26,.4)';
+  for(let i=0;i<6;i++){ const y=plY-(plY-midY)*(i/6); const wl=plW-(plW-midW)*(i/6);
+    ctx.beginPath(); ctx.moveTo(cx-wl,y); ctx.lineTo(cx+wl,y); ctx.stroke(); }
+  ctx.strokeStyle='rgba(30,18,26,.65)'; ctx.lineWidth=3;
+  ctx.beginPath(); ctx.moveTo(cx,topY); ctx.lineTo(cx,topY-18); ctx.stroke();
+  ctx.fillStyle='rgba(255,220,150,.9)'; ctx.beginPath(); ctx.arc(cx,topY-20,3,0,7); ctx.fill();
+  ctx.restore();
+}
+function drawSkyline(){
+  ctx.fillStyle='rgba(25,16,24,.5)';
+  const b=[[40,60,120],[130,90,90],[210,40,150],[720,70,120],[810,110,100],[900,50,150]];
+  for(const [x,w,hh] of b){ ctx.fillRect(x,GROUND+18-hh,w,hh);
+    ctx.fillStyle='rgba(255,210,140,.18)';
+    for(let wy=GROUND+18-hh+10;wy<GROUND+8;wy+=18) for(let wx=x+6;wx<x+w-6;wx+=16) if((wx*7+wy*13)%5<2) ctx.fillRect(wx,wy,4,6);
+    ctx.fillStyle='rgba(25,16,24,.5)';
+  }
+}
+function drawFighter(f){
+  ctx.save();
+  ctx.translate(f.x, f.y);
+  ctx.scale(f.facing,1);
+
+  const hurt = f.hitFlash>0 && Math.floor(f.hitFlash*30)%2===0;
+  const kod = f.state==='ko';
+  if(kod) ctx.rotate(f.facing* -0.9);
+
+  const step=Math.sin(f.walkPhase)*8;
+  ctx.strokeStyle='#8a5a2a'; ctx.lineWidth=6; ctx.lineCap='round';
+  ctx.beginPath(); ctx.moveTo(-8,-8); ctx.lineTo(-10-step*0.3, 0); ctx.stroke();
+  ctx.beginPath(); ctx.moveTo(8,-8);  ctx.lineTo(10+step*0.3, 0); ctx.stroke();
+  ctx.fillStyle='#5c3a1a'; ctx.beginPath(); ctx.ellipse(-11,2,7,4,0,0,7); ctx.fill();
+  ctx.beginPath(); ctx.ellipse(11,2,7,4,0,0,7); ctx.fill();
+
+  const bh=96, bw=40;
+  const bg=ctx.createLinearGradient(-bw/2,0,bw/2,0);
+  bg.addColorStop(0,'#a86a2e'); bg.addColorStop(.4,f.color); bg.addColorStop(.6,'#f2c987'); bg.addColorStop(1,'#a86a2e');
+  ctx.fillStyle=hurt?'#fff':bg;
+  roundedLoaf(0,-bh/2-8,bw,bh); ctx.fill();
+  ctx.strokeStyle='rgba(90,50,20,.55)'; ctx.lineWidth=2.5;
+  for(let i=0;i<4;i++){ const yy=-bh-2+ i*22+16;
+    ctx.beginPath(); ctx.moveTo(-6,yy); ctx.lineTo(8,yy-10); ctx.stroke(); }
+  ctx.fillStyle='rgba(255,250,235,.30)';
+  for(let i=0;i<7;i++){ const fx=((i*97)%bw)-bw/2+4, fy=-bh-4+((i*53)%bh); ctx.fillRect(fx,fy,2,2); }
+
+  // beret
+  ctx.save(); ctx.translate(0,-bh-6);
+  ctx.fillStyle=f.beret;
+  ctx.beginPath(); ctx.ellipse(2,-4,24,13,-.15,0,7); ctx.fill();
+  ctx.beginPath(); ctx.ellipse(0,2,20,7,0,0,7); ctx.fill();
+  ctx.fillStyle='#3a2c22'; ctx.beginPath(); ctx.arc(14,-11,2.5,0,7); ctx.fill();
+  ctx.restore();
+
+  // eyes
+  ctx.fillStyle='#3a2c22'; const eyY=-bh+8;
+  if(kod){ ctx.strokeStyle='#3a2c22'; ctx.lineWidth=2.4; xeye(6,eyY); xeye(20,eyY); }
+  else {
+    ctx.beginPath(); ctx.arc(8,eyY,3.4,0,7); ctx.fill();
+    ctx.beginPath(); ctx.arc(20,eyY,3.4,0,7); ctx.fill();
+    ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(9,eyY-1,1.1,0,7); ctx.fill();
+    ctx.beginPath(); ctx.arc(21,eyY-1,1.1,0,7); ctx.fill();
+  }
+  ctx.strokeStyle='#5c3a1a'; ctx.lineWidth=2; ctx.beginPath();
+  if(f.state==='attack'){ ctx.arc(14,eyY+13,4,0,Math.PI); }
+  else { ctx.moveTo(9,eyY+12); ctx.quadraticCurveTo(14,eyY+15,20,eyY+12); }
+  ctx.stroke();
+
+  // arm + weapon baguette
+  ctx.save();
+  ctx.translate(16,-bh/2-4);
+  const isSuper = f.state==='attack' && f.atk==='super';
+  const base=-0.5, ang=base + f.swing*1.5;
+  ctx.rotate(ang);
+  ctx.strokeStyle='#8a5a2a'; ctx.lineWidth=6; ctx.lineCap='round';
+  ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(22,0); ctx.stroke();
+  ctx.translate(22,0);
+  const wlen = isSuper?74:58;
+  const wg=ctx.createLinearGradient(0,-6,0,6);
+  if(isSuper){ wg.addColorStop(0,'#ffe07a'); wg.addColorStop(.5,'#fff1b0'); wg.addColorStop(1,'#e0a12a'); }
+  else { wg.addColorStop(0,'#e8b96e'); wg.addColorStop(.5,'#f2c987'); wg.addColorStop(1,'#b07a34'); }
+  ctx.fillStyle=wg; roundedRect(0,-7,wlen,14,7); ctx.fill();
+  ctx.strokeStyle='rgba(90,50,20,.5)'; ctx.lineWidth=1.8;
+  for(let i=1;i<5;i++){ ctx.beginPath(); ctx.moveTo(6+i*10,-3); ctx.lineTo(10+i*10,3); ctx.stroke(); }
+  if(f.state==='attack' && f.swing>.3){
+    ctx.globalAlpha=isSuper?.4:.25; ctx.strokeStyle=isSuper?'#fff6cf':'#fff3dd';
+    ctx.lineWidth=isSuper?16:10; ctx.lineCap='round';
+    ctx.beginPath(); ctx.arc(-22,0,isSuper?76:64,-0.6,0.5); ctx.stroke(); ctx.globalAlpha=1;
+  }
+  ctx.restore();
+
+  if(f.blocking && f.blockT>0){
+    ctx.globalAlpha=Math.min(.5,f.blockT*2.5);
+    ctx.strokeStyle='#ffe7b0'; ctx.lineWidth=3;
+    ctx.beginPath(); ctx.ellipse(22,-bh/2-6,26,54,0,-1.2,1.2); ctx.stroke();
+    ctx.globalAlpha=1;
+  }
+  ctx.restore();
+}
+function xeye(x,y){ ctx.beginPath(); ctx.moveTo(x-3,y-3); ctx.lineTo(x+3,y+3); ctx.moveTo(x+3,y-3); ctx.lineTo(x-3,y+3); ctx.stroke(); }
+function roundedLoaf(cx,topY,w,h){
+  const r=w/2; ctx.beginPath();
+  ctx.moveTo(cx-r,topY+r); ctx.arc(cx,topY+r,r,Math.PI,0);
+  ctx.lineTo(cx+r,topY+h-r); ctx.arc(cx,topY+h-r,r,0,Math.PI); ctx.closePath();
+}
+function roundedRect(x,y,w,h,r){
+  ctx.beginPath(); ctx.moveTo(x+r,y);
+  ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
+  ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
+}
+
+// ---------------------------------------------------------------------------
+// HUD
+// ---------------------------------------------------------------------------
+function drawHUD(){
+  hpBar(30,26,360,f1,false,'Pierre',f1.wins);
+  hpBar(W-30-360,26,360,f2,true,'Gaston',f2.wins);
+  meterBar(30,52,360,f1,false);
+  meterBar(W-30-360,52,360,f2,true);
+  ctx.save();
+  ctx.fillStyle='rgba(18,15,22,.7)'; roundedRect(W/2-40,20,80,54,10); ctx.fill();
+  ctx.strokeStyle='rgba(224,164,88,.5)'; ctx.lineWidth=1.5; ctx.stroke();
+  ctx.fillStyle='#f5f2ea'; ctx.font='800 30px Georgia,serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
+  ctx.fillText(Math.ceil(Math.max(0,timeLeft)), W/2, 48);
+  ctx.restore();
+  if(muted){ ctx.save(); ctx.fillStyle='rgba(245,242,234,.6)'; ctx.font='600 12px -apple-system,Arial';
+    ctx.textAlign='center'; ctx.fillText('🔇 muet (M)', W/2, 92); ctx.restore(); }
+}
+function hpBar(x,y,w,f,right,name,wins){
+  const h=20; ctx.save();
+  ctx.fillStyle='#f5f2ea'; ctx.font='700 15px -apple-system,Arial'; ctx.textBaseline='alphabetic';
+  ctx.textAlign=right?'right':'left'; ctx.fillText(name, right? x+w : x, y-6);
+  for(let i=0;i<2;i++){ const px=right? x+w-8-i*16 : x+8+i*16;
+    ctx.beginPath(); ctx.arc(px, y-11, 5, 0,7);
+    ctx.fillStyle = i<wins? '#e0a458':'rgba(255,255,255,.22)'; ctx.fill(); }
+  ctx.fillStyle='rgba(18,15,22,.6)'; roundedRect(x-3,y-3,w+6,h+6,7); ctx.fill();
+  ctx.fillStyle='rgba(0,0,0,.4)'; roundedRect(x,y,w,h,5); ctx.fill();
+  const pct=f.hp/f.maxhp, fw=w*pct, fx = right? x+w-fw : x;
+  const grad=ctx.createLinearGradient(0,y,0,y+h);
+  const col = pct>.4? ['#e0a458','#c65f38'] : ['#e07a45','#a8321f'];
+  grad.addColorStop(0,col[0]); grad.addColorStop(1,col[1]);
+  ctx.fillStyle=grad; roundedRect(fx,y,fw,h,5); ctx.fill();
+  ctx.strokeStyle='rgba(224,164,88,.4)'; ctx.lineWidth=1.5; roundedRect(x-3,y-3,w+6,h+6,7); ctx.stroke();
+  ctx.restore();
+}
+function meterBar(x,y,w,f,right){
+  const h=7; ctx.save();
+  ctx.fillStyle='rgba(0,0,0,.4)'; roundedRect(x,y,w,h,3); ctx.fill();
+  const pct=f.meter/f.maxMeter, fw=w*pct, fx=right? x+w-fw : x;
+  const full=f.meter>=f.maxMeter;
+  const grad=ctx.createLinearGradient(0,y,0,y+h);
+  if(full){ const t=.5+.5*Math.sin(performance.now()/120);
+    grad.addColorStop(0,`rgba(255,${200+40*t|0},120,1)`); grad.addColorStop(1,'#e0a12a'); }
+  else { grad.addColorStop(0,'#7fd3e0'); grad.addColorStop(1,'#3a8fb0'); }
+  ctx.fillStyle=grad; roundedRect(fx,y,fw,h,3); ctx.fill();
+  if(full){ ctx.fillStyle='#ffe7b0'; ctx.font='700 9px -apple-system,Arial';
+    ctx.textAlign=right?'right':'left'; ctx.textBaseline='top';
+    ctx.fillText('CRUST CRUSHER PRÊT', right? x+w : x, y+9); }
+  ctx.restore();
+}
+function drawAnnounce(){
+  ctx.save();
+  const a=Math.min(1,announceT*1.4)*Math.min(1,(1.6-announceT)*4+.2);
+  ctx.globalAlpha=Math.max(0,Math.min(1,a));
+  ctx.textAlign='center'; ctx.textBaseline='middle';
+  ctx.fillStyle=showAnnounce==='K.O.'?'#e0603a':'#f5f2ea';
+  ctx.font="italic 800 60px Georgia,'Playfair Display',serif";
+  ctx.shadowColor='rgba(198,95,56,.6)'; ctx.shadowBlur=24;
+  ctx.fillText(showAnnounce, W/2, 200);
+  ctx.restore();
+}
+function drawCountdown(){
+  const n=Math.ceil(countdownT-0.2);
+  ctx.save(); ctx.textAlign='center'; ctx.textBaseline='middle';
+  const frac=(countdownT-0.2)%1;
+  ctx.globalAlpha=Math.min(1,frac*2);
+  const sc=1+ (1-frac)*0.4;
+  ctx.translate(W/2,270); ctx.scale(sc,sc);
+  if(n>0){ ctx.fillStyle='#e0a458'; ctx.font="800 90px Georgia,serif";
+    ctx.shadowColor='rgba(0,0,0,.5)'; ctx.shadowBlur=18; ctx.fillText(String(n),0,0); }
+  ctx.restore();
+}
+
+// ---------------------------------------------------------------------------
+// Main loop
+// ---------------------------------------------------------------------------
+let last=performance.now();
+function loop(now){
+  let dt=(now-last)/1000; last=now;
+  if(dt>0.05) dt=0.05;
+
+  if(state==='fight'){
+    if(freeze>0){ freeze-=dt; }
+    else {
+      if(announceT>0) announceT-=dt;
+
+      if(roundPhase==='intro'){
+        countdownT-=dt;
+        const n=Math.ceil(countdownT-0.2);
+        if(n<lastCount && n>0){ lastCount=n; sfx.count(); }
+        if(countdownT<=0.2){ roundPhase='fight'; showAnnounce='COMBATTEZ !'; announceT=1.0; sfx.fight(); }
+        // idle facing during intro (light physics only)
+        if(f1&&f2){ f1.vx*=.8; f2.vx*=.8; f1.facing=f2.x<f1.x?-1:1; f2.facing=f1.x<f2.x?-1:1; }
+      } else if(roundPhase==='fight'){
+        if(timeLeft>0) timeLeft-=dt;
+        updateFighter(f1,f2,dt);
+        updateFighter(f2,f1,dt);
+        checkRoundEnd();
+      } else if(roundPhase==='ko'){
+        // let bodies settle in slow-mo
+        updateFighter(f1,f2,dt*0.4);
+        updateFighter(f2,f1,dt*0.4);
+      }
+      updateParticles(dt);
+      if(shake>0) shake=Math.max(0,shake-dt*40);
+    }
+  }
+
+  if(f1&&f2) draw(); else drawScene();
+  clearEdges();
+  requestAnimationFrame(loop);
+}
+requestAnimationFrame(loop);
+
+// ---------------------------------------------------------------------------
+// Menu wiring
+// ---------------------------------------------------------------------------
+document.querySelectorAll('.play[data-mode]').forEach(b=>{
+  b.addEventListener('click',()=>{
+    const diffSel=document.getElementById('difficulty');
+    if(diffSel) difficulty=diffSel.value;
+    startGame(parseInt(b.dataset.mode));
+  });
+});
+document.querySelectorAll('#pause [data-pause]').forEach(b=>{
+  b.addEventListener('click',()=>{
+    const act=b.getAttribute('data-pause');
+    if(act==='resume') togglePause();
+    else if(act==='menu'){ togglePause(); toMenu(); }
+  });
+});
+bindTouch();
+
+// expose a tiny hook for automated smoke tests
+window.__BB__ = { get state(){return state;}, get f1(){return f1;}, get f2(){return f2;}, startGame };
+})();
+
+</script>
+</body>
+</html>
diff --git a/games/particle-text-starling/index.html b/games/particle-text-starling/index.html
index a00ed8b..93d2ecb 100644
--- a/games/particle-text-starling/index.html
+++ b/games/particle-text-starling/index.html
@@ -529,7 +529,9 @@
     octx.fillStyle = "#05060a"; octx.fillRect(0, 0, gw, gh);
     scatterAll();
     var f = 0;
+    function endRec(msg) { try { clearInterval(iv); } catch (e) {} recording = false; gifBtn.disabled = false; codeBtn.disabled = false; if (msg) toast(msg); }
     var iv = setInterval(function () {
+     try {
       // render particles straight at GIF resolution (preserves brightness + additive glow)
       octx.globalCompositeOperation = "source-over";
       octx.fillStyle = "rgba(5,6,10,0.34)"; octx.fillRect(0, 0, gw, gh); // motion trail
@@ -544,11 +546,13 @@
       frames.push(octx.getImageData(0, 0, gw, gh).data.slice());
       if (++f >= TOTAL) { clearInterval(iv); encode(); }
       else toast("Recording GIF… " + Math.round(f / TOTAL * 100) + "%", 900);
+     } catch (e) { console.log(e); endRec("GIF capture failed — try again"); }
     }, DELAY * 10);
 
     function encode() {
       toast("Encoding GIF…", 6000);
       setTimeout(function () {
+       try {
         // Sample for the palette: keep EVERY bright particle pixel (they're sparse but are the
         // whole point), and only a thin slice of the dark background — otherwise median-cut spends
         // all 256 slots on near-black and quantizes the glow away.
@@ -581,6 +585,7 @@
         setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
         recording = false; gifBtn.disabled = false; codeBtn.disabled = false;
         toast("GIF saved · " + ((bytes.length / 1024) | 0) + " KB", 2400);
+       } catch (e) { console.log(e); endRec("GIF encode failed — try again"); }
       }, 60);
     }
   }

← ddb5d46 auto-save: 2026-07-25T11:35:57 (1 files) — sync.sh  ·  back to Games Agentabrams  ·  Refine Baguette Brawl: combo system + damage scaling, impact 4eb380d →