[object Object]

← back to Model Arena

Run on Claude Max subscription only: drop paid API models (kimi/gpt/grok) from arena roster — claude-code (Max plan) + 5 local models = $0. Agent-driven builds already on Max plan.

682fd4679864aa06d07922d98dbce100d7195d23 · 2026-07-25 08:52:21 -0700 · Steve Abrams

Files touched

Diff

commit 682fd4679864aa06d07922d98dbce100d7195d23
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Jul 25 08:52:21 2026 -0700

    Run on Claude Max subscription only: drop paid API models (kimi/gpt/grok) from arena roster — claude-code (Max plan) + 5 local models = $0. Agent-driven builds already on Max plan.
---
 idea-run/run-next.sh               |    2 +-
 staging-90210/90210-blackjack.html | 1354 ++++++++++++++++++++++++++++++++++++
 2 files changed, 1355 insertions(+), 1 deletion(-)

diff --git a/idea-run/run-next.sh b/idea-run/run-next.sh
index d28adcc..8db3e7e 100755
--- a/idea-run/run-next.sh
+++ b/idea-run/run-next.sh
@@ -21,7 +21,7 @@ PROMPT="$PROMPT  DW LUXURY UI REQUIREMENTS (mandatory): (a) a fixed top bar with
 python3 - "$TITLE" "$PROMPT" > /tmp/dwaa-payload.json <<'PY'
 import json,sys
 json.dump({"title":sys.argv[1],"prompt":sys.argv[2],
-           "models":["qwen3-14b","gemma3-12b","hermes3-8b","qwen25-7b","hf-qwen-coder-32b","claude-code","kimi","gpt","grok"],"designTools":True},
+           "models":["qwen3-14b","gemma3-12b","hermes3-8b","qwen25-7b","hf-qwen-coder-32b","claude-code"],"designTools":True},
           sys.stdout)
 PY
 
diff --git a/staging-90210/90210-blackjack.html b/staging-90210/90210-blackjack.html
new file mode 100644
index 0000000..d3d1db8
--- /dev/null
+++ b/staging-90210/90210-blackjack.html
@@ -0,0 +1,1354 @@
+<!DOCTYPE html>
+<!--
+  90210 BLACKJACK — PLAN
+  ======================
+  Single-deck blackjack vs. house dealer.
+
+  RULES:
+  - Standard single deck (52 cards), reshuffled when below 15 cards remain.
+  - Player: hit, stand, double down (first two cards only, costs extra bet from bankroll).
+  - Dealer: hits on soft 16 or less, stands on hard/soft 17+.
+  - Aces count as 11 unless that would bust, then 1.
+  - Blackjack (natural 21 on first two cards) pays 3:2.
+  - Double down: bet doubled, exactly one more card drawn.
+  - Push (tie): bet returned.
+  - No split, no insurance (keeps logic clean and game tight).
+
+  STATE MACHINE: idle → betting → dealing → player-turn → dealer-turn → result
+  - Bet is placed by clicking chip buttons before deal.
+  - Card deal is animated via CSS transform (fly-in from deck position).
+  - Dealer's hole card is face-down until dealer's turn.
+  - Bankroll + best win streak persisted in localStorage.
+
+  SOUND: Web Audio API — card deal tick, chip clink, win fanfare, lose thud,
+  all procedurally generated. Mute toggle top-right.
+
+  UI: Gold-on-marble table. No CDN, no external assets. Everything inline.
+  Serif display font via system stack: "Cormorant Garamond", Georgia, serif.
+  Sans UI font: "Gill Sans", "Optima", sans-serif.
+  Colors: --gold #c9a961, --champagne #f0e6c8, --marble #f7f3ed,
+          --felt #1a3a2a (table felt), --ink #1a1209, --cream #faf6ef.
+-->
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+<title>90210 Blackjack — Beverly Hills Casino</title>
+<style>
+:root {
+  --gold: #c9a961;
+  --gold-light: #e8d49a;
+  --gold-dark: #9a7a3a;
+  --champagne: #f0e6c8;
+  --marble: #f7f3ed;
+  --marble2: #ede8e0;
+  --felt: #1c3d2a;
+  --felt-dark: #122a1c;
+  --felt-rail: #0f2018;
+  --ink: #1a1209;
+  --cream: #faf6ef;
+  --red-card: #8b1a1a;
+  --serif: "Cormorant Garamond", "Garamond", "Times New Roman", Georgia, serif;
+  --sans: "Gill Sans", "Optima", "Trebuchet MS", system-ui, sans-serif;
+}
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+  background: #0e1a14;
+  font-family: var(--sans);
+  color: var(--cream);
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  overflow-x: hidden;
+  user-select: none;
+}
+
+/* ─── HEADER ─── */
+header {
+  width: 100%;
+  text-align: center;
+  padding: 18px 20px 12px;
+  border-bottom: 1px solid rgba(201,169,97,0.25);
+  position: relative;
+  background: linear-gradient(180deg, #0a110d 0%, #0e1a14 100%);
+}
+
+.wordmark {
+  font-family: var(--serif);
+  font-size: clamp(22px, 4vw, 34px);
+  letter-spacing: 0.25em;
+  color: var(--gold);
+  text-transform: uppercase;
+  font-weight: 400;
+}
+
+.wordmark span { color: var(--champagne); }
+
+.tagline {
+  font-family: var(--serif);
+  font-size: clamp(10px, 1.6vw, 13px);
+  letter-spacing: 0.4em;
+  color: rgba(201,169,97,0.55);
+  text-transform: uppercase;
+  margin-top: 3px;
+}
+
+#muteBtn {
+  position: absolute;
+  right: 16px;
+  top: 50%;
+  transform: translateY(-50%);
+  background: rgba(201,169,97,0.12);
+  border: 1px solid rgba(201,169,97,0.3);
+  color: var(--gold);
+  border-radius: 50%;
+  width: 36px;
+  height: 36px;
+  font-size: 16px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: background 0.2s;
+}
+#muteBtn:hover { background: rgba(201,169,97,0.22); }
+
+/* ─── BANKROLL BAR ─── */
+.bankroll-bar {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 32px;
+  padding: 10px 20px;
+  font-size: clamp(11px, 2vw, 13px);
+  letter-spacing: 0.12em;
+  color: rgba(240,230,200,0.7);
+  border-bottom: 1px solid rgba(201,169,97,0.1);
+}
+
+.bankroll-bar .val {
+  font-family: var(--serif);
+  font-size: clamp(15px, 2.5vw, 20px);
+  color: var(--gold);
+  letter-spacing: 0.05em;
+}
+
+/* ─── TABLE ─── */
+.table-wrap {
+  flex: 1;
+  width: 100%;
+  max-width: 900px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0 16px 20px;
+}
+
+.felt-table {
+  width: 100%;
+  background: radial-gradient(ellipse 80% 70% at 50% 45%, #26563a 0%, #1c3d2a 50%, #122a1c 100%);
+  border-radius: 16px;
+  border: 3px solid var(--gold-dark);
+  box-shadow:
+    0 0 0 6px #0a1a10,
+    0 0 0 8px var(--gold-dark),
+    inset 0 2px 8px rgba(0,0,0,0.5),
+    0 20px 60px rgba(0,0,0,0.7);
+  padding: 20px 24px 24px;
+  margin-top: 16px;
+  position: relative;
+  min-height: 400px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+/* table rail ornament */
+.felt-table::before {
+  content: '';
+  position: absolute;
+  inset: 8px;
+  border-radius: 10px;
+  border: 1px solid rgba(201,169,97,0.15);
+  pointer-events: none;
+}
+
+.zone-label {
+  font-family: var(--serif);
+  font-size: clamp(9px, 1.5vw, 11px);
+  letter-spacing: 0.35em;
+  color: rgba(201,169,97,0.35);
+  text-transform: uppercase;
+  text-align: center;
+  margin-bottom: 4px;
+}
+
+/* ─── CARD ZONES ─── */
+.hand-zone {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 4px;
+  flex: 1;
+}
+
+.hand-row {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 120px;
+  gap: 0;
+  position: relative;
+}
+
+/* ─── CARDS ─── */
+.card {
+  width: clamp(56px, 8vw, 72px);
+  height: clamp(80px, 11.4vw, 102px);
+  border-radius: 6px;
+  border: 1.5px solid rgba(180,160,120,0.3);
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  padding: 4px 5px;
+  font-family: var(--serif);
+  font-size: clamp(14px, 2.5vw, 18px);
+  position: relative;
+  margin-left: -14px;
+  transition: transform 0.15s ease;
+  flex-shrink: 0;
+  box-shadow: 2px 3px 8px rgba(0,0,0,0.5);
+
+  /* animation origin */
+  transform-origin: center bottom;
+  animation: cardDeal 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+.card:first-child { margin-left: 0; }
+
+@keyframes cardDeal {
+  from {
+    opacity: 0;
+    transform: translateY(-40px) scale(0.7) rotate(-8deg);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0) scale(1) rotate(0deg);
+  }
+}
+
+.card.face-up {
+  background: linear-gradient(145deg, #fdfaf4 0%, #f5efe3 100%);
+  color: var(--ink);
+}
+
+.card.face-down {
+  background:
+    repeating-linear-gradient(
+      45deg,
+      #1a3d2a 0px, #1a3d2a 3px,
+      #15321f 3px, #15321f 6px
+    );
+  border-color: rgba(201,169,97,0.4);
+}
+
+.card.face-down::after {
+  content: '';
+  position: absolute;
+  inset: 4px;
+  border: 1px solid rgba(201,169,97,0.25);
+  border-radius: 3px;
+}
+
+.card-corner {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  line-height: 1;
+}
+
+.card-corner.bottom {
+  transform: rotate(180deg);
+}
+
+.card-rank {
+  font-size: inherit;
+  font-weight: 600;
+  line-height: 1;
+}
+
+.card-suit {
+  font-size: 0.7em;
+  line-height: 1;
+  margin-top: 1px;
+}
+
+.card.red .card-rank,
+.card.red .card-suit { color: var(--red-card); }
+
+.card-center {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: clamp(20px, 3.5vw, 28px);
+  opacity: 0.18;
+}
+.card.red .card-center { color: var(--red-card); }
+
+/* ─── SCORE BADGE ─── */
+.score-badge {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(0,0,0,0.45);
+  border: 1px solid rgba(201,169,97,0.3);
+  border-radius: 20px;
+  padding: 2px 12px;
+  font-family: var(--serif);
+  font-size: clamp(13px, 2vw, 16px);
+  color: var(--champagne);
+  letter-spacing: 0.08em;
+  min-width: 36px;
+  text-align: center;
+  min-height: 24px;
+}
+
+.score-badge.bust { color: #e07070; border-color: rgba(200,80,80,0.4); }
+.score-badge.blackjack { color: var(--gold); border-color: rgba(201,169,97,0.6); }
+.score-badge.good { color: #7dd4a0; border-color: rgba(80,180,100,0.35); }
+
+/* ─── DIVIDER ─── */
+.table-divider {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 4px 0;
+  color: rgba(201,169,97,0.25);
+  font-size: 11px;
+  letter-spacing: 0.3em;
+}
+.table-divider::before, .table-divider::after {
+  content: '';
+  flex: 1;
+  height: 1px;
+  background: rgba(201,169,97,0.15);
+}
+
+/* ─── BET AREA ─── */
+.bet-area {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 0;
+}
+
+.current-bet-display {
+  font-family: var(--serif);
+  font-size: clamp(18px, 3vw, 26px);
+  color: var(--gold);
+  letter-spacing: 0.08em;
+  min-height: 32px;
+  text-align: center;
+}
+
+.chip-row {
+  display: flex;
+  gap: 10px;
+  flex-wrap: wrap;
+  justify-content: center;
+}
+
+.chip {
+  width: clamp(44px, 7vw, 56px);
+  height: clamp(44px, 7vw, 56px);
+  border-radius: 50%;
+  border: 3px dashed rgba(255,255,255,0.35);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-family: var(--serif);
+  font-size: clamp(11px, 1.8vw, 14px);
+  font-weight: 600;
+  cursor: pointer;
+  transition: transform 0.1s ease, box-shadow 0.15s ease;
+  position: relative;
+  box-shadow: 0 3px 8px rgba(0,0,0,0.4), inset 0 1px 3px rgba(255,255,255,0.15);
+}
+
+.chip:hover { transform: scale(1.08); }
+.chip:active { transform: scale(0.96); }
+
+.chip.c5   { background: radial-gradient(circle at 35% 35%, #e05050, #8b1a1a); color: #fde8e8; }
+.chip.c10  { background: radial-gradient(circle at 35% 35%, #5080e0, #1a3a8b); color: #e8eefe; }
+.chip.c25  { background: radial-gradient(circle at 35% 35%, #50b050, #1a5a1a); color: #e8fde8; }
+.chip.c50  { background: radial-gradient(circle at 35% 35%, #c9a961, #7a5a1a); color: #fdf0d0; }
+.chip.c100 { background: radial-gradient(circle at 35% 35%, #2a2a2a, #111); color: var(--gold); border-color: rgba(201,169,97,0.5); }
+.chip.clear { background: radial-gradient(circle at 35% 35%, #555, #222); color: #aaa; font-size: clamp(9px, 1.4vw, 11px); border-color: rgba(255,255,255,0.15); }
+
+/* ─── ACTION BUTTONS ─── */
+.action-row {
+  display: flex;
+  gap: 10px;
+  justify-content: center;
+  flex-wrap: wrap;
+  padding: 6px 0;
+}
+
+.action-btn {
+  font-family: var(--serif);
+  font-size: clamp(13px, 2vw, 16px);
+  letter-spacing: 0.15em;
+  padding: clamp(9px, 2vw, 12px) clamp(18px, 4vw, 32px);
+  border-radius: 4px;
+  border: none;
+  cursor: pointer;
+  text-transform: uppercase;
+  transition: transform 0.1s ease, box-shadow 0.15s ease, opacity 0.2s;
+  position: relative;
+  overflow: hidden;
+}
+
+.action-btn:active { transform: scale(0.96); }
+.action-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }
+
+.btn-primary {
+  background: linear-gradient(135deg, var(--gold-light) 0%, var(--gold) 50%, var(--gold-dark) 100%);
+  color: var(--ink);
+  box-shadow: 0 3px 14px rgba(201,169,97,0.4);
+}
+.btn-primary:hover:not(:disabled) { box-shadow: 0 5px 20px rgba(201,169,97,0.6); }
+
+.btn-secondary {
+  background: rgba(201,169,97,0.1);
+  color: var(--gold);
+  border: 1px solid rgba(201,169,97,0.4);
+}
+.btn-secondary:hover:not(:disabled) { background: rgba(201,169,97,0.18); }
+
+.btn-danger {
+  background: rgba(160,50,50,0.15);
+  color: #e08888;
+  border: 1px solid rgba(180,60,60,0.35);
+}
+.btn-danger:hover:not(:disabled) { background: rgba(160,50,50,0.25); }
+
+/* ─── STATUS MESSAGE ─── */
+.status-msg {
+  font-family: var(--serif);
+  font-size: clamp(14px, 2.5vw, 18px);
+  letter-spacing: 0.12em;
+  color: var(--champagne);
+  text-align: center;
+  min-height: 26px;
+  padding: 4px 0;
+}
+
+/* ─── RESULT OVERLAY ─── */
+#resultOverlay {
+  position: absolute;
+  inset: 0;
+  border-radius: 13px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  z-index: 10;
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 0.35s ease;
+  backdrop-filter: blur(2px);
+}
+
+#resultOverlay.show {
+  opacity: 1;
+  pointer-events: all;
+}
+
+.result-card {
+  background: rgba(10,18,13,0.92);
+  border: 2px solid var(--gold);
+  border-radius: 12px;
+  padding: 24px 40px;
+  text-align: center;
+  box-shadow: 0 20px 60px rgba(0,0,0,0.8), 0 0 40px rgba(201,169,97,0.15);
+  animation: resultPop 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+}
+
+@keyframes resultPop {
+  from { transform: scale(0.7); opacity: 0; }
+  to   { transform: scale(1);   opacity: 1; }
+}
+
+.result-headline {
+  font-family: var(--serif);
+  font-size: clamp(28px, 5vw, 44px);
+  color: var(--gold);
+  letter-spacing: 0.1em;
+  line-height: 1;
+  margin-bottom: 6px;
+}
+
+.result-headline.lose { color: #e07070; }
+.result-headline.push { color: var(--champagne); }
+
+.result-sub {
+  font-family: var(--serif);
+  font-size: clamp(14px, 2vw, 18px);
+  color: rgba(240,230,200,0.7);
+  letter-spacing: 0.15em;
+  margin-bottom: 14px;
+}
+
+.result-amount {
+  font-family: var(--serif);
+  font-size: clamp(20px, 3.5vw, 30px);
+  letter-spacing: 0.05em;
+  margin-bottom: 18px;
+}
+
+.result-amount.win { color: #80d890; }
+.result-amount.lose { color: #e07070; }
+.result-amount.push { color: var(--champagne); }
+
+/* ─── FOOTER ─── */
+footer {
+  width: 100%;
+  text-align: center;
+  padding: 10px 20px;
+  font-size: 10px;
+  letter-spacing: 0.25em;
+  color: rgba(201,169,97,0.25);
+  text-transform: uppercase;
+  border-top: 1px solid rgba(201,169,97,0.08);
+}
+
+/* ─── STREAK BADGE ─── */
+.streak-badge {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  background: rgba(201,169,97,0.1);
+  border: 1px solid rgba(201,169,97,0.25);
+  border-radius: 20px;
+  padding: 2px 10px;
+  font-size: clamp(10px, 1.5vw, 12px);
+  color: rgba(240,230,200,0.65);
+  letter-spacing: 0.1em;
+}
+
+/* ─── RESPONSIVE ─── */
+@media (max-width: 480px) {
+  .chip-row { gap: 7px; }
+  .action-row { gap: 7px; }
+  .action-btn { padding: 9px 16px; }
+}
+
+/* ─── BUST FLASH ─── */
+@keyframes bustFlash {
+  0%, 100% { box-shadow: inset 0 0 0 transparent; }
+  50% { box-shadow: inset 0 0 30px rgba(200,60,60,0.4); }
+}
+.bust-flash { animation: bustFlash 0.5s ease 2; }
+
+/* ─── CARD FLIP (hole card reveal) ─── */
+@keyframes flipReveal {
+  0%   { transform: rotateY(90deg) scale(0.9); opacity: 0; }
+  100% { transform: rotateY(0deg)  scale(1);   opacity: 1; }
+}
+.flip-reveal { animation: flipReveal 0.3s ease forwards; }
+
+/* ─── BLACKJACK SHIMMER ─── */
+@keyframes bjShimmer {
+  0%, 100% { border-color: var(--gold-dark); }
+  50%       { border-color: var(--gold-light); box-shadow: 0 0 16px rgba(201,169,97,0.6); }
+}
+.bj-shimmer { animation: bjShimmer 0.6s ease 3; }
+
+/* ─── NO SELECT ─── */
+.card, .chip, .action-btn { -webkit-tap-highlight-color: transparent; }
+</style>
+</head>
+<body>
+
+<header>
+  <div class="wordmark">90210 <span>Blackjack</span></div>
+  <div class="tagline">Beverly Hills Casino Royale</div>
+  <button id="muteBtn" title="Toggle sound" aria-label="Toggle sound">🔊</button>
+</header>
+
+<div class="bankroll-bar">
+  <div>
+    <div style="font-size:0.85em;letter-spacing:0.12em;color:rgba(240,230,200,0.5);margin-bottom:2px;">BANKROLL</div>
+    <div class="val" id="bankrollDisplay">$1,000</div>
+  </div>
+  <div>
+    <div style="font-size:0.85em;letter-spacing:0.12em;color:rgba(240,230,200,0.5);margin-bottom:2px;">CURRENT BET</div>
+    <div class="val" id="currentBetDisplay">—</div>
+  </div>
+  <div>
+    <div style="font-size:0.85em;letter-spacing:0.12em;color:rgba(240,230,200,0.5);margin-bottom:2px;">BEST WIN</div>
+    <div class="val" id="bestWinDisplay">$0</div>
+  </div>
+  <div class="streak-badge" id="streakBadge" style="display:none">
+    <span>🔥</span><span id="streakVal">0</span><span>streak</span>
+  </div>
+</div>
+
+<div class="table-wrap">
+  <div class="felt-table" id="feltTable">
+    <div id="resultOverlay">
+      <div class="result-card" id="resultCard">
+        <div class="result-headline" id="resultHeadline">WIN</div>
+        <div class="result-sub" id="resultSub">Blackjack!</div>
+        <div class="result-amount" id="resultAmount">+$150</div>
+        <button class="action-btn btn-primary" id="nextHandBtn" style="letter-spacing:0.2em">DEAL AGAIN</button>
+      </div>
+    </div>
+
+    <!-- Dealer zone -->
+    <div class="hand-zone">
+      <div class="zone-label">Dealer</div>
+      <div class="hand-row" id="dealerHand"></div>
+      <div class="score-badge" id="dealerScore" style="visibility:hidden">0</div>
+    </div>
+
+    <div class="table-divider">⟡ ⟡ ⟡</div>
+
+    <!-- Status -->
+    <div class="status-msg" id="statusMsg">Place your bet to begin</div>
+
+    <!-- Bet / action area -->
+    <div class="bet-area" id="betArea">
+      <div class="current-bet-display" id="betDisplay"></div>
+      <div class="chip-row" id="chipRow">
+        <button class="chip c5"   data-val="5"   aria-label="$5 chip">$5</button>
+        <button class="chip c10"  data-val="10"  aria-label="$10 chip">$10</button>
+        <button class="chip c25"  data-val="25"  aria-label="$25 chip">$25</button>
+        <button class="chip c50"  data-val="50"  aria-label="$50 chip">$50</button>
+        <button class="chip c100" data-val="100" aria-label="$100 chip">$100</button>
+        <button class="chip clear" id="clearBetBtn" aria-label="Clear bet">CLEAR</button>
+      </div>
+    </div>
+
+    <div class="action-row" id="actionRow" style="display:none">
+      <button class="action-btn btn-primary"   id="hitBtn"    disabled>Hit</button>
+      <button class="action-btn btn-secondary" id="standBtn"  disabled>Stand</button>
+      <button class="action-btn btn-secondary" id="doubleBtn" disabled>Double</button>
+    </div>
+
+    <!-- Player zone -->
+    <div class="hand-zone" style="flex-direction:column-reverse">
+      <div class="zone-label" style="margin-bottom:0;margin-top:4px">You</div>
+      <div class="score-badge" id="playerScore" style="visibility:hidden">0</div>
+      <div class="hand-row" id="playerHand"></div>
+    </div>
+
+    <!-- Deal button (shown when not playing) -->
+    <div class="action-row" id="dealRow">
+      <button class="action-btn btn-primary" id="dealBtn" disabled>Deal</button>
+      <button class="action-btn btn-danger"  id="resetBtn" title="Reset bankroll to $1,000">↺ Reset</button>
+    </div>
+
+  </div>
+</div>
+
+<footer>
+  90210 Blackjack · Single Deck · Dealer Stands on 17 · Blackjack Pays 3:2 · Beverly Hills Casino
+</footer>
+
+<script>
+// ═══════════════════════════════════════════════════════
+//  AUDIO ENGINE — procedural Web Audio
+// ═══════════════════════════════════════════════════════
+const AudioCtx = window.AudioContext || window.webkitAudioContext;
+let ctx = null;
+let muted = false;
+
+function getCtx() {
+  if (!ctx) ctx = new AudioCtx();
+  if (ctx.state === 'suspended') ctx.resume();
+  return ctx;
+}
+
+function playTone({ freq = 440, type = 'sine', vol = 0.25, dur = 0.12, attack = 0.01, decay = 0.05, detune = 0 } = {}) {
+  if (muted) return;
+  try {
+    const c = getCtx();
+    const osc = c.createOscillator();
+    const gain = c.createGain();
+    osc.connect(gain); gain.connect(c.destination);
+    osc.type = type;
+    osc.frequency.value = freq;
+    osc.detune.value = detune;
+    gain.gain.setValueAtTime(0, c.currentTime);
+    gain.gain.linearRampToValueAtTime(vol, c.currentTime + attack);
+    gain.gain.linearRampToValueAtTime(0, c.currentTime + dur);
+    osc.start(c.currentTime);
+    osc.stop(c.currentTime + dur + 0.02);
+  } catch(e) {}
+}
+
+function playNoise(vol = 0.08, dur = 0.06) {
+  if (muted) return;
+  try {
+    const c = getCtx();
+    const len = Math.floor(c.sampleRate * dur);
+    const buf = c.createBuffer(1, len, c.sampleRate);
+    const d = buf.getChannelData(0);
+    for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
+    const src = c.createBufferSource();
+    src.buffer = buf;
+    const g = c.createGain();
+    g.gain.setValueAtTime(vol, c.currentTime);
+    g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + dur);
+    const filt = c.createBiquadFilter();
+    filt.type = 'bandpass';
+    filt.frequency.value = 1200;
+    filt.Q.value = 2;
+    src.connect(filt); filt.connect(g); g.connect(c.destination);
+    src.start(); src.stop(c.currentTime + dur + 0.02);
+  } catch(e) {}
+}
+
+const sounds = {
+  deal() {
+    playNoise(0.12, 0.04);
+    setTimeout(() => playTone({ freq: 900, type: 'triangle', vol: 0.08, dur: 0.08 }), 20);
+  },
+  chip() {
+    playNoise(0.1, 0.03);
+    playTone({ freq: 1800, type: 'sine', vol: 0.06, dur: 0.06, attack: 0.005 });
+  },
+  win() {
+    [0, 80, 200, 360].forEach((ms, i) => {
+      setTimeout(() => {
+        const freqs = [523, 659, 784, 1047];
+        playTone({ freq: freqs[i], type: 'triangle', vol: 0.18 - i*0.02, dur: 0.25, attack: 0.01 });
+      }, ms);
+    });
+  },
+  blackjack() {
+    [0, 100, 220, 360, 520].forEach((ms, i) => {
+      setTimeout(() => {
+        const freqs = [523, 659, 784, 1047, 1319];
+        playTone({ freq: freqs[i], type: 'sine', vol: 0.22 - i*0.02, dur: 0.3, attack: 0.01 });
+      }, ms);
+    });
+  },
+  lose() {
+    playTone({ freq: 220, type: 'sawtooth', vol: 0.1, dur: 0.35, attack: 0.01 });
+    setTimeout(() => playTone({ freq: 165, type: 'sawtooth', vol: 0.08, dur: 0.4 }), 180);
+  },
+  push() {
+    playTone({ freq: 440, type: 'triangle', vol: 0.1, dur: 0.2 });
+    setTimeout(() => playTone({ freq: 440, type: 'triangle', vol: 0.07, dur: 0.2 }), 220);
+  },
+  bust() {
+    playTone({ freq: 300, type: 'sawtooth', vol: 0.12, dur: 0.15 });
+    setTimeout(() => playTone({ freq: 200, type: 'sawtooth', vol: 0.1, dur: 0.25 }), 100);
+  },
+  click() {
+    playTone({ freq: 600, type: 'sine', vol: 0.06, dur: 0.06 });
+  }
+};
+
+// ═══════════════════════════════════════════════════════
+//  CARD DECK
+// ═══════════════════════════════════════════════════════
+const SUITS  = ['♠', '♥', '♦', '♣'];
+const RANKS  = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
+const RED_SUITS = new Set(['♥', '♦']);
+
+function buildDeck() {
+  const deck = [];
+  for (const suit of SUITS)
+    for (const rank of RANKS)
+      deck.push({ rank, suit });
+  return deck;
+}
+
+function shuffle(arr) {
+  for (let i = arr.length - 1; i > 0; i--) {
+    const j = Math.floor(Math.random() * (i + 1));
+    [arr[i], arr[j]] = [arr[j], arr[i]];
+  }
+  return arr;
+}
+
+function cardValue(rank) {
+  if (rank === 'A') return 11;
+  if (['J', 'Q', 'K'].includes(rank)) return 10;
+  return parseInt(rank);
+}
+
+function handTotal(cards) {
+  let total = 0, aces = 0;
+  for (const c of cards) {
+    total += cardValue(c.rank);
+    if (c.rank === 'A') aces++;
+  }
+  while (total > 21 && aces > 0) { total -= 10; aces--; }
+  return total;
+}
+
+function isSoft(cards) {
+  // returns true if hand has an ace counted as 11
+  let total = 0, aces = 0;
+  for (const c of cards) {
+    total += cardValue(c.rank);
+    if (c.rank === 'A') aces++;
+  }
+  let softAce = aces > 0 && total <= 21;
+  while (total > 21 && aces > 0) { total -= 10; aces--; softAce = false; }
+  return softAce;
+}
+
+function isBlackjack(cards) {
+  return cards.length === 2 && handTotal(cards) === 21;
+}
+
+// ═══════════════════════════════════════════════════════
+//  GAME STATE
+// ═══════════════════════════════════════════════════════
+let deck       = [];
+let playerCards = [];
+let dealerCards = [];
+let phase      = 'betting'; // betting | player | dealer | done
+let currentBet = 0;
+let bankroll   = 1000;
+let bestWin    = 0;
+let streak     = 0;
+let doubled    = false;
+
+// Persist
+function saveState() {
+  localStorage.setItem('bj90210_bankroll', bankroll);
+  localStorage.setItem('bj90210_bestWin',  bestWin);
+}
+function loadState() {
+  bankroll = parseInt(localStorage.getItem('bj90210_bankroll') || '1000');
+  bestWin  = parseInt(localStorage.getItem('bj90210_bestWin')  || '0');
+  if (isNaN(bankroll) || bankroll < 1) bankroll = 1000;
+  if (isNaN(bestWin)) bestWin = 0;
+}
+
+// ═══════════════════════════════════════════════════════
+//  DOM REFS
+// ═══════════════════════════════════════════════════════
+const $  = id => document.getElementById(id);
+const dh = $('dealerHand');
+const ph = $('playerHand');
+const dealerScore   = $('dealerScore');
+const playerScore   = $('playerScore');
+const statusMsg     = $('statusMsg');
+const betArea       = $('betArea');
+const actionRow     = $('actionRow');
+const dealRow       = $('dealRow');
+const betDisplay    = $('betDisplay');
+const bankrollDisp  = $('bankrollDisplay');
+const currentBetDisp= $('currentBetDisplay');
+const bestWinDisp   = $('bestWinDisplay');
+const streakBadge   = $('streakBadge');
+const streakVal     = $('streakVal');
+const resultOverlay = $('resultOverlay');
+const resultCard    = $('resultCard');
+const hitBtn        = $('hitBtn');
+const standBtn      = $('standBtn');
+const doubleBtn     = $('doubleBtn');
+const dealBtn       = $('dealBtn');
+const nextHandBtn   = $('nextHandBtn');
+const muteBtn       = $('muteBtn');
+
+// ═══════════════════════════════════════════════════════
+//  UI HELPERS
+// ═══════════════════════════════════════════════════════
+function fmt(n) { return '$' + n.toLocaleString(); }
+
+function updateBankrollUI() {
+  bankrollDisp.textContent   = fmt(bankroll);
+  currentBetDisp.textContent = currentBet > 0 ? fmt(currentBet) : '—';
+  bestWinDisp.textContent    = fmt(bestWin);
+  betDisplay.textContent     = currentBet > 0 ? `Bet: ${fmt(currentBet)}` : '';
+  dealBtn.disabled           = currentBet <= 0 || currentBet > bankroll;
+}
+
+function updateStreak() {
+  if (streak >= 2) {
+    streakBadge.style.display = 'inline-flex';
+    streakVal.textContent = streak;
+  } else {
+    streakBadge.style.display = 'none';
+  }
+}
+
+function setStatus(msg) { statusMsg.textContent = msg; }
+
+function showBettingUI() {
+  betArea.style.display  = 'flex';
+  actionRow.style.display = 'none';
+  dealRow.style.display  = 'flex';
+  setChipsEnabled(true);
+}
+
+function showActionUI(enableDouble) {
+  betArea.style.display   = 'none';
+  actionRow.style.display = 'flex';
+  dealRow.style.display   = 'none';
+  hitBtn.disabled    = false;
+  standBtn.disabled  = false;
+  doubleBtn.disabled = !enableDouble;
+}
+
+function hideAllActions() {
+  betArea.style.display   = 'none';
+  actionRow.style.display = 'none';
+  dealRow.style.display   = 'none';
+}
+
+function setChipsEnabled(on) {
+  document.querySelectorAll('.chip[data-val]').forEach(c => c.disabled = !on);
+  $('clearBetBtn').disabled = !on;
+}
+
+// ═══════════════════════════════════════════════════════
+//  CARD RENDERING
+// ═══════════════════════════════════════════════════════
+function makeCardEl(card, faceDown = false, animated = true) {
+  const el = document.createElement('div');
+  el.classList.add('card');
+  if (!animated) el.style.animation = 'none';
+
+  if (faceDown) {
+    el.classList.add('face-down');
+    el.dataset.card = JSON.stringify(card);
+    return el;
+  }
+
+  el.classList.add('face-up');
+  if (RED_SUITS.has(card.suit)) el.classList.add('red');
+
+  el.innerHTML = `
+    <div class="card-corner">
+      <div class="card-rank">${card.rank}</div>
+      <div class="card-suit">${card.suit}</div>
+    </div>
+    <div class="card-center">${card.suit}</div>
+    <div class="card-corner bottom">
+      <div class="card-rank">${card.rank}</div>
+      <div class="card-suit">${card.suit}</div>
+    </div>
+  `;
+  return el;
+}
+
+function revealHoleCard() {
+  const holeEl = dh.querySelector('.face-down');
+  if (!holeEl) return;
+  const card = JSON.parse(holeEl.dataset.card);
+  const newEl = makeCardEl(card, false, false);
+  newEl.classList.add('flip-reveal');
+  dh.replaceChild(newEl, holeEl);
+}
+
+function addCardToHand(hand, handEl, card, faceDown = false, delay = 0) {
+  return new Promise(resolve => {
+    setTimeout(() => {
+      hand.push(card);
+      const el = makeCardEl(card, faceDown);
+      handEl.appendChild(el);
+      sounds.deal();
+      resolve();
+    }, delay);
+  });
+}
+
+function updateScores(showDealer = false) {
+  const ps = handTotal(playerCards);
+  playerScore.style.visibility = 'visible';
+  playerScore.textContent = ps;
+  playerScore.className = 'score-badge';
+  if (ps > 21)  playerScore.classList.add('bust');
+  else if (ps === 21) playerScore.classList.add('blackjack');
+  else if (ps >= 18) playerScore.classList.add('good');
+
+  if (showDealer) {
+    const ds = handTotal(dealerCards);
+    dealerScore.style.visibility = 'visible';
+    dealerScore.textContent = ds;
+    dealerScore.className = 'score-badge';
+    if (ds > 21)  dealerScore.classList.add('bust');
+    else if (ds === 21) dealerScore.classList.add('blackjack');
+    else if (ds >= 17) dealerScore.classList.add('good');
+  } else {
+    // Show only first dealer card value
+    if (dealerCards.length > 0) {
+      dealerScore.style.visibility = 'visible';
+      dealerScore.textContent = cardValue(dealerCards[0].rank);
+      dealerScore.className = 'score-badge';
+    }
+  }
+}
+
+// ═══════════════════════════════════════════════════════
+//  DECK MANAGEMENT
+// ═══════════════════════════════════════════════════════
+function ensureDeck() {
+  if (deck.length < 15) {
+    deck = shuffle(buildDeck());
+    setStatus('Shuffling deck…');
+  }
+}
+
+function draw() {
+  ensureDeck();
+  return deck.pop();
+}
+
+// ═══════════════════════════════════════════════════════
+//  RESULT / OVERLAY
+// ═══════════════════════════════════════════════════════
+function showResult(headline, sub, amount, type) {
+  const h = $('resultHeadline');
+  const s = $('resultSub');
+  const a = $('resultAmount');
+  h.textContent = headline;
+  h.className   = 'result-headline ' + (type === 'lose' ? 'lose' : type === 'push' ? 'push' : '');
+  s.textContent = sub;
+  a.textContent = amount;
+  a.className   = 'result-amount ' + type;
+  resultCard.style.animation = 'none';
+  resultCard.offsetHeight; // reflow
+  resultCard.style.animation = '';
+  resultOverlay.classList.add('show');
+}
+
+function hideResult() {
+  resultOverlay.classList.remove('show');
+}
+
+// ═══════════════════════════════════════════════════════
+//  DEAL
+// ═══════════════════════════════════════════════════════
+async function deal() {
+  if (currentBet <= 0 || currentBet > bankroll || phase !== 'betting') return;
+  sounds.click();
+
+  // Deduct bet from bankroll immediately
+  bankroll -= currentBet;
+  doubled = false;
+  saveState();
+
+  // Clear hands
+  playerCards = [];
+  dealerCards = [];
+  dh.innerHTML = '';
+  ph.innerHTML = '';
+  dealerScore.style.visibility = 'hidden';
+  playerScore.style.visibility = 'hidden';
+  hideResult();
+  hideAllActions();
+  phase = 'dealing';
+  setStatus('Dealing…');
+
+  // Deal: player, dealer, player, dealer(face-down)
+  await addCardToHand(playerCards, ph, draw(), false, 0);
+  await addCardToHand(dealerCards, dh, draw(), false, 200);
+  await addCardToHand(playerCards, ph, draw(), false, 400);
+  await addCardToHand(dealerCards, dh, draw(), true, 600);
+
+  updateScores(false);
+  updateBankrollUI();
+
+  // Check player blackjack
+  if (isBlackjack(playerCards)) {
+    await new Promise(r => setTimeout(r, 200));
+    ph.classList.add('bj-shimmer');
+    revealHoleCard();
+    updateScores(true);
+
+    if (isBlackjack(dealerCards)) {
+      // Both blackjack = push
+      await new Promise(r => setTimeout(r, 600));
+      resolvePush('Both have Blackjack');
+    } else {
+      // Player BJ wins 3:2
+      const win = Math.floor(currentBet * 1.5);
+      await new Promise(r => setTimeout(r, 600));
+      resolveWin(win, 'Blackjack!', true);
+    }
+    return;
+  }
+
+  // Check dealer blackjack (peek)
+  if (isBlackjack(dealerCards)) {
+    await new Promise(r => setTimeout(r, 200));
+    revealHoleCard();
+    updateScores(true);
+    await new Promise(r => setTimeout(r, 600));
+    resolveLose('Dealer has Blackjack');
+    return;
+  }
+
+  // Normal play
+  phase = 'player';
+  const canDouble = bankroll >= currentBet; // has enough for another bet
+  showActionUI(canDouble);
+  setStatus('Your turn');
+}
+
+// ═══════════════════════════════════════════════════════
+//  PLAYER ACTIONS
+// ═══════════════════════════════════════════════════════
+function hit() {
+  if (phase !== 'player') return;
+  sounds.click();
+  const card = draw();
+  playerCards.push(card);
+  ph.appendChild(makeCardEl(card));
+  sounds.deal();
+
+  updateScores(false);
+  doubleBtn.disabled = true; // can't double after hitting
+
+  const total = handTotal(playerCards);
+  if (total > 21) {
+    playerScore.classList.add('bust');
+    $('feltTable').classList.add('bust-flash');
+    setTimeout(() => $('feltTable').classList.remove('bust-flash'), 1000);
+    hideAllActions();
+    setStatus('Bust!');
+    sounds.bust();
+    setTimeout(() => resolveLose('Busted'), 800);
+  } else if (total === 21) {
+    stand();
+  }
+}
+
+function stand() {
+  if (phase !== 'player') return;
+  sounds.click();
+  phase = 'dealer';
+  hideAllActions();
+  dealerTurn();
+}
+
+function doubleDown() {
+  if (phase !== 'player' || bankroll < currentBet) return;
+  sounds.click();
+  sounds.chip();
+  bankroll -= currentBet;
+  currentBet *= 2;
+  doubled = true;
+  saveState();
+  updateBankrollUI();
+  setStatus('Doubled down!');
+
+  const card = draw();
+  playerCards.push(card);
+  ph.appendChild(makeCardEl(card));
+  sounds.deal();
+  updateScores(false);
+
+  const total = handTotal(playerCards);
+  if (total > 21) {
+    playerScore.classList.add('bust');
+    $('feltTable').classList.add('bust-flash');
+    setTimeout(() => $('feltTable').classList.remove('bust-flash'), 1000);
+    setStatus('Bust!');
+    sounds.bust();
+    setTimeout(() => resolveLose('Busted on Double'), 800);
+  } else {
+    phase = 'dealer';
+    setTimeout(() => dealerTurn(), 400);
+  }
+}
+
+// ═══════════════════════════════════════════════════════
+//  DEALER TURN
+// ═══════════════════════════════════════════════════════
+async function dealerTurn() {
+  phase = 'dealer';
+  revealHoleCard();
+  await new Promise(r => setTimeout(r, 350));
+  updateScores(true);
+
+  let dealerTotal = handTotal(dealerCards);
+  setStatus(`Dealer: ${dealerTotal}`);
+
+  while (dealerTotal < 17 || (dealerTotal === 17 && isSoft(dealerCards))) {
+    await new Promise(r => setTimeout(r, 600));
+    const card = draw();
+    dealerCards.push(card);
+    dh.appendChild(makeCardEl(card));
+    sounds.deal();
+    dealerTotal = handTotal(dealerCards);
+    updateScores(true);
+    setStatus(`Dealer: ${dealerTotal}`);
+  }
+
+  await new Promise(r => setTimeout(r, 400));
+
+  const playerTotal = handTotal(playerCards);
+
+  if (dealerTotal > 21) {
+    resolveWin(currentBet, 'Dealer Busted', false);
+  } else if (dealerTotal > playerTotal) {
+    resolveLose(`Dealer ${dealerTotal} vs Your ${playerTotal}`);
+  } else if (playerTotal > dealerTotal) {
+    resolveWin(currentBet, `Your ${playerTotal} vs Dealer ${dealerTotal}`, false);
+  } else {
+    resolvePush(`${playerTotal} — Push`);
+  }
+}
+
+// ═══════════════════════════════════════════════════════
+//  RESOLUTION
+// ═══════════════════════════════════════════════════════
+function resolveWin(winAmount, sub, bj = false) {
+  bankroll += currentBet + winAmount; // return bet + profit
+  if (winAmount > bestWin) { bestWin = winAmount; }
+  streak++;
+  saveState();
+  updateBankrollUI();
+  updateStreak();
+  phase = 'done';
+  setStatus('');
+  if (bj) sounds.blackjack(); else sounds.win();
+  showResult('WIN', sub, `+${fmt(winAmount)}`, 'win');
+}
+
+function resolveLose(sub) {
+  // bet already deducted
+  streak = 0;
+  saveState();
+  updateBankrollUI();
+  updateStreak();
+  phase = 'done';
+  setStatus('');
+  sounds.lose();
+  showResult('LOSE', sub, `−${fmt(currentBet)}`, 'lose');
+  checkBankrupt();
+}
+
+function resolvePush(sub) {
+  bankroll += currentBet; // return bet
+  saveState();
+  updateBankrollUI();
+  phase = 'done';
+  setStatus('');
+  sounds.push();
+  showResult('PUSH', sub, fmt(currentBet) + ' returned', 'push');
+}
+
+function checkBankrupt() {
+  if (bankroll <= 0) {
+    setTimeout(() => {
+      setStatus('Bankrupt — resetting to $1,000');
+      bankroll = 1000;
+      saveState();
+      updateBankrollUI();
+    }, 2000);
+  }
+}
+
+// ═══════════════════════════════════════════════════════
+//  NEXT HAND / RESET
+// ═══════════════════════════════════════════════════════
+function nextHand() {
+  hideResult();
+  phase = 'betting';
+  currentBet = 0;
+  playerCards = [];
+  dealerCards = [];
+  dh.innerHTML = '';
+  ph.innerHTML = '';
+  dealerScore.style.visibility = 'hidden';
+  playerScore.style.visibility = 'hidden';
+  setStatus('Place your bet to begin');
+  showBettingUI();
+  updateBankrollUI();
+}
+
+function resetBankroll() {
+  if (confirm) { /* no native confirm — just reset silently after a hand */ }
+  bankroll = 1000;
+  bestWin  = 0;
+  streak   = 0;
+  saveState();
+  nextHand();
+  setStatus('Bankroll reset to $1,000');
+  updateStreak();
+}
+
+// ═══════════════════════════════════════════════════════
+//  KEYBOARD
+// ═══════════════════════════════════════════════════════
+document.addEventListener('keydown', e => {
+  if (e.repeat) return;
+  const k = e.key.toLowerCase();
+  if (phase === 'player') {
+    if (k === 'h' || k === 'arrowup')   { e.preventDefault(); hit(); }
+    if (k === 's' || k === 'arrowdown') { e.preventDefault(); stand(); }
+    if (k === 'd')                      { e.preventDefault(); if (!doubleBtn.disabled) doubleDown(); }
+  }
+  if (phase === 'betting') {
+    if (k === 'enter' || k === ' ') { e.preventDefault(); if (!dealBtn.disabled) deal(); }
+  }
+  if (phase === 'done') {
+    if (k === 'enter' || k === ' ' || k === 'n') { e.preventDefault(); nextHand(); }
+  }
+  if (k === 'm') { muted = !muted; muteBtn.textContent = muted ? '🔇' : '🔊'; }
+});
+
+// ═══════════════════════════════════════════════════════
+//  EVENT LISTENERS
+// ═══════════════════════════════════════════════════════
+document.querySelectorAll('.chip[data-val]').forEach(chip => {
+  chip.addEventListener('click', () => {
+    if (phase !== 'betting') return;
+    const val = parseInt(chip.dataset.val);
+    const newBet = currentBet + val;
+    if (newBet > bankroll) return; // can't bet more than bankroll
+    currentBet = newBet;
+    sounds.chip();
+    updateBankrollUI();
+  });
+});
+
+$('clearBetBtn').addEventListener('click', () => {
+  if (phase !== 'betting') return;
+  currentBet = 0;
+  sounds.click();
+  updateBankrollUI();
+});
+
+dealBtn.addEventListener('click', deal);
+hitBtn.addEventListener('click', hit);
+standBtn.addEventListener('click', stand);
+doubleBtn.addEventListener('click', doubleDown);
+nextHandBtn.addEventListener('click', nextHand);
+$('resetBtn').addEventListener('click', resetBankroll);
+muteBtn.addEventListener('click', () => {
+  muted = !muted;
+  muteBtn.textContent = muted ? '🔇' : '🔊';
+});
+
+// ═══════════════════════════════════════════════════════
+//  INIT
+// ═══════════════════════════════════════════════════════
+loadState();
+deck = shuffle(buildDeck());
+updateBankrollUI();
+updateStreak();
+showBettingUI();
+setStatus('Place your bet to begin');
+</script>
+</body>
+</html>

← 7527c24 Agent-driven pivot: DW Color Story Studio (frontend-develope  ·  back to Model Arena  ·  auto-save: 2026-07-25T09:05:11 (2 files) — staging-90210/902 52073ae →