// Cosmetics registry (window.COSMETICS) — the launch catalogue for the Cosmetics store.
// Data only: the kit (gamekit.cosmetics) owns storage, the economy (trophies = challenge
// points) and the store modal; games apply a selected item in their own render.
//   item: { id: '<game>.<set>.<key>', game ('' = site-wide), set, name, desc, price, painter,
//           music? (kit track id), audio? (see "audio builders") }
// Rules the tests enforce: ids unique, every set has exactly one FREE default (price 0),
// prices stay in the bands (10/25 cheap · 50 mid · 100 premium) except flagged exceptions
// (Meadow Flyer's progressive bird tail, capped at the 🏆 500 hard ceiling).
// The painter draws a small swatch preview (any square canvas) — it must not touch the DOM.
(function () {
  'use strict';

  // ---- swatch painter helpers (w×h canvas, drawing only) ----
  function fill(c) { return function (g, w, h) { g.fillStyle = c; g.fillRect(0, 0, w, h); }; }
  function grad(stops, ang) { // ang: 0 = horizontal, 1 = diagonal, 2 = vertical
    return function (g, w, h) {
      var lg = g.createLinearGradient(0, 0, ang === 2 ? 0 : w, ang === 0 ? 0 : h);
      for (var i = 0; i < stops.length; i++) lg.addColorStop(i / (stops.length - 1), stops[i]);
      g.fillStyle = lg; g.fillRect(0, 0, w, h);
    };
  }
  function orb(col, glow, bg) {
    return function (g, w, h) {
      g.fillStyle = bg || '#0a1018'; g.fillRect(0, 0, w, h);
      var r = Math.min(w, h) * 0.3;
      if (glow) { g.shadowColor = glow; g.shadowBlur = r * 0.9; }
      g.fillStyle = col; g.beginPath(); g.arc(w / 2, h / 2, r, 0, 7); g.fill();
      g.shadowBlur = 0;
      g.fillStyle = 'rgba(255,255,255,0.85)'; g.beginPath(); g.arc(w / 2 - r * 0.3, h / 2 - r * 0.3, r * 0.28, 0, 7); g.fill();
    };
  }
  function rainbowOrb(g, w, h) {
    g.fillStyle = '#0a1018'; g.fillRect(0, 0, w, h);
    var r = Math.min(w, h) * 0.32, cx = w / 2, cy = h / 2;
    var cols = ['#ff5b5b', '#ffd166', '#7fe0a0', '#7fd0ff', '#b98cff'];
    for (var i = 0; i < cols.length; i++) {
      g.fillStyle = cols[i];
      g.beginPath(); g.moveTo(cx, cy);
      g.arc(cx, cy, r, (i / cols.length) * 6.283 - 1.57, ((i + 1) / cols.length) * 6.283 - 1.57);
      g.closePath(); g.fill();
    }
    g.fillStyle = 'rgba(255,255,255,0.8)'; g.beginPath(); g.arc(cx - r * 0.3, cy - r * 0.3, r * 0.25, 0, 7); g.fill();
  }
  function star(col, glow, bg) {
    return function (g, w, h) {
      g.fillStyle = bg || '#0a1018'; g.fillRect(0, 0, w, h);
      var cx = w / 2, cy = h / 2, R = Math.min(w, h) * 0.36, r2 = R * 0.45;
      if (glow) { g.shadowColor = glow; g.shadowBlur = R * 0.6; }
      g.fillStyle = col; g.beginPath();
      for (var i = 0; i < 10; i++) { var rr = i % 2 ? r2 : R, a = -1.57 + i * 0.6283; g[i ? 'lineTo' : 'moveTo'](cx + Math.cos(a) * rr, cy + Math.sin(a) * rr); }
      g.closePath(); g.fill(); g.shadowBlur = 0;
    };
  }
  function gem(g, w, h) {
    g.fillStyle = '#0a1018'; g.fillRect(0, 0, w, h);
    var cx = w / 2, cy = h / 2, r = Math.min(w, h) * 0.34;
    var lg = g.createLinearGradient(cx - r, cy - r, cx + r, cy + r);
    lg.addColorStop(0, '#b98cff'); lg.addColorStop(1, '#7fd0ff');
    g.shadowColor = '#9f8cff'; g.shadowBlur = r * 0.7; g.fillStyle = lg;
    g.beginPath(); g.moveTo(cx, cy - r); g.lineTo(cx + r, cy); g.lineTo(cx, cy + r); g.lineTo(cx - r, cy); g.closePath(); g.fill();
    g.shadowBlur = 0;
    g.strokeStyle = 'rgba(255,255,255,0.6)'; g.lineWidth = 1;
    g.beginPath(); g.moveTo(cx - r * 0.5, cy - r * 0.5); g.lineTo(cx + r * 0.5, cy + r * 0.5); g.stroke();
  }
  function cherry(g, w, h) {
    g.fillStyle = '#0a1018'; g.fillRect(0, 0, w, h);
    var cx = w / 2, cy = h / 2, r = Math.min(w, h) * 0.19;
    g.strokeStyle = '#7fe0a0'; g.lineWidth = 2;
    g.beginPath(); g.moveTo(cx - r * 0.7, cy); g.quadraticCurveTo(cx, cy - r * 2.2, cx + r * 0.9, cy - r * 0.4); g.stroke();
    g.shadowColor = '#ff5b5b'; g.shadowBlur = r;
    g.fillStyle = '#ff4b4b'; g.beginPath(); g.arc(cx - r * 0.7, cy + r * 0.5, r, 0, 7); g.fill();
    g.fillStyle = '#ff6b6b'; g.beginPath(); g.arc(cx + r * 0.9, cy + r * 0.2, r, 0, 7); g.fill();
    g.shadowBlur = 0;
  }
  function ball(body, halo, core) {
    return function (g, w, h) {
      g.fillStyle = '#0a1018'; g.fillRect(0, 0, w, h);
      var r = Math.min(w, h) * 0.26, cx = w / 2, cy = h / 2;
      g.globalAlpha = 0.35; g.fillStyle = halo; g.beginPath(); g.arc(cx, cy, r * 1.8, 0, 7); g.fill();
      g.globalAlpha = 1; g.fillStyle = body; g.beginPath(); g.arc(cx, cy, r, 0, 7); g.fill();
      g.fillStyle = core; g.beginPath(); g.arc(cx - r * 0.25, cy - r * 0.25, r * 0.45, 0, 7); g.fill();
    };
  }
  function eightBall(g, w, h) {
    g.fillStyle = '#0a1018'; g.fillRect(0, 0, w, h);
    var r = Math.min(w, h) * 0.3, cx = w / 2, cy = h / 2;
    g.fillStyle = '#15161c'; g.beginPath(); g.arc(cx, cy, r, 0, 7); g.fill();
    g.strokeStyle = '#3a4050'; g.lineWidth = 1; g.beginPath(); g.arc(cx, cy, r, 0, 7); g.stroke();
    g.fillStyle = '#fff'; g.beginPath(); g.arc(cx, cy, r * 0.45, 0, 7); g.fill();
    g.fillStyle = '#15161c'; g.font = 'bold ' + Math.round(r * 0.62) + 'px system-ui, sans-serif';
    g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText('8', cx, cy + r * 0.03);
  }
  function paddle(colsOrPainter, glow) {
    return function (g, w, h) {
      g.fillStyle = '#0a1018'; g.fillRect(0, 0, w, h);
      var pw = w * 0.72, ph = Math.max(5, h * 0.2), px = (w - pw) / 2, py = (h - ph) / 2;
      if (glow) { g.shadowColor = glow; g.shadowBlur = ph; }
      if (typeof colsOrPainter === 'string') g.fillStyle = colsOrPainter;
      else { var lg = g.createLinearGradient(px, py, px + pw, py + ph); for (var i = 0; i < colsOrPainter.length; i++) lg.addColorStop(i / (colsOrPainter.length - 1), colsOrPainter[i]); g.fillStyle = lg; }
      if (g.roundRect) { g.beginPath(); g.roundRect(px, py, pw, ph, ph / 2); g.fill(); } else g.fillRect(px, py, pw, ph);
      g.shadowBlur = 0;
    };
  }
  function castle(wall, roof, bg) {
    return function (g, w, h) {
      g.fillStyle = bg || '#12202e'; g.fillRect(0, 0, w, h);
      var bw = w * 0.56, bh = h * 0.42, bx = (w - bw) / 2, by = h * 0.42;
      g.fillStyle = wall; g.fillRect(bx, by, bw, bh);
      var mw = bw / 5;
      for (var i = 0; i < 3; i++) g.fillRect(bx + i * 2 * mw, by - mw, mw, mw); // battlements
      g.fillStyle = roof; g.fillRect(bx + bw * 0.38, by + bh * 0.4, bw * 0.24, bh * 0.6); // gate
    };
  }
  function target(rings, bg) {
    return function (g, w, h) {
      g.fillStyle = bg || '#101820'; g.fillRect(0, 0, w, h);
      var r = Math.min(w, h) * 0.36, cx = w / 2, cy = h / 2;
      for (var i = 0; i < rings.length; i++) { g.fillStyle = rings[i]; g.beginPath(); g.arc(cx, cy, r * (1 - i / rings.length), 0, 7); g.fill(); }
    };
  }
  // Range's regulation target, exactly as drawTarget renders it (dark disc + orange outline rings)
  function ringsTarget(g, w, h) {
    g.fillStyle = '#0d1117'; g.fillRect(0, 0, w, h);
    var cx = w / 2, cy = h / 2, r = Math.min(w, h) * 0.32;
    g.fillStyle = '#1a2030'; g.beginPath(); g.arc(cx, cy, r, 0, 7); g.fill();
    g.strokeStyle = '#ff7a3c'; g.lineWidth = 2.5; g.beginPath(); g.arc(cx, cy, r, 0, 7); g.stroke();
    g.strokeStyle = 'rgba(255,122,60,0.6)'; g.lineWidth = 1.5; g.beginPath(); g.arc(cx, cy, r * 0.65, 0, 7); g.stroke();
    g.strokeStyle = '#ff7a3c'; g.beginPath(); g.arc(cx, cy, r * 0.35, 0, 7); g.stroke();
    g.fillStyle = '#ff7a3c'; g.beginPath(); g.arc(cx, cy, r * 0.12, 0, 7); g.fill();
    g.strokeStyle = 'rgba(255,122,60,0.53)'; g.lineWidth = 1;
    var tk = r * 1.15;
    [[1, 0], [-1, 0], [0, 1], [0, -1]].forEach(function (d) {
      g.beginPath(); g.moveTo(cx + d[0] * tk * 0.7, cy + d[1] * tk * 0.7); g.lineTo(cx + d[0] * tk, cy + d[1] * tk); g.stroke();
    });
  }
  // Range's classic hit marker — the crisp orange X drawHitMarker draws
  function markerX(g, w, h) {
    g.fillStyle = '#101820'; g.fillRect(0, 0, w, h);
    var cx = w / 2, cy = h / 2, s = Math.min(w, h) * 0.2;
    g.strokeStyle = '#ff7a3c'; g.lineWidth = 2; g.lineCap = 'round';
    g.beginPath(); g.moveTo(cx - s, cy - s); g.lineTo(cx + s, cy + s); g.stroke();
    g.beginPath(); g.moveTo(cx + s, cy - s); g.lineTo(cx - s, cy + s); g.stroke();
  }
  function popDots(col, bg) { // Bubble Pop's classic pop — round particles, not rays
    return function (g, w, h) {
      g.fillStyle = bg || '#0a1420'; g.fillRect(0, 0, w, h);
      var cx = w / 2, cy = h / 2, R = Math.min(w, h) * 0.3;
      for (var i = 0; i < 8; i++) {
        var a = i * 0.785 + 0.39, d = R * (0.55 + (i % 3) * 0.22);
        g.fillStyle = col;
        g.beginPath(); g.arc(cx + Math.cos(a) * d, cy + Math.sin(a) * d, 2 + (i % 3) * 1.2, 0, 7); g.fill();
      }
    };
  }
  function burst(cols, bg) {
    return function (g, w, h) {
      g.fillStyle = bg || '#0a1420'; g.fillRect(0, 0, w, h);
      var cx = w / 2, cy = h / 2, R = Math.min(w, h) * 0.36;
      for (var i = 0; i < 8; i++) {
        var a = i * 0.785;
        g.strokeStyle = cols[i % cols.length]; g.lineWidth = 2;
        g.beginPath(); g.moveTo(cx + Math.cos(a) * R * 0.3, cy + Math.sin(a) * R * 0.3);
        g.lineTo(cx + Math.cos(a) * R, cy + Math.sin(a) * R); g.stroke();
      }
    };
  }
  function palette(cols) {
    return function (g, w, h) {
      var n = cols.length, bw = w / n;
      for (var i = 0; i < n; i++) { g.fillStyle = cols[i]; g.fillRect(i * bw, 0, bw + 1, h); }
    };
  }
  function ship(col, bg) {
    return function (g, w, h) {
      g.fillStyle = bg || '#05070d'; g.fillRect(0, 0, w, h);
      var cx = w / 2, cy = h / 2, r = Math.min(w, h) * 0.32;
      g.strokeStyle = col; g.lineWidth = 2; g.shadowColor = col; g.shadowBlur = r * 0.5;
      g.beginPath(); g.moveTo(cx, cy - r); g.lineTo(cx + r * 0.75, cy + r * 0.8); g.lineTo(cx, cy + r * 0.35); g.lineTo(cx - r * 0.75, cy + r * 0.8); g.closePath(); g.stroke();
      g.shadowBlur = 0;
    };
  }
  function trail(cols, bg) {
    return function (g, w, h) {
      g.fillStyle = bg || '#05070d'; g.fillRect(0, 0, w, h);
      var n = 6;
      for (var i = 0; i < n; i++) {
        var t = i / (n - 1);
        g.globalAlpha = 0.25 + 0.75 * t;
        g.fillStyle = cols[Math.min(cols.length - 1, Math.floor(t * cols.length))];
        var r = 2 + t * Math.min(w, h) * 0.11;
        g.beginPath(); g.arc(w * (0.15 + 0.7 * t), h / 2 + Math.sin(t * 5) * h * 0.08, r, 0, 7); g.fill();
      }
      g.globalAlpha = 1;
    };
  }
  function bird(body, wing, glow) {
    return function (g, w, h) {
      g.fillStyle = '#0e1622'; g.fillRect(0, 0, w, h);
      var cx = w / 2, cy = h / 2, r = Math.min(w, h) * 0.26;
      if (glow) { g.shadowColor = glow; g.shadowBlur = r; }
      g.fillStyle = body; g.beginPath(); g.arc(cx, cy, r, 0, 7); g.fill();
      g.shadowBlur = 0;
      g.fillStyle = wing; g.beginPath(); g.ellipse(cx - r * 0.35, cy + r * 0.1, r * 0.62, r * 0.4, -0.5, 0, 7); g.fill();
      g.fillStyle = '#ffb03a'; g.beginPath(); g.moveTo(cx + r, cy - r * 0.15); g.lineTo(cx + r * 1.55, cy); g.lineTo(cx + r, cy + r * 0.2); g.closePath(); g.fill();
      g.fillStyle = '#111'; g.beginPath(); g.arc(cx + r * 0.4, cy - r * 0.3, r * 0.14, 0, 7); g.fill();
    };
  }
  // Meadow Flyer's bee, ported from the game's drawBirdModel (model space R=14, mid-flap)
  function beeBird(g, w, h) {
    g.fillStyle = '#0e1622'; g.fillRect(0, 0, w, h);
    var s = Math.min(w, h) / 46;
    g.save(); g.translate(w / 2, h / 2 + s * 6); g.scale(s, s);
    var R = 14, flap = 2;
    g.fillStyle = '#f9e040'; g.beginPath(); g.ellipse(0, 0, R, R * 0.8, 0, 0, 7); g.fill();
    g.save(); g.beginPath(); g.ellipse(0, 0, R, R * 0.8, 0, 0, 7); g.clip(); // brown stripes
    g.fillStyle = '#6a4a18'; g.fillRect(-10, -R, 5, R * 2); g.fillRect(-1, -R, 5, R * 2);
    g.restore();
    g.fillStyle = '#6a4a18'; // stinger
    g.beginPath(); g.moveTo(-R + 1, -2); g.lineTo(-R - 5, 0); g.lineTo(-R + 1, 2); g.closePath(); g.fill();
    g.fillStyle = 'rgba(255,255,255,0.75)'; // gauzy wings above the back
    g.beginPath(); g.ellipse(-2, -flap - 9, 9, 5, -0.5, 0, 7); g.fill();
    g.beginPath(); g.ellipse(3, -flap - 8, 7, 4, -0.3, 0, 7); g.fill();
    g.strokeStyle = '#8a6020'; g.lineWidth = 1.5; // antennae + pom-poms
    g.beginPath(); g.moveTo(-2, -R * 0.7); g.quadraticCurveTo(-8, -R * 1.3, -12, -R * 1.5); g.stroke();
    g.beginPath(); g.moveTo(4, -R * 0.7); g.quadraticCurveTo(8, -R * 1.3, 11, -R * 1.5); g.stroke();
    g.fillStyle = '#f9c080';
    g.beginPath(); g.arc(-12, -R * 1.5, 2.5, 0, 7); g.fill();
    g.beginPath(); g.arc(11, -R * 1.5, 2.5, 0, 7); g.fill();
    g.fillStyle = '#3a2a17'; g.beginPath(); g.arc(R * 0.55, -2, 3, 0, 7); g.fill(); // eye
    g.fillStyle = '#fff'; g.beginPath(); g.arc(R * 0.55 + 1.05, -3.2, 1.2, 0, 7); g.fill();
    g.restore();
  }
  // rot matches the in-use cursor's NW tilt (game-kit CURSOR_TWEAK) so the swatch previews the real thing
  function cursorSwatch(draw, rot) {
    return function (g, w, h) { g.fillStyle = '#131a27'; g.fillRect(0, 0, w, h); g.save(); g.translate(w / 2, h / 2); if (rot) g.rotate(rot); g.scale(Math.min(w, h) / 34, Math.min(w, h) / 34); draw(g); g.restore(); };
  }
  // cursor glyph painters draw in a 34×34 box centered on 0,0 — shared by the swatch AND the
  // kit's live cursor renderer (gamekit reads COSMETICS.cursors[key] to build the cursor image)
  var CURSORS = {
    classic: function (g) { g.fillStyle = '#fff'; g.strokeStyle = '#000'; g.lineWidth = 1.4; g.beginPath(); g.moveTo(-6, -10); g.lineTo(-6, 8); g.lineTo(-1.5, 4); g.lineTo(1.5, 10); g.lineTo(4.5, 8.6); g.lineTo(1.6, 2.8); g.lineTo(7, 2); g.closePath(); g.fill(); g.stroke(); },
    crosshair: function (g) { g.strokeStyle = '#7fe0a0'; g.lineWidth = 2; g.beginPath(); g.arc(0, 0, 8, 0, 7); g.stroke(); g.beginPath(); g.moveTo(0, -13); g.lineTo(0, -4); g.moveTo(0, 4); g.lineTo(0, 13); g.moveTo(-13, 0); g.lineTo(-4, 0); g.moveTo(4, 0); g.lineTo(13, 0); g.stroke(); g.fillStyle = '#7fe0a0'; g.beginPath(); g.arc(0, 0, 1.6, 0, 7); g.fill(); },
    paw: function (g) { g.fillStyle = '#f0a6c8'; g.beginPath(); g.ellipse(0, 4, 6.4, 5.4, 0, 0, 7); g.fill(); [[-7, -3], [-2.6, -7], [2.6, -7], [7, -3]].forEach(function (p) { g.beginPath(); g.arc(p[0], p[1], 2.6, 0, 7); g.fill(); }); },
    sword: function (g) { g.save(); g.rotate(-0.785); g.fillStyle = '#cfe0f0'; g.fillRect(-1.7, -12, 3.4, 15); g.fillStyle = '#8a97ad'; g.beginPath(); g.moveTo(-1.7, -12); g.lineTo(0, -15); g.lineTo(1.7, -12); g.closePath(); g.fill(); g.fillStyle = '#c8894a'; g.fillRect(-5.5, 3, 11, 3); g.fillRect(-1.7, 6, 3.4, 6); g.restore(); },
    comet: function (g) { var lg = g.createLinearGradient(-12, 12, 8, -8); lg.addColorStop(0, 'rgba(159,232,255,0)'); lg.addColorStop(1, '#9fe8ff'); g.strokeStyle = lg; g.lineWidth = 4; g.beginPath(); g.moveTo(-12, 12); g.lineTo(6, -6); g.stroke(); g.fillStyle = '#e8fbff'; g.shadowColor = '#9fe8ff'; g.shadowBlur = 8; g.beginPath(); g.arc(7, -7, 4, 0, 7); g.fill(); g.shadowBlur = 0; },
    rainbow: function (g) { var cols = ['#ff5b5b', '#ffd166', '#7fe0a0', '#7fd0ff', '#b98cff']; for (var i = 0; i < cols.length; i++) { g.strokeStyle = cols[i]; g.lineWidth = 2.2; g.beginPath(); g.moveTo(-12 + i * 1.4, 12 - i * 1.4); g.lineTo(5 + i * 0.4, -5 - i * 0.4); g.stroke(); } g.fillStyle = '#fff'; g.beginPath(); g.arc(7, -7, 3.4, 0, 7); g.fill(); },
    // terminal: a phosphor block (the live cursor is a blinking follower — see gamekit startTermCursor)
    terminal: function (g) { g.fillStyle = '#33ff88'; g.shadowColor = '#33ff88'; g.shadowBlur = 5; g.fillRect(-4, -9, 8, 17); g.shadowBlur = 0; },
    // game-themed crosshairs
    sonar: function (g) { // Minesweeper's cyan sonar ping
      g.strokeStyle = '#35e0ff'; g.lineWidth = 1.6;
      g.beginPath(); g.arc(0, 0, 11, 0, 7); g.stroke();
      g.globalAlpha = 0.5; g.beginPath(); g.arc(0, 0, 6.5, 0, 7); g.stroke(); g.globalAlpha = 1;
      g.beginPath();
      [[0, -14, 0, -9], [0, 9, 0, 14], [-14, 0, -9, 0], [9, 0, 14, 0]].forEach(function (l) { g.moveTo(l[0], l[1]); g.lineTo(l[2], l[3]); });
      g.stroke();
      g.fillStyle = '#35e0ff'; g.beginPath(); g.arc(0, 0, 1.8, 0, 7); g.fill();
    },
    reticle: function (g) { // Range's amber mil-dot scope
      g.strokeStyle = '#ffb35c'; g.lineWidth = 1.8;
      g.beginPath(); g.arc(0, 0, 10, 0, 7); g.stroke();
      g.beginPath(); g.moveTo(0, -15); g.lineTo(0, 15); g.moveTo(-15, 0); g.lineTo(15, 0); g.stroke();
      g.fillStyle = '#ffb35c';
      [[-6, 0], [6, 0], [0, -6], [0, 6]].forEach(function (p) { g.beginPath(); g.arc(p[0], p[1], 1.2, 0, 7); g.fill(); });
    },
    starfighter: function (g) { // Asteroids+ violet targeting brackets
      g.strokeStyle = '#b98cff'; g.lineWidth = 2; g.lineCap = 'round';
      for (var q = 0; q < 4; q++) {
        var a = Math.PI / 4 + q * Math.PI / 2, x1 = Math.cos(a) * 12, y1 = Math.sin(a) * 12;
        g.beginPath();
        g.moveTo(x1 - Math.cos(a + Math.PI / 2) * 5, y1 - Math.sin(a + Math.PI / 2) * 5);
        g.lineTo(x1, y1);
        g.lineTo(x1 - Math.cos(a - Math.PI / 2) * 5, y1 - Math.sin(a - Math.PI / 2) * 5);
        g.stroke();
      }
      g.fillStyle = '#e8dcff'; g.shadowColor = '#b98cff'; g.shadowBlur = 5;
      g.beginPath(); g.arc(0, 0, 2, 0, 7); g.fill(); g.shadowBlur = 0;
    },
  };

  // forcefield marker swatch — a glowing needle + triangle head over a faint track
  function forcefieldMarker(color) {
    return function (g, w, h) {
      g.fillStyle = '#0e1420'; g.fillRect(0, 0, w, h);
      var cx = w / 2, cy = h / 2;
      g.fillStyle = 'rgba(255,255,255,0.10)'; g.fillRect(w * 0.15, cy - 3, w * 0.7, 6);
      g.save(); g.shadowBlur = 8; g.shadowColor = color; g.fillStyle = color;
      g.fillRect(cx - 2, cy - h * 0.28, 4, h * 0.56);
      g.beginPath(); g.moveTo(cx, cy - h * 0.30); g.lineTo(cx - 7, cy - h * 0.30 - 9); g.lineTo(cx + 7, cy - h * 0.30 - 9); g.closePath(); g.fill();
      g.restore();
    };
  }

  // forcefield planet swatch — a small gradient world
  function planetSwatch(c0, c1) {
    return function (g, w, h) {
      g.fillStyle = '#0a1020'; g.fillRect(0, 0, w, h);
      var cx = w / 2, cy = h / 2, r = Math.min(w, h) * 0.34;
      var grd = g.createRadialGradient(cx - r * 0.3, cy - r * 0.3, r * 0.2, cx, cy, r);
      grd.addColorStop(0, c0); grd.addColorStop(1, c1);
      g.fillStyle = grd; g.beginPath(); g.arc(cx, cy, r, 0, 7); g.fill();
    };
  }

  // frog-bonk hammer swatch — handle + a head that previews the skin's shape
  function hammerSwatch(c0, c1, shape) {
    return function (g, w, h) {
      g.fillStyle = '#12240f'; g.fillRect(0, 0, w, h);
      var cx = w / 2, cy = h / 2, hw = w * 0.52, hh = h * 0.3;
      g.save(); g.translate(cx, cy); g.rotate(-0.5);
      g.strokeStyle = '#8a5c30'; g.lineWidth = 4; g.lineCap = 'round';
      g.beginPath(); g.moveTo(0, h * 0.34); g.lineTo(0, -h * 0.05); g.stroke();
      var lg = g.createLinearGradient(-hw / 2, -h * 0.05 - hh, hw / 2, -h * 0.05);
      lg.addColorStop(0, c0); lg.addColorStop(1, c1);
      g.fillStyle = lg;
      if (shape === 'fluff') {
        for (var i = 0; i < 6; i++) { var a = i * 1.05; g.beginPath(); g.arc(Math.cos(a) * hw * 0.26, -h * 0.05 - hh / 2 + Math.sin(a) * hh * 0.3, hh * 0.36, 0, 7); g.fill(); }
        g.beginPath(); g.ellipse(0, -h * 0.05 - hh / 2, hw / 2, hh / 2 + 1, 0, 0, 7); g.fill();
      } else if (shape === 'balloon') {
        g.globalAlpha = 0.9;
        g.beginPath(); g.ellipse(0, -h * 0.05 - hh / 2, hw / 2 + 2, hh / 2 + 3, 0, 0, 7); g.fill();
        g.globalAlpha = 1;
        g.fillStyle = 'rgba(255,255,255,0.6)'; g.beginPath(); g.ellipse(-hw * 0.16, -h * 0.1 - hh / 2, 4, 2.4, -0.5, 0, 7); g.fill();
      } else {
        if (g.roundRect) { g.beginPath(); g.roundRect(-hw / 2, -h * 0.05 - hh, hw, hh, 5); g.fill(); }
        else g.fillRect(-hw / 2, -h * 0.05 - hh, hw, hh);
        if (shape === 'stripe') {
          g.fillStyle = '#fff';
          for (var s = -1; s <= 1; s++) g.fillRect(s * hw * 0.3 - 2, -h * 0.05 - hh, 4, hh);
        }
        if (shape === 'gold') { g.fillStyle = 'rgba(255,255,255,0.7)'; g.beginPath(); g.ellipse(-hw * 0.2, -h * 0.08 - hh * 0.7, 4, 2, -0.5, 0, 7); g.fill(); }
      }
      g.restore();
    };
  }
  // frog-bonk meadow swatch — grass gradient + a season mark
  function meadowSwatch(stops, mark) {
    return function (g, w, h) {
      var lg = g.createLinearGradient(0, 0, 0, h);
      for (var i = 0; i < stops.length; i++) lg.addColorStop(i / (stops.length - 1), stops[i]);
      g.fillStyle = lg; g.fillRect(0, 0, w, h);
      if (mark === 'rain') { g.strokeStyle = 'rgba(190,225,255,0.7)'; g.lineWidth = 1.5; for (var r = 0; r < 5; r++) { g.beginPath(); g.moveTo(w * (0.15 + r * 0.17), h * 0.2); g.lineTo(w * (0.12 + r * 0.17), h * 0.45); g.stroke(); } }
      else if (mark === 'snow') { g.fillStyle = 'rgba(255,255,255,0.9)'; for (var s = 0; s < 6; s++) { g.beginPath(); g.arc(w * ((s * 0.31 + 0.13) % 1), h * ((s * 0.23 + 0.14) % 0.6), 2, 0, 7); g.fill(); } }
      else if (mark === 'leaf') { g.fillStyle = '#c85a28'; for (var l = 0; l < 4; l++) { g.beginPath(); g.ellipse(w * (0.2 + l * 0.2), h * (0.2 + (l % 2) * 0.18), 3.4, 2, l, 0, 7); g.fill(); } }
      else { g.fillStyle = '#ffd7e6'; for (var f = 0; f < 4; f++) { g.beginPath(); g.arc(w * (0.18 + f * 0.21), h * (0.62 + (f % 2) * 0.16), 2.4, 0, 7); g.fill(); } g.fillStyle = '#ffe08a'; g.beginPath(); g.arc(w * 0.75, h * 0.2, 5, 0, 7); g.fill(); }
    };
  }

  // waveform swatch for music tracks
  function wavePainter(col) {
    return function (g, w, h) {
      g.fillStyle = '#0a1018'; g.fillRect(0, 0, w, h);
      g.strokeStyle = col; g.lineWidth = Math.max(1.5, w * 0.03); g.lineJoin = 'round';
      g.shadowColor = col; g.shadowBlur = w * 0.08;
      g.beginPath();
      for (var i = 0; i <= 32; i++) { var x = (i / 32) * w, y = h / 2 + Math.sin(i * 0.9) * h * 0.28 * (0.4 + 0.6 * Math.abs(Math.sin(i * 0.35))); if (i) g.lineTo(x, y); else g.moveTo(x, y); }
      g.stroke(); g.shadowBlur = 0;
    };
  }
  // ---- audio builders (`audio` on an item) ----------------------------------------------------
  // A cosmetic that IS a sound but not a music track (an engine drone, a thruster) carries
  // `audio: (ac, out) => tune`, which wires a continuous graph into `out` and returns
  // `tune(t, sp)` — t = ac.currentTime, sp = 0..1 intensity (may reach ~1.1). SPEED is the only
  // input: there used to be a third `boost` flag, but it was hard-coded false at both call sites for
  // its whole life, and once wired up its deltas (a few hundred Hz, a Q nudge) were inaudible next to
  // what sp already does — and one of the three engines never read it at all. Dropped rather than
  // inflated: the engines are distinct enough on speed alone.
  // It lives HERE, not in the game, because cosmetics.js is loaded by the catalogue too: the shop's
  // ▶ preview has to make the noise on a page where the game's own code never runs.
  // Tube Racer's three engines (auditioned in plans/tube-engine-lab.html); their frequency ranges
  // are the game's cruise→top-speed span (14→52 units/s) folded into sp.
  function engReactor(ac, out) {   // a throb whose RATE rises with speed — the engine audibly races
    var lp = ac.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 600;
    var saw = ac.createOscillator(); saw.type = 'sawtooth'; saw.frequency.value = 70;
    var sg = ac.createGain(); sg.gain.value = 0.5;
    var trem = ac.createGain(); trem.gain.value = 1;
    var lfo = ac.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 4;
    var lg = ac.createGain(); lg.gain.value = 0.55;
    saw.connect(sg); sg.connect(lp); lp.connect(trem); trem.connect(out);
    lfo.connect(lg); lg.connect(trem.gain);
    saw.start(); lfo.start();
    return function (t, sp) {
      saw.frequency.setTargetAtTime(80.4 + sp * 98.8, t, 0.06);
      lp.frequency.setTargetAtTime(720 + sp * 1140, t, 0.1);
      lfo.frequency.setTargetAtTime(3.5 + sp * 13, t, 0.12);
    };
  }
  function engMaglev(ac, out) {    // clean sines a fifth apart with a shimmer — leaves room for the music
    var a = ac.createOscillator(); a.type = 'sine'; a.frequency.value = 80;
    var b = ac.createOscillator(); b.type = 'sine'; b.frequency.value = 120;
    var ag = ac.createGain(); ag.gain.value = 0.3;
    var bg = ac.createGain(); bg.gain.value = 0.16;
    var lfo = ac.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 0.7;
    var lg = ac.createGain(); lg.gain.value = 6;
    a.connect(ag); ag.connect(out); b.connect(bg); bg.connect(out);
    lfo.connect(lg); lg.connect(b.frequency);
    a.start(); b.start(); lfo.start();
    return function (t, sp) {
      var f = 112.4 + sp * 136.8;
      a.frequency.setTargetAtTime(f, t, 0.08);
      b.frequency.setTargetAtTime(f * 1.5, t, 0.08);
      bg.gain.setTargetAtTime(0.1 + sp * 0.14, t, 0.1);
      lg.gain.setTargetAtTime(4 + sp * 14, t, 0.12);
    };
  }
  function engTwostroke(ac, out) { // two squares beating hard through a bright bandpass — small and angry
    var bp = ac.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 700; bp.Q.value = 1.4;
    var a = ac.createOscillator(); a.type = 'square'; a.frequency.value = 90;
    var b = ac.createOscillator(); b.type = 'square'; b.frequency.value = 91; b.detune.value = 28;
    var ag = ac.createGain(); ag.gain.value = 0.3;
    var bg = ac.createGain(); bg.gain.value = 0.26;
    a.connect(ag); ag.connect(bp); b.connect(bg); bg.connect(bp); bp.connect(out);
    a.start(); b.start();
    return function (t, sp) {
      var f = 121.6 + sp * 167.2;
      a.frequency.setTargetAtTime(f, t, 0.06);
      b.frequency.setTargetAtTime(f * 1.01, t, 0.06);
      bp.frequency.setTargetAtTime(940 + sp * 1140, t, 0.09);
    };
  }

  var items = [];
  function add(game, set, key, name, price, desc, painter, audio) {
    var it = { id: (game || 'site') + '.' + set + '.' + key, game: game, set: (game || 'site') + '.' + set, name: name, desc: desc, price: price, painter: painter };
    if (audio) it.audio = audio;
    items.push(it);
  }

  // ---- Site-wide — cursor skins (desktop only; the kit applies the selected one) ----
  add('', 'cursor', 'classic',   'Classic',       0,   'The trusty arrow, as nature intended.', cursorSwatch(CURSORS.classic));
  add('', 'cursor', 'crosshair', 'Crosshair',     25,  'Pinpoint green crosshair — everything is a target.', cursorSwatch(CURSORS.crosshair));
  add('', 'cursor', 'paw',       'Paw',           25,  'A soft pink paw pads across your screen.', cursorSwatch(CURSORS.paw, -0.7854));
  add('', 'cursor', 'sword',     'Pixel Sword',   50,  'Point with a tiny hero’s blade.', cursorSwatch(CURSORS.sword));
  add('', 'cursor', 'comet',     'Neon Comet',    50,  'A glowing comet head with a neon tail.', cursorSwatch(CURSORS.comet, -1.5708));
  add('', 'cursor', 'rainbow',   'Rainbow Trail', 100, 'Leaves a shimmering rainbow wake as you move.', cursorSwatch(CURSORS.rainbow, -1.5708));
  add('', 'cursor', 'terminal',  'Terminal',      75,  'A blinking block cursor, like an old terminal prompt. Glows in your CRT colour when CRT mode is on.', cursorSwatch(CURSORS.terminal));
  add('', 'cursor', 'sonar',       'Sonar Ping',      25, 'Minesweeper’s cyan sonar ring — sweep as you point.', cursorSwatch(CURSORS.sonar));
  add('', 'cursor', 'reticle',     'Marksman Scope',  50, 'Range’s amber mil-dot reticle for steady aim.', cursorSwatch(CURSORS.reticle));
  add('', 'cursor', 'starfighter', 'Lock-On', 50, 'Asteroids+ targeting brackets, locked on.', cursorSwatch(CURSORS.starfighter));

  // ---- Site-wide — CRT display mode (ONE unlock; on/off + colour are free preferences after that) ----
  add('', 'fx', 'off', 'Standard', 0, 'No screen filter — the plain look.', function (g, w, h) {
    g.fillStyle = '#0e1420'; g.fillRect(0, 0, w, h);
    g.strokeStyle = 'rgba(255,255,255,0.28)'; g.lineWidth = 1.5; g.strokeRect(w * 0.22, h * 0.3, w * 0.56, h * 0.4);
  });
  add('', 'fx', 'crt', 'CRT Mode', 500, 'Retro phosphor glow over the whole site — tap ▾ to pick a colour.', function (g, w, h) {
    g.fillStyle = '#00160a'; g.fillRect(0, 0, w, h);
    g.fillStyle = '#39ff9b'; for (var y = 2; y < h; y += 3) { g.globalAlpha = 0.85; g.fillRect(3, y, w - 6, 1); }
    g.globalAlpha = 1;
  });

  // ---- 🐍 Neon Snake — food skins ----
  add('snake', 'food', 'apple',   'Apple',        0,   'The classic neon pip.', orb('#f0f', '#f0f'));
  add('snake', 'food', 'cherry',  'Cherry',       10,  'A juicy pair on one stem.', cherry);
  add('snake', 'food', 'golden',  'Golden Apple', 25,  'Neon Snake’s food gleams gold.', orb('#ffd166', '#ffb03a'));
  add('snake', 'food', 'gem',     'Gem',          25,  'A cut jewel worth chasing.', gem);
  add('snake', 'food', 'star',    'Star Fruit',   50,  'A five-pointed snack from space.', star('#ffe066', '#ffd166'));
  add('snake', 'food', 'rainbow', 'Rainbow Orb',  100, 'Cycles through every neon colour.', rainbowOrb);

  // ---- 🐍 Neon Snake — music tracks (preview + unlock in the shop; `music` = kit track id) ----
  items.push({ id: 'snake.track.remaster', game: 'snake', set: 'snake.track', name: 'Neon', desc: 'The classic neon track — punchy drums, sub bass and stereo.', price: 0, painter: wavePainter('#7fffb0'), music: 'snake', engine: 2 });
  items.push({ id: 'snake.track.banger', game: 'snake', set: 'snake.track', name: 'Neon Banger', desc: 'A pumped-up electro remix — rolling bassline and a catchy square-wave hook.', price: 100, painter: wavePainter('#39ff14'), music: 'snakebanger', engine: 2 });


  // ---- 🎵 soundtracks: every game can run the ORIGINAL (v2) or the REBUILT (v3) engine ----
  // One entry per track so a player can keep the original in one game and the rebuilt one in
  // another without re-picking every session. `engine` tells the kit which renderer to use;
  // `track` overrides the id a game asks for (Neon Snake's two tunes).
  // Snake already shipped two paid track items — ids and prices kept, engine tagged on.
  items.push({ id: 'asteroids.track.classic', game: 'asteroids', set: 'asteroids.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'asteroids', engine: 2 });
  items.push({ id: 'asteroids.track.rebuilt', game: 'asteroids', set: 'asteroids.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'asteroids', engine: 3 });
  items.push({ id: 'asteroids-plus.track.classic', game: 'asteroids-plus', set: 'asteroids-plus.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'asteroidsplus', engine: 2 });
  items.push({ id: 'asteroids-plus.track.rebuilt', game: 'asteroids-plus', set: 'asteroids-plus.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'asteroidsplus', engine: 3 });
  items.push({ id: 'forcefield.track.classic', game: 'forcefield', set: 'forcefield.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'forcefield', engine: 2 });
  items.push({ id: 'forcefield.track.rebuilt', game: 'forcefield', set: 'forcefield.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'forcefield', engine: 3 });
  items.push({ id: 'aim-trainer.track.classic', game: 'aim-trainer', set: 'aim-trainer.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'range', engine: 2 });
  items.push({ id: 'aim-trainer.track.rebuilt', game: 'aim-trainer', set: 'aim-trainer.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'range', engine: 3 });
  items.push({ id: 'breakout.track.classic', game: 'breakout', set: 'breakout.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'breakout', engine: 2 });
  items.push({ id: 'breakout.track.rebuilt', game: 'breakout', set: 'breakout.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'breakout', engine: 3 });
  items.push({ id: 'bubbles.track.classic', game: 'bubbles', set: 'bubbles.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'bubbles', engine: 2 });
  items.push({ id: 'bubbles.track.rebuilt', game: 'bubbles', set: 'bubbles.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'bubbles', engine: 3 });
  items.push({ id: 'frog-bonk.track.classic', game: 'frog-bonk', set: 'frog-bonk.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'frogbonk', engine: 2 });
  items.push({ id: 'frog-bonk.track.rebuilt', game: 'frog-bonk', set: 'frog-bonk.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'frogbonk', engine: 3 });
  items.push({ id: 'tower-defense.track.classic', game: 'tower-defense', set: 'tower-defense.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'keep', engine: 2 });
  items.push({ id: 'tower-defense.track.rebuilt', game: 'tower-defense', set: 'tower-defense.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'keep', engine: 3 });
  items.push({ id: 'stacker.track.classic', game: 'stacker', set: 'stacker.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'stacker', engine: 2 });
  items.push({ id: 'stacker.track.rebuilt', game: 'stacker', set: 'stacker.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'stacker', engine: 3 });
  items.push({ id: 'sudoku.track.classic', game: 'sudoku', set: 'sudoku.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'sudoku', engine: 2 });
  items.push({ id: 'sudoku.track.rebuilt', game: 'sudoku', set: 'sudoku.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'sudoku', engine: 3 });
  items.push({ id: 'minesweeper.track.classic', game: 'minesweeper', set: 'minesweeper.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'minesweeper', engine: 2 });
  items.push({ id: 'minesweeper.track.rebuilt', game: 'minesweeper', set: 'minesweeper.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'minesweeper', engine: 3 });
  items.push({ id: '2048.track.classic', game: '2048', set: '2048.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: '2048', engine: 2 });
  items.push({ id: '2048.track.rebuilt', game: '2048', set: '2048.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: '2048', engine: 3 });
  items.push({ id: 'trap-the-cat.track.classic', game: 'trap-the-cat', set: 'trap-the-cat.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'trapthecat', engine: 2 });
  items.push({ id: 'trap-the-cat.track.rebuilt', game: 'trap-the-cat', set: 'trap-the-cat.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'trapthecat', engine: 3 });
  items.push({ id: 'glow-says.track.classic', game: 'glow-says', set: 'glow-says.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'glowsays', engine: 2 });
  items.push({ id: 'glow-says.track.rebuilt', game: 'glow-says', set: 'glow-says.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'glowsays', engine: 3 });
  items.push({ id: 'balloon-pop.track.classic', game: 'balloon-pop', set: 'balloon-pop.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'balloonpop', engine: 2 });
  items.push({ id: 'balloon-pop.track.rebuilt', game: 'balloon-pop', set: 'balloon-pop.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'balloonpop', engine: 3 });
  items.push({ id: 'critter-match.track.classic', game: 'critter-match', set: 'critter-match.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'crittermatch', engine: 2 });
  items.push({ id: 'critter-match.track.rebuilt', game: 'critter-match', set: 'critter-match.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'crittermatch', engine: 3 });
  items.push({ id: 'floodgate.track.classic', game: 'floodgate', set: 'floodgate.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'floodgate', engine: 2 });
  items.push({ id: 'floodgate.track.rebuilt', game: 'floodgate', set: 'floodgate.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'floodgate', engine: 3 });
  items.push({ id: 'mirror-maze.track.classic', game: 'mirror-maze', set: 'mirror-maze.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'mirrormaze', engine: 2 });
  items.push({ id: 'mirror-maze.track.rebuilt', game: 'mirror-maze', set: 'mirror-maze.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'mirrormaze', engine: 3 });
  items.push({ id: 'type-siege.track.classic', game: 'type-siege', set: 'type-siege.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'typesiege', engine: 2 });
  items.push({ id: 'type-siege.track.rebuilt', game: 'type-siege', set: 'type-siege.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'typesiege', engine: 3 });
  items.push({ id: 'dusk-runner.track.classic', game: 'dusk-runner', set: 'dusk-runner.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'duskrunner', engine: 2 });
  items.push({ id: 'dusk-runner.track.rebuilt', game: 'dusk-runner', set: 'dusk-runner.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'duskrunner', engine: 3 });
  items.push({ id: 'tube-racer.track.classic', game: 'tube-racer', set: 'tube-racer.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'tuberacer', engine: 2 });
  items.push({ id: 'tube-racer.track.rebuilt', game: 'tube-racer', set: 'tube-racer.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'tuberacer', engine: 3 });
  items.push({ id: 'flappy.track.classic', game: 'flappy', set: 'flappy.track', name: 'Classic', desc: 'The original soundtrack — a short, dry loop that just plays.', price: 0, painter: wavePainter('#8b93a7'), music: 'meadow', engine: 2 });
  items.push({ id: 'flappy.track.rebuilt', game: 'flappy', set: 'flappy.track', name: 'Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'meadow', engine: 3 });
  items.push({ id: 'snake.track.rebuilt', game: 'snake', set: 'snake.track', name: 'Neon · Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#a78bfa'), music: 'snake', engine: 3 });
  items.push({ id: 'snake.track.rebuiltbanger', game: 'snake', set: 'snake.track', name: 'Neon Banger · Rebuilt', desc: 'The rebuilt soundtrack — fuller mix, a written tune, and it builds and drops with your run.', price: 100, painter: wavePainter('#c4b5fd'), music: 'snakebanger', engine: 3 });

  // ---- 🧱 Brick Breaker — paddle + ball skins ----
  add('breakout', 'paddle', 'synthwave', 'Synthwave',    0,   'The original cyan glow.', paddle(['#00ffff', '#0088aa'], '#00ffff'));
  add('breakout', 'paddle', 'wood',      'Wood Classic', 10,  'Warm varnished wood, arcade-cabinet style.', paddle(['#d09a5c', '#8a5a30']));
  add('breakout', 'paddle', 'ice',       'Neon Ice',     25,  'Frosted glass with a cold blue shine.', paddle(['#e8fbff', '#9fd8f0'], '#bfeaff'));
  add('breakout', 'paddle', 'lava',      'Lava Core',    50,  'Molten rock, glowing from within.', paddle(['#ffd166', '#ff5a2a'], '#ff7a3c'));
  add('breakout', 'paddle', 'chrome',    'Chrome',       100, 'Mirror-polished and untouchable.', paddle(['#f4f8ff', '#8a97ad', '#e8eef8'], '#cfd8e8'));
  add('breakout', 'ball', 'neon',   'Neon',   0,   'The classic cyan streak.', ball('#00ffff', '#00e6ff', '#eaffff'));
  add('breakout', 'ball', 'eight',  '8-Ball', 25,  'Rack ’em up. Break ’em down.', eightBall);
  add('breakout', 'ball', 'comet',  'Comet',  50,  'A fireball with a blazing tail.', ball('#ffb03a', '#ff7a3c', '#fff2d0'));
  add('breakout', 'ball', 'disco',  'Disco',  50,  'Shifts colour with every bounce.', ball('#ff7ab6', '#b98cff', '#fff'));
  add('breakout', 'ball', 'plasma', 'Plasma', 100, 'Crackling violet energy.', ball('#b98cff', '#7a4dff', '#f0e8ff'));

  // ---- 🏰 Keep Defender — castle skins ----
  add('tower-defense', 'castle', 'stone',    'Stone Keep',        0,   'The keep as the masons built it.', castle('#78788a', '#3a2a17'));
  add('tower-defense', 'castle', 'rose',     'Rose Keep',         10,  'Pink granite, quarried for a princess.', castle('#c88f9c', '#6a3a48'));
  add('tower-defense', 'castle', 'oak',      'Oak Fort',          25,  'Timber walls, frontier spirit.', castle('#a87848', '#6a4a28'));
  add('tower-defense', 'castle', 'sand',     'Sandcastle',        50,  'Bucket-built, surprisingly sturdy.', castle('#e8cc8a', '#c8a45c', '#2a4a66'));
  add('tower-defense', 'castle', 'ice',      'Ice Keep',          50,  'Carved from a glacier, cold to the core.', castle('#cfeaff', '#8ac0e8', '#0e2233'));
  add('tower-defense', 'castle', 'obsidian', 'Obsidian Citadel',  200, 'Black glass walls wreathed in nether-fire.', castle('#22242e', '#0e0f16', '#1a1230'));

  // ---- 🫧 Bubble Pop — pop effects + shooter bases ----
  add('bubbles', 'pop', 'classic',   'Classic',   0,   'A clean, satisfying pop.', popDots('#9fe8ff'));
  add('bubbles', 'pop', 'confetti',  'Confetti',  25,  'Every pop throws a tiny party.', burst(['#ff7ab6', '#ffd166', '#7fe0a0', '#7fd0ff']));
  add('bubbles', 'pop', 'stars',     'Stars',     50,  'Bubbles burst into twinkling stars.', star('#ffe066', '#ffd166', '#0a1420'));
  add('bubbles', 'pop', 'fireworks', 'Fireworks', 100, 'Full skyrocket finale on every match.', burst(['#ff5b5b', '#ffd166', '#fff', '#7fd0ff']));
  add('bubbles', 'base', 'aqua',  'Aqua',       0,  'The standard-issue aqua launcher.', grad(['#2ee8c8', '#1a8a9a'], 1));
  add('bubbles', 'base', 'candy', 'Candy',      25, 'Sugar-striped and sweet.', grad(['#ff9ad1', '#fff', '#ff9ad1'], 0));
  add('bubbles', 'base', 'gold',  'Royal Gold', 50, 'A launcher fit for bubble royalty.', grad(['#ffe08a', '#c89a2a'], 1));

  // ---- 🎯 Range — target skins + hit markers ----
  add('aim-trainer', 'target', 'rings',    'Rings',      0,   'Regulation rings, no excuses.', ringsTarget);
  add('aim-trainer', 'target', 'donut',    'Neon Donut', 25,  'A glazed ring of pure neon.', target(['#ff7ab6', '#2a1420', '#ff7ab6']));
  add('aim-trainer', 'target', 'fruit',    'Fruit',      50,  'Watermelon slices. Juicy hits.', target(['#7fe05a', '#fff', '#ff5b6b']));
  add('aim-trainer', 'target', 'alien',    'Alien Blob', 50,  'It wobbles. It stares. Shoot it.', target(['#7fe07a', '#3aa858', '#0e2a14']));
  add('aim-trainer', 'target', 'goldstar', 'Gold Star',  100, 'Every hit feels like a gold medal.', star('#ffd166', '#ffb03a', '#141c28'));
  add('aim-trainer', 'marker', 'classic', 'Classic', 0,  'A crisp white hitmarker.', markerX);
  add('aim-trainer', 'marker', 'spark',   'Spark',   25, 'Hits throw electric sparks.', burst(['#ffe066', '#fff'], '#101820'));
  add('aim-trainer', 'marker', 'boom',    'Boom',    50, 'Tiny comic-book explosions.', burst(['#ff7a3c', '#ffd166', '#ff4b4b'], '#101820'));

  // ---- 🧊 Stack — block palettes ----
  add('stacker', 'palette', 'pastel',    'Pastel',      0,   'Soft sunset pastels.', palette(['#ff9aa2', '#ffd3b6', '#c7ceea', '#b5ead7']));
  add('stacker', 'palette', 'synthwave', 'Synthwave',   25,  'Neon pinks over midnight blue.', palette(['#ff5cc8', '#b94dff', '#4d5bff', '#00e6ff']));
  add('stacker', 'palette', 'forest',    'Forest',      25,  'Mossy greens and warm bark.', palette(['#7fb069', '#5a8a4a', '#a87848', '#3a5a30']));
  add('stacker', 'palette', 'candy',     'Candy',       50,  'Bubblegum, mint and lemon drops.', palette(['#ff9ad1', '#7fe0d0', '#ffe08a', '#b0a0ff']));
  add('stacker', 'palette', 'gilded',    'Gilded Mono', 100, 'Black marble with gold veins.', palette(['#16181e', '#ffd166', '#22242e', '#e8b93a']));

  // ---- 🛸 Asteroids — ship colours (+ one whole-game retro tint) ----
  add('asteroids', 'ship', 'cyan',    'Cyan',      0,   'The classic wireframe.', ship('#9fe8ff'));
  add('asteroids', 'ship', 'emerald', 'Emerald',   10,  'Green-lit and ready.', ship('#5fe07a'));
  add('asteroids', 'ship', 'crimson', 'Crimson',   25,  'Red shift, full thrust.', ship('#ff5b5b'));
  add('asteroids', 'ship', 'violet',  'Violet',    25,  'Deep-space purple.', ship('#b98cff'));
  add('asteroids', 'ship', 'crt',     'CRT Green', 50,  'The whole game in green phosphor, like 1979.', ship('#3aff5a', '#041008'));
  add('asteroids', 'ship', 'gold',    'Gold',      100, 'A gilded hull for a legend.', ship('#ffd166'));

  // ---- ☄️ Asteroids+ — hull colours + engine trails ----
  add('asteroids-plus', 'hull', 'violet', 'Violet',    0,   'The roguelite’s signature hull.', ship('#b98cff'));
  add('asteroids-plus', 'hull', 'copper', 'Copper',    10,  'A workshop-fresh copper shine.', ship('#ff9d5c'));
  add('asteroids-plus', 'hull', 'teal',   'Teal',      25,  'Cool-headed under fire.', ship('#2ee8c8'));
  add('asteroids-plus', 'hull', 'blood',  'Blood Red', 50,  'For pilots who never brake.', ship('#e0304a'));
  add('asteroids-plus', 'hull', 'crt',    'CRT Green', 50,  'The whole run in green phosphor, like 1979.', ship('#3aff5a', '#041008'));
  add('asteroids-plus', 'hull', 'gold',   'Gold',      100, 'Won, not bought. Well… bought.', ship('#ffd166'));
  add('asteroids-plus', 'trail', 'ion',     'Ion',     0,   'A steady blue ion stream.', trail(['#4d9aff', '#9fe8ff']));
  add('asteroids-plus', 'trail', 'ember',   'Ember',   50,  'Sparks and embers in your wake.', trail(['#ff5a2a', '#ffd166']));
  add('asteroids-plus', 'trail', 'rainbow', 'Rainbow', 100, 'Full spectrum afterburn.', trail(['#ff5b5b', '#ffd166', '#7fe0a0', '#7fd0ff', '#b98cff']));

  // ---- 🐤 Meadow Flyer — birds (migrated from banked cash; progressive tail is the one
  //      approved band exception, Phoenix = the 🏆 500 aspirational ceiling) ----
  add('flappy', 'bird', 'bee',      'Bee',      0,   'Not technically a bird. Flies anyway.', beeBird);
  add('flappy', 'bird', 'robin',    'Robin',    10,  'A cheerful meadow regular.', bird('#a86a48', '#7a4e34'));
  add('flappy', 'bird', 'bluebird', 'Bluebird', 25,  'Swift, sleek and sky-coloured.', bird('#5fa8e8', '#3f86c8'));
  add('flappy', 'bird', 'parrot',   'Parrot',   50,  'Loud in colour and in spirit.', bird('#5fd07a', '#3aa858'));
  add('flappy', 'bird', 'owl',      'Owl',      75,  'Silent night hunter with slow, heavy flaps.', bird('#b08a5a', '#8a6a40'));
  add('flappy', 'bird', 'bielik',   'Bielik',   100, 'The white-tailed eagle, king of Polish skies.', bird('#a8875c', '#7a6038'));
  add('flappy', 'bird', 'rarog',    'Raróg', 150, 'A fiery falcon spirit from Slavic legend.', bird('#c8232e', '#8a1a22', 'rgba(255,120,40,0.7)'));
  add('flappy', 'bird', 'raven',    'Raven',    250, 'Midnight feathers, ancient secrets.', bird('#17181d', '#0c0d11', 'rgba(170,200,255,0.55)'));
  add('flappy', 'bird', 'phoenix',  'Phoenix',  500, 'The aspirational one. Rises from every game over.', bird('#ff8a3a', '#e85a1a', 'rgba(255,140,40,0.85)'));

  // ---- 🐸 Frog Bonk — hammer skins + meadow seasons ----
  add('frog-bonk', 'hammer', 'wood',    'Wooden',     0,   'The royal bonker, carved from oak.', hammerSwatch('#b0793f', '#7a4e28', 'block'));
  add('frog-bonk', 'hammer', 'fluffy',  'Fluffy',     25,  'A plush pink mallet. Maximum softness.', hammerSwatch('#ffb6d9', '#f08ab8', 'fluff'));
  add('frog-bonk', 'hammer', 'candy',   'Candy Cane', 50,  'Peppermint-striped and perfectly bonkable.', hammerSwatch('#ff5b6b', '#ff8a94', 'stripe'));
  add('frog-bonk', 'hammer', 'balloon', 'Inflatable', 50,  'Squeaky, bouncy, surprisingly effective.', hammerSwatch('#9fd8ff', '#4d9ae0', 'balloon'));
  add('frog-bonk', 'hammer', 'gold',    'Royal Gold', 100, 'Solid gold. The frogs feel honoured.', hammerSwatch('#ffe08a', '#c89a2a', 'gold'));
  add('frog-bonk', 'meadow', 'sunny',   'Sunny',      0,   'The meadow on its best day.', meadowSwatch(['#8fce5e', '#5da741', '#3f8a33'], 'sun'));
  add('frog-bonk', 'meadow', 'autumn',  'Autumn',     25,  'Golden grass and drifting leaves.', meadowSwatch(['#d8b25a', '#b08a3e', '#8a6a30'], 'leaf'));
  add('frog-bonk', 'meadow', 'rain',    'Rainy Day',  50,  'Soft rain over deep green grass.', meadowSwatch(['#6aa84f', '#457a38', '#2f5e2c'], 'rain'));
  add('frog-bonk', 'meadow', 'snow',    'Winter',     100, 'A quiet snowfall over the frozen meadow.', meadowSwatch(['#dfe8ea', '#b8ccd0', '#9ab4bc'], 'snow'));

  // ---- 🔢 Sudoku — board themes + numeral styles ----
  function sudokuBoard(card, line, given, entry) {
    return function (g, w, h) {
      g.fillStyle = card; g.fillRect(0, 0, w, h);
      g.strokeStyle = line; g.lineWidth = Math.max(1, w * 0.02);
      for (let k = 1; k < 3; k++) {
        g.beginPath(); g.moveTo((w / 3) * k, w * 0.08); g.lineTo((w / 3) * k, h - w * 0.08); g.stroke();
        g.beginPath(); g.moveTo(w * 0.08, (h / 3) * k); g.lineTo(w - w * 0.08, (h / 3) * k); g.stroke();
      }
      g.textAlign = 'center'; g.textBaseline = 'middle';
      g.font = '700 ' + (w * 0.24) + 'px Georgia, serif';
      g.fillStyle = given; g.fillText('5', w / 6, h / 6 + h * 0.02);
      g.fillStyle = entry; g.fillText('3', w / 2, h / 2 + h * 0.02);
      g.fillStyle = given; g.fillText('9', w * 5 / 6, h * 5 / 6 + h * 0.02);
    };
  }
  function sudokuDigits(family, weight) {
    // truthful preview: the classic set is numerals in the board's real ink/entry colors
    return function (g, w, h) {
      g.fillStyle = '#f2efe8'; g.fillRect(0, 0, w, h);
      g.strokeStyle = '#b9b3a4'; g.lineWidth = 1;
      g.beginPath(); g.moveTo(w / 2, h * 0.1); g.lineTo(w / 2, h * 0.9); g.stroke();
      g.textAlign = 'center'; g.textBaseline = 'middle';
      g.font = weight + ' ' + (w * 0.5) + 'px ' + family;
      g.fillStyle = '#23272e'; g.fillText('7', w * 0.27, h / 2 + h * 0.03);
      g.fillStyle = '#1d6fb8'; g.fillText('3', w * 0.73, h / 2 + h * 0.03);
    };
  }
  function sudokuAnimals(g, w, h) {
    g.fillStyle = '#f2efe8'; g.fillRect(0, 0, w, h);
    g.strokeStyle = '#b9b3a4'; g.lineWidth = 1;
    g.beginPath(); g.moveTo(w / 2, h * 0.1); g.lineTo(w / 2, h * 0.9); g.stroke();
    g.textAlign = 'center'; g.textBaseline = 'middle';
    g.font = (w * 0.42) + 'px system-ui, sans-serif';
    g.fillText('🦊', w * 0.27, h / 2 + h * 0.03);
    g.fillText('🐸', w * 0.73, h / 2 + h * 0.03);
  }
  function sudokuShapes(g, w, h) {
    g.fillStyle = '#f2efe8'; g.fillRect(0, 0, w, h);
    var r = w * 0.16;
    g.fillStyle = '#e74c3c'; g.beginPath(); g.arc(w * 0.26, h * 0.32, r, 0, Math.PI * 2); g.fill();
    g.fillStyle = '#3498db'; g.beginPath();
    for (var k = 0; k < 10; k++) { var rr = k % 2 ? r * 0.45 : r; var a = -Math.PI / 2 + k * Math.PI / 5; g[k ? 'lineTo' : 'moveTo'](w * 0.72 + rr * Math.cos(a), h * 0.36 + rr * Math.sin(a)); }
    g.closePath(); g.fill();
    g.fillStyle = '#2ecc71'; g.beginPath();
    g.moveTo(w * 0.5, h * 0.72 - r); g.lineTo(w * 0.5 + r, h * 0.72); g.lineTo(w * 0.5, h * 0.72 + r); g.lineTo(w * 0.5 - r, h * 0.72);
    g.closePath(); g.fill();
  }
  add('sudoku', 'board', 'paper',     'Paper',     0,   'Clean off-white paper and warm ink.', sudokuBoard('#f2efe8', '#3a3f47', '#2b2f36', '#1d6fb8'));
  add('sudoku', 'board', 'mint',      'Mint',      10,  'Cool mint paper for fresh mornings.', sudokuBoard('#e6f2ec', '#9ab8aa', '#2c4a3c', '#1a8a5e'));
  add('sudoku', 'board', 'slate',     'Slate',     25,  'A dark board for late-night solving.', sudokuBoard('#2b323c', '#97a3b4', '#e8e2d4', '#6ad6ff'));
  add('sudoku', 'board', 'blueprint', 'Blueprint', 50,  'Cyan grid lines on drafting blue.', sudokuBoard('#0e2a42', '#7fd8ff', '#eaf6ff', '#ffd24d'));
  add('sudoku', 'board', 'pastel',    'Pastel',    100, 'Soft rose paper and plum ink.', sudokuBoard('#fdf2f8', '#b07aa0', '#5a4a58', '#7a5fd0'));
  add('sudoku', 'digits', 'ink',     'Ink Serif', 0,  'Classic newspaper numerals.', sudokuDigits('Georgia, serif', 700));
  add('sudoku', 'digits', 'animals', 'Animals',   50, 'Nine little critters instead of numbers.', sudokuAnimals);
  add('sudoku', 'digits', 'shapes',  'Shapes',    50, 'Nine colorful shapes instead of numbers.', sudokuShapes);

  // ---- 💣 Minesweeper — board themes (mini 2×2 tile swatch: raised tiles + one open number) ----
  function msBoard(tile, hi, open, numC) {
    return function (g, w, h) {
      const s = Math.floor(Math.min(w, h) / 2);
      const ox = (w - s * 2) / 2, oy = (h - s * 2) / 2;
      for (let k = 0; k < 4; k++) {
        const x = ox + (k % 2) * s, y = oy + Math.floor(k / 2) * s;
        if (k === 3) {
          g.fillStyle = open; g.fillRect(x, y, s - 1, s - 1);
          g.fillStyle = numC; g.font = '700 ' + Math.round(s * 0.6) + 'px system-ui';
          g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText('1', x + s / 2, y + s / 2);
        } else {
          g.fillStyle = tile; g.fillRect(x, y, s - 1, s - 1);
          g.fillStyle = hi; g.fillRect(x, y, s - 1, 2); g.fillRect(x, y, 2, s - 1);
        }
      }
    };
  }
  add('minesweeper', 'board', 'sonar',  'Sonar',  0,   'Deep-sea charts and glowing cyan pings.', msBoard('#0d2a40', '#1e4a68', '#061420', '#4fd8ff'));
  add('minesweeper', 'board', 'abyss',  'Abyss',  10,  'Lightless trench charts, violet pings.', msBoard('#1a1430', '#2e2650', '#0c0918', '#b48aff'));
  add('minesweeper', 'board', 'coral',  'Coral',  25,  'A sunlit reef survey in warm sand.', msBoard('#c8a878', '#e2c898', '#7a5a3a', '#1a8a8a'));
  add('minesweeper', 'board', 'retro',  'Retro',  50,  'The beige office classic, pixel bevels and all.', msBoard('#c0c7cf', '#eef2f6', '#b3bac2', '#1c46c8'));
  add('minesweeper', 'board', 'meadow', 'Meadow', 100, 'Dig a sunny field — daisies mark the moles.', msBoard('#3f8a3c', '#63b45a', '#8a6b42', '#ffe066'));

  // ---- 🧮 2048 — tile themes (mini 2×2 swatch of graded value tiles) ----
  function tiles2048(cols) {
    return function (g, w, h) {
      const s = Math.floor(Math.min(w, h) / 2), ox = (w - s * 2) / 2, oy = (h - s * 2) / 2;
      const vals = ['2', '8', '64', '512'];
      for (let k = 0; k < 4; k++) {
        const x = ox + (k % 2) * s, y = oy + Math.floor(k / 2) * s;
        const grd = g.createLinearGradient(x, y, x, y + s);
        grd.addColorStop(0, cols[k][0]); grd.addColorStop(1, cols[k][1]);
        g.fillStyle = grd; g.fillRect(x + 1, y + 1, s - 2, s - 2);
        g.fillStyle = cols[k][2]; g.font = '800 ' + Math.round(s * 0.42) + 'px system-ui';
        g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText(vals[k], x + s / 2, y + s / 2);
      }
    };
  }
  add('2048', 'tiles', 'honey', 'Honey', 0,  'Warm cream to amber — the cozy classic.',
    tiles2048([['#efe4d2', '#e2d3ba', '#5a4632'], ['#f6b568', '#ee9d4d', '#fff7ec'], ['#f7623c', '#ea4423', '#fff7ec'], ['#f2c73f', '#e8ab18', '#fff7ec']]));
  add('2048', 'tiles', 'mint', 'Mint', 10, 'Cool teal-to-lime freshness.',
    tiles2048([['hsl(165,60%,72%)', 'hsl(165,60%,55%)', '#0e2a20'], ['hsl(150,65%,55%)', 'hsl(150,70%,42%)', '#eafff4'], ['hsl(120,65%,50%)', 'hsl(120,70%,38%)', '#eafff4'], ['hsl(90,70%,48%)', 'hsl(90,75%,36%)', '#eafff4']]));
  add('2048', 'tiles', 'rose', 'Rose', 25, 'Blush pinks warming into gold.',
    tiles2048([['hsl(335,70%,80%)', 'hsl(335,60%,66%)', '#4a1a2e'], ['hsl(345,75%,62%)', 'hsl(345,75%,50%)', '#fff0f5'], ['hsl(15,80%,58%)', 'hsl(15,85%,46%)', '#fff0f5'], ['hsl(40,85%,55%)', 'hsl(40,90%,44%)', '#fff8ea']]));
  add('2048', 'tiles', 'neon',  'Neon',  50, 'Glassy glow tiles on midnight blue.',
    tiles2048([['hsl(200,85%,62%)', 'hsl(200,90%,45%)', '#eafcff'], ['hsl(260,85%,62%)', 'hsl(260,90%,45%)', '#eafcff'], ['hsl(345,85%,62%)', 'hsl(345,90%,45%)', '#eafcff'], ['hsl(65,85%,62%)', 'hsl(65,90%,45%)', '#eafcff']]));
  add('2048', 'tiles', 'kraft', 'Kraft', 100, 'Stamped cardboard and moss-green inks.',
    tiles2048([['#e8dcc4', '#d9c9a8', '#4a3a26'], ['#c9a06a', '#b98a50', '#fff8ea'], ['#a05a2e', '#87461e', '#fff8ea'], ['#6b9439', '#527a22', '#fff8ea']]));

  // ---- 🐱 Trap the Cat — cat colours (simple cat-face swatch) ----
  function catFace(body, belly, nose, glow) {
    return function (g, w, h) {
      const s = Math.min(w, h) * 0.4, cx = w / 2, cy = h / 2 + s * 0.08;
      if (glow) { g.shadowColor = glow; g.shadowBlur = s * 0.5; }
      g.fillStyle = body;
      g.beginPath(); g.moveTo(cx - s * 0.9, cy - s * 0.4); g.lineTo(cx - s * 0.65, cy - s * 1.25); g.lineTo(cx - s * 0.2, cy - s * 0.7); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(cx + s * 0.9, cy - s * 0.4); g.lineTo(cx + s * 0.65, cy - s * 1.25); g.lineTo(cx + s * 0.2, cy - s * 0.7); g.closePath(); g.fill();
      const grd = g.createRadialGradient(cx - s * 0.3, cy - s * 0.4, s * 0.2, cx, cy, s * 1.2);
      grd.addColorStop(0, belly); grd.addColorStop(1, body);
      g.fillStyle = grd; g.beginPath(); g.arc(cx, cy, s, 0, 7); g.fill();
      g.fillStyle = '#1a141c';
      g.beginPath(); g.arc(cx - s * 0.35, cy - s * 0.12, s * 0.2, 0, 7); g.arc(cx + s * 0.35, cy - s * 0.12, s * 0.2, 0, 7); g.fill();
      g.fillStyle = '#fff';
      g.beginPath(); g.arc(cx - s * 0.42, cy - s * 0.2, s * 0.07, 0, 7); g.arc(cx + s * 0.28, cy - s * 0.2, s * 0.07, 0, 7); g.fill();
      g.fillStyle = nose; g.beginPath(); g.arc(cx, cy + s * 0.18, s * 0.11, 0, 7); g.fill();
    };
  }
  add('trap-the-cat', 'cat', 'noir',   'Noir',   0,   'The classic midnight escape artist.', catFace('#2b2530', '#3a3342', '#f0a6c8'));
  add('trap-the-cat', 'cat', 'smoke',  'Smoke',  10,  'A soft grey shadow of a cat.',      catFace('#8a8a96', '#a8a8b4', '#f0a6c8'));
  add('trap-the-cat', 'cat', 'caramel', 'Caramel', 25, 'Sweet-toothed and sun-warmed.',     catFace('#c89a5a', '#e8c088', '#e06a6a'));
  add('trap-the-cat', 'cat', 'ginger', 'Ginger', 50,  'A marmalade menace on the run.',      catFace('#e08b3d', '#f2b571', '#e06a6a'));
  add('trap-the-cat', 'cat', 'snow',   'Snow',   100, 'Fluffy, elegant, and slippery.',       catFace('#e8e4ea', '#f7f4f8', '#f08aae'));
  add('trap-the-cat', 'cat', 'ember',  'Ember Cat', 250, 'Wreathed in living flame — sparks follow every hop.', catFace('#3a1408', '#c8451a', '#ffd166', '#ff7a2a'));
  add('trap-the-cat', 'cat', 'void',   'Void Cat',  500, 'A hole in the night, trailing violet wisps.',         catFace('#0c0a14', '#241c38', '#b48aff', '#8a5cff'));

  // ---- 🏮 Glow Says — pad shapes ----
  function glowPad(shape) {
    return function (g, w, h) {
      const cx = w / 2, cy = h / 2, rr = Math.min(w, h) * 0.32;
      g.fillStyle = '#0a2a20'; g.fillRect(0, 0, w, h);
      g.shadowColor = '#7ee787'; g.shadowBlur = rr * 0.6;
      const grd = g.createRadialGradient(cx - rr * 0.2, cy - rr * 0.2, rr * 0.1, cx, cy, rr * 1.1);
      grd.addColorStop(0, '#d8ffdf'); grd.addColorStop(1, '#3fae5a');
      g.fillStyle = grd;
      if (shape === 'star') {
        g.beginPath();
        for (let k = 0; k < 10; k++) {
          const a = -Math.PI / 2 + k * Math.PI / 5, rad = k % 2 ? rr * 0.42 : rr;
          const x = cx + Math.cos(a) * rad, y = cy + Math.sin(a) * rad;
          k ? g.lineTo(x, y) : g.moveTo(x, y);
        }
        g.closePath();
      } else if (shape === 'heart') {
        const w2 = rr * 2, h2 = rr * 2, x0 = cx - rr, y0 = cy - rr;
        g.beginPath();
        g.moveTo(x0 + w2 / 2, y0 + h2 * 0.32);
        g.bezierCurveTo(x0 + w2 / 2, y0 + h2 * 0.24, x0 + w2 * 0.42, y0 + h2 * 0.06, x0 + w2 * 0.26, y0 + h2 * 0.06);
        g.bezierCurveTo(x0 + w2 * 0.02, y0 + h2 * 0.06, x0, y0 + h2 * 0.35, x0, y0 + h2 * 0.35);
        g.bezierCurveTo(x0, y0 + h2 * 0.55, x0 + w2 * 0.2, y0 + h2 * 0.76, x0 + w2 / 2, y0 + h2 * 0.94);
        g.bezierCurveTo(x0 + w2 * 0.8, y0 + h2 * 0.76, x0 + w2, y0 + h2 * 0.55, x0 + w2, y0 + h2 * 0.35);
        g.bezierCurveTo(x0 + w2, y0 + h2 * 0.35, x0 + w2 * 0.98, y0 + h2 * 0.06, x0 + w2 * 0.74, y0 + h2 * 0.06);
        g.bezierCurveTo(x0 + w2 * 0.58, y0 + h2 * 0.06, x0 + w2 / 2, y0 + h2 * 0.24, x0 + w2 / 2, y0 + h2 * 0.32);
        g.closePath();
      } else if (shape === 'diamond') {
        g.beginPath();
        g.moveTo(cx, cy - rr); g.lineTo(cx + rr * 0.72, cy); g.lineTo(cx, cy + rr); g.lineTo(cx - rr * 0.72, cy);
        g.closePath();
      } else if (shape === 'moon') {
        g.beginPath();
        g.arc(cx, cy, rr, 0.829, -0.829, false);
        g.arc(cx + rr * 0.42, cy, rr * 0.78, -1.237, 1.237, true);
        g.closePath();
      } else {
        g.beginPath(); g.arc(cx, cy, rr, 0, Math.PI * 2);
      }
      g.fill(); g.shadowBlur = 0;
    };
  }
  add('glow-says', 'pads', 'orbs',   'Lantern Orbs', 0,   'Round glowing lanterns.',      glowPad('orb'));
  add('glow-says', 'pads', 'diamonds', 'Diamonds', 10, 'Cut-gem lanterns.',            glowPad('diamond'));
  add('glow-says', 'pads', 'moons',    'Moons',     25, 'Sleepy crescent lanterns.',    glowPad('moon'));
  add('glow-says', 'pads', 'stars',  'Stars',        50,  'Twinkly star lanterns.',       glowPad('star'));
  add('glow-says', 'pads', 'hearts', 'Hearts',       100, 'Warm heart-shaped lanterns.',  glowPad('heart'));

  // ---- 🎈 Balloon Pop — balloon styles ----
  function balloonSwatch(kind) {
    return function (g, w, h) {
      const bg = g.createLinearGradient(0, 0, 0, h);
      bg.addColorStop(0, '#9adcff'); bg.addColorStop(1, '#e2f6e8');
      g.fillStyle = bg; g.fillRect(0, 0, w, h);
      const cx = w / 2, cy = h / 2, r = Math.min(w, h) * 0.3;
      const grd = g.createRadialGradient(cx - r * 0.3, cy - r * 0.4, r * 0.1, cx, cy, r * 1.1);
      grd.addColorStop(0, '#fff'); grd.addColorStop(0.3, '#ff6b8a'); grd.addColorStop(1, '#c23a5a');
      g.fillStyle = grd;
      g.beginPath(); g.ellipse(cx, cy, r * 0.92, r, 0, 0, Math.PI * 2); g.fill();
      if (kind === 'critter') {
        g.fillStyle = '#c23a5a';
        g.beginPath(); g.ellipse(cx - r * 0.55, cy - r * 0.8, r * 0.26, r * 0.34, -0.4, 0, 7); g.fill();
        g.beginPath(); g.ellipse(cx + r * 0.55, cy - r * 0.8, r * 0.26, r * 0.34, 0.4, 0, 7); g.fill();
        g.fillStyle = '#2b2030';
        g.beginPath(); g.arc(cx - r * 0.28, cy - r * 0.1, r * 0.09, 0, 7); g.arc(cx + r * 0.28, cy - r * 0.1, r * 0.09, 0, 7); g.fill();
      } else if (kind === 'stripes' || kind === 'dots') {
        g.save();
        g.beginPath(); g.ellipse(cx, cy, r * 0.92, r, 0, 0, Math.PI * 2); g.clip();
        g.fillStyle = 'rgba(255,255,255,0.55)';
        if (kind === 'stripes') {
          g.save(); g.translate(cx, cy); g.rotate(-0.5);
          for (let k = -2; k <= 2; k++) g.fillRect(-r * 1.5, k * r * 0.42 - r * 0.09, r * 3, r * 0.18);
          g.restore();
        } else {
          for (let k = 0; k < 7; k++) { g.beginPath(); g.arc(cx + Math.cos(k * 0.9) * r * 0.55, cy + Math.sin(k * 1.7) * r * 0.6, r * 0.12, 0, 7); g.fill(); }
        }
        g.restore();
      } else if (kind === 'planet') {
        g.strokeStyle = 'rgba(255,255,255,0.7)'; g.lineWidth = Math.max(2, r * 0.1);
        g.beginPath(); g.ellipse(cx, cy, r * 1.25, r * 0.32, -0.28, 0, Math.PI * 2); g.stroke();
      }
      g.fillStyle = 'rgba(255,255,255,0.5)';
      g.beginPath(); g.ellipse(cx - r * 0.3, cy - r * 0.4, r * 0.2, r * 0.12, -0.6, 0, 7); g.fill();
    };
  }
  add('balloon-pop', 'balloons', 'classic',  'Classic',  0,   'Bright party balloons.',            balloonSwatch('classic'));
  add('balloon-pop', 'balloons', 'stripes', 'Stripes', 10, 'Carnival-striped balloons.', balloonSwatch('stripes'));
  add('balloon-pop', 'balloons', 'dots',    'Dots',    25, 'Cheerful polka-dot balloons.', balloonSwatch('dots'));
  add('balloon-pop', 'balloons', 'critters', 'Critters', 50,  'Balloons with little animal ears.', balloonSwatch('critter'));
  add('balloon-pop', 'balloons', 'planets',  'Planets',  100, 'Ringed balloon planets in the sky.', balloonSwatch('planet'));

  // ---- 🐾 Critter Match — critter card sets ----
  function critterCard(emoji) {
    return function (g, w, h) {
      g.fillStyle = '#3a2415'; g.fillRect(0, 0, w, h);
      const cw2 = w * 0.52, ch2 = h * 0.68, px = (w - cw2) / 2, py = (h - ch2) / 2;
      g.fillStyle = '#fff8ec';
      g.beginPath();
      if (g.roundRect) g.roundRect(px, py, cw2, ch2, w * 0.08); else g.rect(px, py, cw2, ch2);
      g.fill();
      g.strokeStyle = '#d9b98a'; g.lineWidth = 2; g.stroke();
      g.font = Math.round(w * 0.34) + 'px system-ui';
      g.textAlign = 'center'; g.textBaseline = 'middle';
      g.fillText(emoji, w / 2, h / 2 + w * 0.02);
    };
  }
  add('critter-match', 'critters', 'farm',   'Farm Friends',  0,   'Cows, chicks and fluffy sheep.', critterCard('🐮'));
  add('critter-match', 'critters', 'woodland', 'Woodland', 10, 'Foxes, bears and forest friends.', critterCard('🦊'));
  add('critter-match', 'critters', 'birds',    'Birds',    25, 'Owls, ducks and flamingos.',        critterCard('🦉'));
  add('critter-match', 'critters', 'jungle', 'Jungle Crew',   50,  'Lions, monkeys and parrots.',    critterCard('🦁'));
  add('critter-match', 'critters', 'ocean',  'Ocean Pals',    100, 'Fish, octopuses and whales.',    critterCard('🐙'));

  // ---- 🦖 Chrome Runner — whole-look skins + the runner silhouette (shape, not colour) ----
  // A 46x40 swatch is too small for outline art — draw a chunky FILLED silhouette instead, so the
  // three runners are told apart at a glance (snout length, ears, square vs round).
  function runnerSwatch(kind) {
    return function (g, w, h) {
      g.fillStyle = '#f4f1e8'; g.fillRect(0, 0, w, h);
      const gy = h * 0.82, u = Math.min(w, h) / 40;   // unit
      g.strokeStyle = '#c3bcab'; g.lineWidth = Math.max(1, 1.2 * u);
      g.beginPath(); g.moveTo(w * 0.04, gy); g.lineTo(w * 0.96, gy); g.stroke();
      g.fillStyle = '#2b2b2b';
      const bw = 15 * u, bh = 11 * u, bx = w * 0.5 - bw * 0.62, by = gy - bh - 2.4 * u;
      const rr = (x, y, ww, hh, r) => { g.beginPath(); if (g.roundRect) g.roundRect(x, y, ww, hh, r); else g.rect(x, y, ww, hh); g.fill(); };
      rr(bx, by, bw, bh, kind === 'robot' ? 1.5 * u : 4 * u);                       // body
      if (kind !== 'robot') {                                                        // tail
        g.beginPath();
        g.moveTo(bx + 1 * u, by + bh * 0.35);
        g.quadraticCurveTo(bx - 9 * u, by + bh * 0.1, bx - 7 * u, by + bh * 0.95);
        g.quadraticCurveTo(bx - 4 * u, by + bh * 0.5, bx + 1 * u, by + bh * 0.75);
        g.fill();
      }
      const hw = (kind === 'dino' ? 12 : 9) * u, hh = 7.5 * u;
      const hx = bx + bw - 2 * u, hy = by - hh + 1.5 * u;
      rr(hx, hy, hw, hh, kind === 'robot' ? 1 * u : 3 * u);                          // head
      if (kind === 'fox') {                                                          // ears
        g.beginPath(); g.moveTo(hx + hw * 0.1, hy + 1 * u); g.lineTo(hx + hw * 0.22, hy - 4.2 * u);
        g.lineTo(hx + hw * 0.5, hy + 1 * u); g.closePath(); g.fill();
      }
      if (kind === 'robot') {                                                        // antenna
        g.fillRect(hx + hw * 0.45, hy - 4 * u, 1.4 * u, 4 * u);
        g.beginPath(); g.arc(hx + hw * 0.45 + 0.7 * u, hy - 4.6 * u, 1.5 * u, 0, 6.283); g.fill();
      }
      g.fillStyle = '#f4f1e8';                                                        // eye (knockout)
      g.beginPath(); g.arc(hx + hw * 0.72, hy + hh * 0.42, 1.5 * u, 0, 6.283); g.fill();
      g.fillStyle = '#2b2b2b';                                                        // legs
      g.fillRect(bx + bw * 0.22, by + bh, 1.8 * u, gy - (by + bh));
      g.fillRect(bx + bw * 0.62, by + bh, 1.8 * u, gy - (by + bh));
    };
  }
  // whole-game LOOK skins: the entire palette + draw mode, not just one entity
  function lookSwatch(kind) {
    return function (g, w, h) {
      const gy = h * 0.78, u = Math.min(w, h) / 40;
      if (kind === 'comic') {
        g.fillStyle = '#7ec8f0'; g.fillRect(0, 0, w, h);
        g.fillStyle = 'rgba(20,18,28,0.18)';                  // halftone
        for (let y = 2; y < gy; y += 5) for (let x = (y % 10 ? 2 : 4); x < w; x += 5) { g.beginPath(); g.arc(x, y, 0.9, 0, 6.283); g.fill(); }
        g.fillStyle = '#c9a26a'; g.fillRect(0, gy, w, h - gy);
        g.strokeStyle = '#16141c'; g.lineWidth = 2; g.strokeRect(1.5, 1.5, w - 3, h - 3);
      } else if (kind === 'chrome') {
        const sk = g.createLinearGradient(0, 0, 0, gy);
        sk.addColorStop(0, '#1a2432'); sk.addColorStop(1, '#3a4a5c');
        g.fillStyle = sk; g.fillRect(0, 0, w, h);
        const fl = g.createLinearGradient(0, gy, 0, h);
        fl.addColorStop(0, '#8d99a6'); fl.addColorStop(1, '#2b333d');
        g.fillStyle = fl; g.fillRect(0, gy, w, h - gy);
      } else {
        g.fillStyle = '#f4f1e8'; g.fillRect(0, 0, w, h);
        g.fillStyle = '#ded8c9'; g.fillRect(0, gy, w, h - gy);
      }
      // one runner-ish blob in the skin's own material, so the swatch shows the LOOK
      const bw = 15 * u, bh = 10 * u, bx = w * 0.5 - bw * 0.5, by = gy - bh - 3 * u;
      if (kind === 'comic') { g.fillStyle = '#ffd23f'; }
      else if (kind === 'chrome') {
        const mg = g.createLinearGradient(0, by, 0, by + bh);
        mg.addColorStop(0, '#ffffff'); mg.addColorStop(0.45, '#e8eef5');
        mg.addColorStop(0.72, '#5d6a78'); mg.addColorStop(1, '#c9d3dd');
        g.fillStyle = mg;
      } else g.fillStyle = '#2b2b2b';
      g.beginPath();
      if (g.roundRect) g.roundRect(bx, by, bw, bh, 3 * u); else g.rect(bx, by, bw, bh);
      g.fill();
      if (kind === 'chrome' || kind === 'comic') { g.strokeStyle = kind === 'comic' ? '#16141c' : '#0d141c'; g.lineWidth = Math.max(1, u * (kind === 'comic' ? 1.4 : 1)); g.stroke(); }
      g.fillStyle = kind === 'chrome' ? '#e8eef5' : kind === 'comic' ? '#16141c' : '#2b2b2b';
      g.fillRect(bx + bw * 0.2, by + bh, 1.6 * u, gy - (by + bh));
      g.fillRect(bx + bw * 0.66, by + bh, 1.6 * u, gy - (by + bh));
    };
  }
  add('dusk-runner', 'style', 'chrome', 'Chrome',      0,  'Polished chromium — the factory finish.', lookSwatch('chrome'));
  add('dusk-runner', 'style', 'paper',  'Paper Sketch', 25, 'Flat ink on warm drawing paper.',        lookSwatch('paper'));
  add('dusk-runner', 'style', 'comic',  'Comic Panel',  50, 'Halftone dots, heavy ink and a KABOOM when you clip.', lookSwatch('comic'));

  add('dusk-runner', 'runner', 'dino',  'Dino',  0,  'The little desert lizard.', runnerSwatch('dino'));
  add('dusk-runner', 'runner', 'fox',   'Fox',   25, 'Pointy ears, bushy tail.',  runnerSwatch('fox'));
  add('dusk-runner', 'runner', 'robot', 'Robot', 50, 'All boxes and right angles.', runnerSwatch('robot'));

  // ---- 🚰 Floodgate — pipe metals (DRY pipework + the drafting accent; the utility colours are
  //      rule-bearing and never re-skinned) ----
  function pipeSwatch(body, hi, lo, accent) {
    return function (g, w, h) {
      g.fillStyle = '#0b1b26'; g.fillRect(0, 0, w, h);
      g.strokeStyle = accent; g.globalAlpha = 0.35; g.lineWidth = 1;
      for (let x = w * 0.2; x < w; x += w * 0.2) { g.beginPath(); g.moveTo(x, 0); g.lineTo(x, h); g.stroke(); }
      g.globalAlpha = 1;
      const t = Math.max(5, h * 0.3), y = (h - t) / 2;
      const grd = g.createLinearGradient(0, y, 0, y + t);
      grd.addColorStop(0, hi); grd.addColorStop(0.45, body); grd.addColorStop(1, lo);
      g.fillStyle = grd; g.fillRect(w * 0.06, y, w * 0.88, t);
      g.fillStyle = lo;                                  // bolted collars
      g.fillRect(w * 0.24, y - 1.5, Math.max(2, w * 0.05), t + 3);
      g.fillRect(w * 0.66, y - 1.5, Math.max(2, w * 0.05), t + 3);
      g.globalAlpha = 0.8; g.fillStyle = '#ffffff';
      g.fillRect(w * 0.06, y + t * 0.16, w * 0.88, Math.max(1, t * 0.12));
      g.globalAlpha = 1;
    };
  }
  add('floodgate', 'pipe', 'copper', 'Copper',    0,  'Standard copper run, warm and workmanlike.', pipeSwatch('#b87333', '#e8b98a', '#7b4a1f', '#5fb8d9'));
  add('floodgate', 'pipe', 'brass',  'Brass',     25, 'Polished brass fittings for a heritage plant room.', pipeSwatch('#c89a2a', '#f0dc9a', '#8a6712', '#5fb8d9'));
  add('floodgate', 'pipe', 'neon',   'Blueprint', 50, 'Cold steel that glows like the drawing it came from.', pipeSwatch('#7fb6cf', '#dff2ff', '#3d6a80', '#2ee8c8'));

  // ---- ⌨️ Type Siege — ink & paper looks (the whole page, not one entity) ----
  function inkSwatch(paper, rule, ink, accent) {
    return function (g, w, h) {
      g.fillStyle = paper; g.fillRect(0, 0, w, h);
      g.strokeStyle = rule; g.lineWidth = 1;
      for (let y = h * 0.22; y < h; y += Math.max(4, h * 0.2)) { g.beginPath(); g.moveTo(0, y); g.lineTo(w, y); g.stroke(); }
      g.strokeStyle = accent; g.beginPath(); g.moveTo(w * 0.16, 0); g.lineTo(w * 0.16, h); g.stroke();
      // a doodle soldier: head, body, spear
      g.strokeStyle = ink; g.lineWidth = Math.max(1.4, w * 0.045); g.lineCap = 'round';
      const cx = w * 0.55, base = h * 0.78, bh = h * 0.34;
      g.beginPath(); g.arc(cx, base - bh - h * 0.08, Math.max(2, w * 0.07), 0, 6.283); g.stroke();
      g.beginPath(); g.moveTo(cx, base - bh); g.lineTo(cx, base - bh * 0.3); g.stroke();
      g.beginPath(); g.moveTo(cx - w * 0.09, base); g.lineTo(cx, base - bh * 0.3); g.lineTo(cx + w * 0.09, base); g.stroke();
      g.beginPath(); g.moveTo(cx + w * 0.14, base - bh * 1.25); g.lineTo(cx + w * 0.14, base - h * 0.02); g.stroke();
      g.lineCap = 'butt';
    };
  }
  add('type-siege', 'ink', 'pencil',    'Pencil',    0,   'Graphite on warm ruled paper — the default doodle.', inkSwatch('#f6f1e4', '#dfeaf4', '#1e2126', '#f0c7cc'));
  add('type-siege', 'ink', 'blueprint', 'Blueprint', 50,  'Chalk-white lines on dark navy grid paper: the siege as a battle plan.', inkSwatch('#0b2130', '#1c4761', '#e4f3ff', '#2f7093'));
  add('type-siege', 'ink', 'marker',    'Marker',    100, 'Thick black marker on kraft card, with rust-orange banners.', inkSwatch('#f0e3ca', '#dcc9a4', '#1f160f', '#b6431a'));

  // ---- 🔆 Mirror Maze — beam palettes (the whole spectrum the puzzle draws with) ----
  function beamSwatch(cols) {
    return function (g, w, h) {
      g.fillStyle = '#050b12'; g.fillRect(0, 0, w, h);
      const n = cols.length, pad = Math.max(2, w * 0.08), bw = (w - pad * 2) / n;
      for (let i = 0; i < n; i++) {
        const x = pad + i * bw;
        g.globalAlpha = 0.28; g.fillStyle = cols[i];
        g.fillRect(x, h * 0.16, bw - 1, h * 0.68);                 // glow
        g.globalAlpha = 1; g.fillStyle = cols[i];
        g.fillRect(x + bw * 0.28, h * 0.16, Math.max(1.4, bw * 0.34), h * 0.68);   // core
      }
      g.globalAlpha = 1;
      g.strokeStyle = 'rgba(255,255,255,0.22)'; g.lineWidth = 1;
      g.strokeRect(0.5, 0.5, w - 1, h - 1);
    };
  }
  add('mirror-maze', 'beam', 'neon',     'Neon',     0,   'The default arcade spectrum — hot pink, mint and ice blue.', beamSwatch(['#ff4d6d', '#4dffa1', '#4db8ff', '#ffe14d']));
  add('mirror-maze', 'beam', 'sunset',   'Sunset',   25,  'Warm desert light: ember red, lime and violet.',             beamSwatch(['#ff4a2b', '#9fe870', '#8a6bff', '#ffc247']));
  add('mirror-maze', 'beam', 'spectrum', 'Spectrum', 50,  'Pure saturated RGB, straight off the prism.',                beamSwatch(['#ff0040', '#00ff6a', '#2b7fff', '#ffe600']));
  add('mirror-maze', 'beam', 'aurora',   'Aurora',   100, 'Soft polar glow — pastel beams on a black sky.',             beamSwatch(['#ff6f91', '#6ef2a0', '#69a6ff', '#ffe066']));

  // ---- 🌐 Forcefield — bolt colours + planet skins ----
  add('forcefield', 'marker', 'default', 'Classic', 0,  'A clean golden bolt.', forcefieldMarker('#ffd36b'));
  add('forcefield', 'marker', 'magma',   'Magma',   25, 'A molten-orange bolt.', forcefieldMarker('#ff8a3d'));
  add('forcefield', 'marker', 'lime',    'Lime',    50, 'A zesty green bolt.', forcefieldMarker('#b6ff5c'));
  add('forcefield', 'planet', 'azure',   'Azure',   0,  'A deep blue ocean world.', planetSwatch('#2f57a0', '#0a1530'));
  add('forcefield', 'planet', 'verdant', 'Verdant', 25, 'A lush green world.', planetSwatch('#2f8a5a', '#08210f'));
  add('forcefield', 'planet', 'amber',   'Amber',   50, 'A golden desert world.', planetSwatch('#c39433', '#241706'));
  add('forcefield', 'planet', 'violet',  'Violet',  75, 'A violet gas giant.', planetSwatch('#7a46b8', '#180a2e'));
  add('forcefield', 'planet', 'nova',    'Nova',    100, 'A blazing rose-red star-world.', planetSwatch('#d84a6a', '#2e0a14'));

  // ---- 🌀 Tube Racer — tube palettes (the whole screen) + bike hulls ----
  function tubeSwatch(ring, seam, bg0, bg1) {
    return function (g, w, h) {
      const bg = g.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, Math.max(w, h) * 0.7);
      bg.addColorStop(0, bg0); bg.addColorStop(1, bg1);
      g.fillStyle = bg; g.fillRect(0, 0, w, h);
      const cx = w / 2, cy = h / 2, R = Math.min(w, h) * 0.46;
      for (let i = 5; i >= 1; i--) {
        g.strokeStyle = ring; g.globalAlpha = 0.14 + i * 0.15; g.lineWidth = i === 5 ? 2 : 1;
        g.beginPath(); g.arc(cx, cy, R * i / 5, 0, 6.283); g.stroke();
      }
      g.globalAlpha = 0.85; g.strokeStyle = seam; g.lineWidth = 1.6;
      g.beginPath(); g.moveTo(cx, cy); g.lineTo(cx, cy + R); g.stroke();
      g.globalAlpha = 1;
    };
  }
  function bikeSwatch(body, trim) {
    return function (g, w, h) {
      g.fillStyle = '#050a16'; g.fillRect(0, 0, w, h);
      const cx = w / 2, cy = h * 0.58, s = Math.min(w, h) * 0.42;
      const grd = g.createLinearGradient(cx - s, cy - s, cx + s, cy + s);
      grd.addColorStop(0, '#ffffff'); grd.addColorStop(0.5, body); grd.addColorStop(1, trim);
      g.fillStyle = grd;
      g.beginPath(); g.moveTo(cx, cy - s); g.lineTo(cx + s * 0.66, cy + s * 0.5);
      g.lineTo(cx, cy + s * 0.1); g.lineTo(cx - s * 0.66, cy + s * 0.5); g.closePath(); g.fill();
      g.strokeStyle = trim; g.lineWidth = 1.2; g.stroke();
      g.fillStyle = 'rgba(255,255,255,.8)';
      g.beginPath(); g.ellipse(cx - s * 0.06, cy - s * 0.38, s * 0.12, s * 0.22, -0.3, 0, 6.283); g.fill();
    };
  }
  add('tube-racer', 'tube', 'ion',    'Ion',      0,   'The standard cyan service tunnel.',                    tubeSwatch('#4ff2ff', '#ffb43d', '#0a1c33', '#02040a'));
  add('tube-racer', 'tube', 'magma',  'Magma',    25,  'A cooling duct bored straight through hot rock.',      tubeSwatch('#ff8a3d', '#ffec8c', '#2a1006', '#150703'));
  add('tube-racer', 'tube', 'vapor',  'Vapor',    50,  'Violet neon and long chrome nights.',                  tubeSwatch('#d682ff', '#6ef5ff', '#1b0a33', '#0d0520'));
  add('tube-racer', 'tube', 'toxic',  'Toxic',    100, 'Coolant green, and the warning lights all agree.',     tubeSwatch('#8cff78', '#e6ff5a', '#0a2410', '#04120a'));
  // engine notes — the drone you fly to, auditioned in plans/tube-engine-lab.html
  function waveSwatch(kind, col) {
    return function (g, w, h) {
      g.fillStyle = '#050a16'; g.fillRect(0, 0, w, h);
      const mid = h / 2, amp = h * 0.3, n = Math.max(24, w | 0);
      g.strokeStyle = col; g.lineWidth = Math.max(1.6, w * 0.055); g.lineJoin = 'round';
      g.beginPath();
      for (let i = 0; i <= n; i++) {
        const x = i / n, px = x * w;
        let v;
        if (kind === 'sine') v = Math.sin(x * Math.PI * 4);
        else if (kind === 'square') v = Math.sin(x * Math.PI * 7) > 0 ? 1 : -1;
        else { // gated sawtooth: the pulse envelope IS the character
          const saw = ((x * 7) % 1) * 2 - 1;
          v = saw * (0.35 + 0.65 * Math.max(0, Math.sin(x * Math.PI * 2)));
        }
        const py = mid - v * amp;
        if (i) g.lineTo(px, py); else g.moveTo(px, py);
      }
      g.stroke();
      g.strokeStyle = 'rgba(255,255,255,.1)'; g.lineWidth = 1;
      g.beginPath(); g.moveTo(0, mid); g.lineTo(w, mid); g.stroke();
    };
  }
  add('tube-racer', 'engine', 'reactor',   'Reactor Pulse', 0,  'A throb that races as you speed up — the stock drive.', waveSwatch('pulse',  '#4ff2ff'), engReactor);
  add('tube-racer', 'engine', 'maglev',    'Maglev',        25, 'Almost no grit. Two clean tones and a shimmer.',        waveSwatch('sine',   '#9ff0d8'), engMaglev);
  add('tube-racer', 'engine', 'twostroke', 'Two-Stroke',    50, 'Buzzy and mean. Sounds small, sounds angry.',           waveSwatch('square', '#ffd24d'), engTwostroke);
  add('tube-racer', 'bike', 'arrow',  'Arrow',    0,   'Standard-issue delta hull.',                           bikeSwatch('#eafcff', '#4ff2ff'));
  add('tube-racer', 'bike', 'wasp',   'Wasp',     10,  'Stubby, wide-winged and mean.',                        bikeSwatch('#ffd24d', '#ff8a3d'));
  add('tube-racer', 'bike', 'delta',  'Delta',    25,  'A clean swept wing with no wasted metal.',             bikeSwatch('#b6c6ff', '#7c5cff'));
  add('tube-racer', 'bike', 'fox',      'Fox',    50,  'Ears, tail, and a suspicious amount of speed.',        bikeSwatch('#ffb86b', '#e0883a'));
  add('tube-racer', 'bike', 'ghost',  'Ghost',    100, 'Barely there, and gone before you looked.',            bikeSwatch('#9ff0d8', '#2ee8c8'));

  window.COSMETICS = {
    version: 1,
    items: items,
    cursors: CURSORS,
    // set labels for the store modal + in-game groups
    sets: {
      'site.cursor':          { label: 'Cursor skins', note: 'desktop only' },
      'site.fx':              { label: 'Display mode', note: 'site-wide' },
      'snake.food':           { label: 'Food skins' },
      'snake.track':          { label: 'Music' },
      'asteroids.track':      { label: 'Music' },
      'asteroids-plus.track':  { label: 'Music' },
      'forcefield.track':     { label: 'Music' },
      'aim-trainer.track':          { label: 'Music' },
      'breakout.track':       { label: 'Music' },
      'bubbles.track':        { label: 'Music' },
      'frog-bonk.track':       { label: 'Music' },
      'tower-defense.track':           { label: 'Music' },
      'stacker.track':        { label: 'Music' },
      'sudoku.track':         { label: 'Music' },
      'minesweeper.track':    { label: 'Music' },
      '2048.track':           { label: 'Music' },
      'trap-the-cat.track':     { label: 'Music' },
      'glow-says.track':       { label: 'Music' },
      'balloon-pop.track':     { label: 'Music' },
      'critter-match.track':   { label: 'Music' },
      'floodgate.track':      { label: 'Music' },
      'mirror-maze.track':     { label: 'Music' },
      'type-siege.track':      { label: 'Music' },
      'dusk-runner.track':     { label: 'Music' },
      'tube-racer.track':      { label: 'Music' },
      'flappy.track':         { label: 'Music' },
      'breakout.paddle':      { label: 'Paddle skins' },
      'breakout.ball':        { label: 'Ball skins' },
      'tower-defense.castle': { label: 'Castle skins' },
      'bubbles.pop':          { label: 'Pop effects' },
      'bubbles.base':         { label: 'Shooter bases' },
      'aim-trainer.target':   { label: 'Target skins' },
      'aim-trainer.marker':   { label: 'Hit markers' },
      'stacker.palette':      { label: 'Block palettes' },
      'asteroids.ship':       { label: 'Ship colours' },
      'asteroids-plus.hull':  { label: 'Hull colours' },
      'asteroids-plus.trail': { label: 'Engine trails' },
      'flappy.bird':          { label: 'Birds' },
      'forcefield.marker':         { label: 'Bolt colours' },
      'forcefield.planet':         { label: 'Planet skins' },
      'frog-bonk.hammer':    { label: 'Hammer skins' },
      'frog-bonk.meadow':    { label: 'Meadow seasons' },
      'sudoku.board':        { label: 'Board themes' },
      'sudoku.digits':       { label: 'Numeral styles' },
      'minesweeper.board':   { label: 'Board themes' },
      '2048.tiles':          { label: 'Tile themes' },
      'trap-the-cat.cat':    { label: 'Cat colours' },
      'glow-says.pads':      { label: 'Lantern shapes' },
      'balloon-pop.balloons': { label: 'Balloon styles' },
      'critter-match.critters': { label: 'Critter sets' },
      'dusk-runner.style': { label: 'Look' },
      'dusk-runner.runner': { label: 'Runners' },
      'mirror-maze.beam': { label: 'Beam palettes' },
      'type-siege.ink': { label: 'Ink & paper' },
      'floodgate.pipe': { label: 'Pipe metals' },
      'tube-racer.tube': { label: 'Tube palettes' },
      'tube-racer.bike': { label: 'Bike hulls' },
      'tube-racer.engine': { label: 'Engine notes' },
    },
    // game meta for the store modal (games don't load games.js; '' = site-wide sets)
    games: {
      '':               { title: 'Site-wide', icon: '🖱️', accent: '#9fe8ff' },
      'snake':          { title: 'Neon Snake', icon: '🐍', accent: '#7fffb0' },
      'breakout':       { title: 'Brick Breaker', icon: '🧱', accent: '#ff5cc8' },
      'tower-defense':  { title: 'Keep Defender', icon: '🏰', accent: '#e0b25a' },
      'bubbles':        { title: 'Bubble Pop', icon: '🫧', accent: '#2ee8c8' },
      'aim-trainer':    { title: 'Range', icon: '🎯', accent: '#ff7a3c' },
      'stacker':        { title: 'Stack', icon: '🗼', accent: '#ff9aa2' },
      'asteroids':      { title: 'Asteroids', icon: '🛸', accent: '#9fe8ff' },
      'asteroids-plus': { title: 'Asteroids+', icon: '☄️', accent: '#b98cff' },
      'flappy':         { title: 'Meadow Flyer', icon: '🐤', accent: '#8fd3a6' },
      'forcefield':          { title: 'Forcefield', icon: '🌐', accent: '#38bdf8' },
      'frog-bonk':     { title: 'Frog Bonk', icon: '🐸', accent: '#7ed957' },
      'sudoku':        { title: 'Sudoku', icon: '🔢', accent: '#f0b429' },
      'minesweeper':   { title: 'Minesweeper', icon: '💣', accent: '#35e0ff' },
      '2048':          { title: '2048', icon: '🧮', accent: '#f2b179' },
      'trap-the-cat':  { title: 'Trap the Cat', icon: '🐱', accent: '#f0a6c8' },
      'glow-says':     { title: 'Glow Says', icon: '🟢', accent: '#7ee787' },
      'balloon-pop':   { title: 'Balloon Pop', icon: '🎈', accent: '#ff9ec2' },
      'critter-match': { title: 'Critter Match', icon: '🐾', accent: '#ffb86b' },
      'dusk-runner':   { title: 'Chrome Runner', icon: '🦖', accent: '#cfe0f2' },
      'mirror-maze':   { title: 'Mirror Maze', icon: '🔆', accent: '#7fe8ff' },
      'type-siege':    { title: 'Type Siege', icon: '⌨️', accent: '#6ad6ff' },
      'floodgate':     { title: 'Floodgate', icon: '🚰', accent: '#5fb8d9' },
      'tube-racer':    { title: 'Tube Racer', icon: '🌀', accent: '#4ff2ff' },
    },
  };
})();
