Add Breakout to glemy-website's game catalog, generalizing game_card into a parameterized component at its second real caller
Context
The final concretely-scoped item of the approved Breakout plan's Phase 3: publishing Breakout's live demo through glemy-website (a separate repository, decisions.jsonl-tracked from glemy's own log per this project's existing cross-repo convention) alongside Tiers', and making the catalog page (/play) list both games. glemy_website/game_card.gleam's own pre-existing doc comment already anticipated this moment explicitly: 'more games, in different genres, are the actual test of whether glemy generalizes... this component -- and the page it's used on -- are shaped for that from the start rather than needing a rewrite when the second game lands.' tiers_card was hardcoded (icon/name/tagline/description/button text all Tiers-specific inline), with exactly one real caller until now.
Options considered
Decision
Generalized glemy_website/game_card.gleam's tiers_card into a parameterized game_card(base_url, demo, icon, name, tagline, description) function; tiers_card and a new breakout_card are now both thin callers of it. Added breakout_icon() built from Breakout's own real on-screen colors (paddle #5af, ball white, bricks #fa5 -- matching breakout.html's CSS and games/breakout.ball_color, the same 'real colors, not a generic icon' principle tiers_icon() already established). game_card.catalog now takes one DemoPaths per game (tiers_demo, breakout_demo) and returns both cards; glemy_website/pages/play.gleam's view threads both through. build.gleam gained two more prepare_play_demo_dir calls (play-demo-breakout-stable/-edge) alongside the existing Tiers pair, producing a second DemoPaths passed into play.view. .github/workflows/deploy.yml gained a second checkout-copy block (breakout.html renamed to index.html on copy, plus its own full build/dev/javascript copy, mirroring the existing Tiers block exactly) for both stable and edge channels. The homepage (home.gleam) was deliberately left calling tiers_card directly, unchanged -- it's a single-game teaser by design (its own prose still reads 'glemy's first proof is Tiers, below'), not the full catalog; /play is where the generalized catalog now shows both games.
Verification
gleam build and gleam test both pass in glemy-website (17/17, unchanged -- no test exercises game_card/build.gleam directly, matching the pre-existing lack of coverage there). Ran a full local build simulating .github/workflows/deploy.yml's own steps (copied glemy's real decisions.jsonl/development-plan.jsonl and real index.html/breakout.html + build/dev/javascript into static/play-demo-edge and static/play-demo-breakout-edge, then gleam run -m build with a real GLEMY_WEBSITE_BASE_URL): build succeeded, dist/play-demo-breakout-edge-<hash>/ was produced as its own independently content-hashed directory containing a real, self-contained copy of breakout.html (renamed to index.html) and game_breakout.mjs/game_breakout_ffi.mjs, and dist/play/index.html contains both 'Play Tiers ↗'/'Play Breakout ↗' buttons linking to the correct hashed edge paths (stable correctly fell back to the unhashed /play-demo-stable and /play-demo-breakout-stable paths, since no stable checkout exists in this local simulation -- exactly prepare_play_demo_dir's documented, deliberate fallback behavior, not a bug).
Consequences
glemy-website's /play catalog now has two real entries, the first genuine test of whether game_card's shape holds up past one caller -- it did, with zero structural changes needed beyond parameterizing what was already hardcoded. A third game follows the exact same three-piece pattern (an icon function, a *_card wrapper calling game_card, one more prepare_play_demo_dir pair plus one more deploy.yml copy block) rather than needing new design. This is also the first time glemy's own repo (game_breakout.gleam/breakout.html, decision 0053) and glemy-website's independent deploy pipeline have been exercised together for a second game end-to-end, closing out Breakout's full roadmap (decisions 0051-0055) other than the deliberately-deferred hands-on feel-tuning (decision 0054).