← Deckbuilder results

Deckbuilder

Kimi k3 · composer

Initial view of Deckbuilder

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 94,790 bytes · SHA-256 79c76719b6fd
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shadows of Aethelgard — Roguelite Deckbuilder</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cinzel:wght@600;800&amp;family=Spectral:ital,wght@0,400;0,600;1,400&amp;display=swap">
<style>
/* ── css: base ── */
:root{
  --bg:#07070d; --ink:#e9dfc4; --ink-dim:#9a8f74; --gold:#c9a24b; --gold-hi:#ecd28a;
  --blood:#a83a2e; --panel:#100d16; --edge:#3a3244;
  --atk:#d4543a; --def:#5f9fd0; --oth:#9a6fd0;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
  background:var(--bg); color:var(--ink);
  font-family:'Spectral',Georgia,serif; overflow:hidden;
  user-select:none;-webkit-user-select:none;
}
#app{position:fixed;inset:0;overflow:hidden}
#fx-bg{position:absolute;inset:0;width:100%;height:100%;z-index:0}
.fog{position:absolute;width:140vmax;height:140vmax;border-radius:50%;filter:blur(90px);opacity:.16;pointer-events:none;z-index:1}
.fog-a{background:radial-gradient(circle,#3b2a5e 0%,transparent 60%);top:-50vmax;left:-30vmax;animation:fogdrift 46s ease-in-out infinite alternate}
.fog-b{background:radial-gradient(circle,#5e2a2a 0%,transparent 60%);bottom:-60vmax;right:-40vmax;animation:fogdrift 61s ease-in-out infinite alternate-reverse}
#vignette{position:absolute;inset:0;z-index:2;pointer-events:none;
  background:radial-gradient(ellipse at 50% 42%,transparent 40%,rgba(0,0,0,.55) 82%,rgba(0,0,0,.9) 100%)}
.screen{position:absolute;inset:0;z-index:5;display:flex;flex-direction:column;align-items:center;justify-content:center}
.hidden{display:none !important}

/* ---- etched metal buttons ---- */
.btn{
  font-family:'Cinzel',serif;font-weight:600;letter-spacing:.14em;font-size:13px;
  color:var(--ink);background:linear-gradient(180deg,#3a3a40 0%,#232327 45%,#19191d 55%,#2b2b31 100%);
  border:1px solid #565661;border-radius:4px;padding:10px 22px;cursor:pointer;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.12),inset 0 -2px 4px rgba(0,0,0,.6),0 4px 10px rgba(0,0,0,.5);
  text-shadow:0 1px 2px #000;transition:all .15s;position:relative;
}
.btn:hover{border-color:var(--gold);color:var(--gold-hi);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),inset 0 -2px 4px rgba(0,0,0,.6),0 0 14px rgba(201,162,75,.35)}
.btn:active{transform:translateY(1px)}
.btn:disabled{opacity:.35;cursor:not-allowed;filter:grayscale(.6)}
.btn-big{font-size:16px;padding:14px 34px}
.btn-mini{padding:4px 10px;font-size:12px}
.btn-ghost{background:transparent;border-color:#454550;box-shadow:none;margin-top:26px}
::-webkit-scrollbar{width:8px}
::-webkit-scrollbar-track{background:#0b0a10}
::-webkit-scrollbar-thumb{background:#3a3244;border-radius:4px}
/* ── css: screens ── */
/* ---- title ---- */
#screen-title{background:radial-gradient(ellipse at 50% 30%,rgba(58,42,94,.25),transparent 60%)}
.title-wrap{text-align:center;animation:fadein 1.2s ease}
.title-sigil{font-size:44px;color:var(--gold);text-shadow:0 0 30px rgba(201,162,75,.8);animation:sigilfloat 5s ease-in-out infinite}
#screen-title h1{
  font-family:'Cinzel',serif;font-weight:800;font-size:64px;letter-spacing:.12em;line-height:1.05;margin:14px 0 10px;
  background:linear-gradient(180deg,#f4e9c8 20%,#c9a24b 55%,#7a5f26 80%,#e8d191 100%);
  -webkit-background-clip:text;background-clip:text;color:transparent;
  filter:drop-shadow(0 4px 18px rgba(0,0,0,.9)) drop-shadow(0 0 34px rgba(201,162,75,.22));
}
#screen-title h1 span{font-size:26px;letter-spacing:.5em;color:#b9ab83;-webkit-text-fill-color:#b9ab83}
.tagline{color:var(--ink-dim);font-style:italic;font-size:15px;letter-spacing:.06em}
.menu-col{display:flex;flex-direction:column;gap:14px;align-items:center;margin-top:38px}
.footnote{margin-top:34px;font-size:11px;letter-spacing:.3em;color:#5b5348;text-transform:uppercase}

/* ---- class select ---- */
.screen-h{font-family:'Cinzel',serif;font-size:34px;letter-spacing:.14em;color:var(--gold-hi);text-shadow:0 2px 12px #000}
.screen-sub{color:var(--ink-dim);font-style:italic;margin:8px 0 26px}
#class-row{display:flex;gap:34px;align-items:stretch}
.class-card{
  width:264px;background:linear-gradient(180deg,#16131d,#0c0a11);border:1px solid var(--edge);border-radius:10px;
  padding:20px 18px 18px;text-align:center;cursor:pointer;transition:all .2s;position:relative;
}
.class-card:hover{transform:translateY(-8px);border-color:var(--cls-color,#c9a24b);box-shadow:0 14px 40px rgba(0,0,0,.7),0 0 26px color-mix(in srgb,var(--cls-color,#c9a24b) 30%,transparent)}
.class-card .cls-hero{height:170px;display:flex;align-items:flex-end;justify-content:center}
.class-card h3{font-family:'Cinzel',serif;font-size:20px;letter-spacing:.1em;color:var(--cls-color);margin:10px 0 2px}
.class-card .cls-title{font-size:11px;letter-spacing:.28em;text-transform:uppercase;color:var(--ink-dim)}
.class-card .cls-desc{font-size:12.5px;color:#bfb49a;font-style:italic;margin:10px 0 12px;min-height:48px}
.cls-stats{display:flex;justify-content:center;gap:14px;font-size:12px;color:var(--ink);border-top:1px solid #2a2434;border-bottom:1px solid #2a2434;padding:7px 0;margin-bottom:12px}
.cls-stats b{color:var(--gold-hi)}
.cls-back{position:absolute;top:12px;right:12px;width:44px;filter:drop-shadow(0 3px 6px #000);transform:rotate(8deg)}
.class-card .btn{width:100%}
/* ── css: game ── */
/* ---- game layout ---- */
#screen-game{justify-content:flex-start}
#topbar{
  width:100%;height:46px;display:flex;align-items:center;justify-content:space-between;flex:0 0 auto;
  background:linear-gradient(180deg,#17141f,#0d0b12);border-bottom:1px solid #2c2536;padding:0 16px;z-index:6;
  font-family:'Cinzel',serif;font-size:13px;letter-spacing:.1em;
}
.tb-cell{display:flex;align-items:center;gap:14px}
#tb-class{color:var(--gold-hi)}
#tb-floor{color:var(--ink)}
#tb-pips{letter-spacing:.35em;color:var(--gold);font-size:11px}
#tb-gold{color:var(--gold-hi)}
#tb-gold::before{content:'◈ ';color:var(--gold)}
#tb-boss{color:#ff6a5a;font-size:12px}
#stage{flex:1;display:flex;width:100%;min-height:0;position:relative;z-index:5}

/* hand column (left) */
#hand-col{width:196px;flex:0 0 auto;padding:6px 4px 10px 12px;overflow-y:auto;overflow-x:visible;position:relative;z-index:20}
.col-label{text-align:center;font-family:'Cinzel',serif;font-size:10px;letter-spacing:.4em;color:#6a5f49;margin-bottom:8px}
#hand{display:flex;flex-direction:column;align-items:center;padding-bottom:120px}

/* arena (center) */
#arena{flex:1;position:relative;display:flex;align-items:flex-end;justify-content:center;gap:6%;padding:0 10px 34px;overflow:hidden}
#arena::before{content:'';position:absolute;left:4%;right:4%;bottom:0;height:120px;
  background:radial-gradient(ellipse at 50% 100%,rgba(201,162,75,.07),transparent 70%);pointer-events:none}
#hero-zone{position:relative;width:220px;display:flex;flex-direction:column;align-items:center;margin-bottom:8px}
#enemy-zone{display:flex;align-items:flex-end;justify-content:center;gap:44px;min-height:340px}

/* side panel (right) */
#side-panel{width:188px;flex:0 0 auto;display:flex;flex-direction:column;gap:12px;align-items:stretch;
  padding:14px 14px;border-left:1px solid #241f2e;background:linear-gradient(270deg,rgba(20,16,28,.85),rgba(20,16,28,.4));z-index:6}
#energy-box{text-align:center}
#energy-box .e-label{font-family:'Cinzel',serif;font-size:10px;letter-spacing:.35em;color:#6a5f49}
#energy-num{font-family:'Cinzel',serif;font-size:30px;color:#7fd4ff;text-shadow:0 0 16px rgba(127,212,255,.6);line-height:1.1}
#energy-num small{font-size:14px;color:#4a7d99}
.e-pips{display:flex;justify-content:center;gap:5px;margin-top:6px}
.e-pip{width:13px;height:13px;border-radius:50%;border:1px solid #3f6f8a;background:#0d1420}
.e-pip.on{background:radial-gradient(circle at 35% 35%,#bdeaff,#3d9fd0 70%);box-shadow:0 0 8px rgba(127,212,255,.8)}
#pile-box{display:flex;justify-content:space-between;gap:8px}
.pile{flex:1;text-align:center;background:#0d0b12;border:1px solid #2c2536;border-radius:6px;padding:7px 4px}
.pile .p-num{font-family:'Cinzel',serif;font-size:17px;color:var(--ink)}
.pile .p-label{font-size:9px;letter-spacing:.2em;color:#6a5f49;text-transform:uppercase}
.pile .mini-back{display:inline-block;width:26px;margin-bottom:3px}
#log{
  width:100%;height:64px;flex:0 0 auto;overflow:hidden;padding:6px 18px;z-index:6;
  background:#0a080e;border-top:1px solid #241f2e;font-size:11.5px;color:#8f8468;line-height:1.45;
  display:flex;flex-direction:column;justify-content:flex-end;
}
#log .l-new{color:#d8c9a3}
#log .l-dmg{color:#e08a74} #log .l-good{color:#9fc98a} #log .l-gold{color:var(--gold-hi)}
/* ── css: cards ── */
/* ---- cards ---- */
.hcard{width:158px;flex:0 0 auto;position:relative;cursor:pointer;
  transition:transform .18s ease,filter .18s ease;filter:drop-shadow(0 6px 10px rgba(0,0,0,.65))}
.hcard + .hcard{margin-top:-146px}
.hcard:hover,.hcard.selected{transform:translateX(58px) scale(1.34);z-index:100 !important;
  filter:drop-shadow(0 10px 22px rgba(0,0,0,.85)) drop-shadow(0 0 14px rgba(201,162,75,.35))}
.hcard.cant{filter:grayscale(.75) brightness(.5) drop-shadow(0 4px 8px rgba(0,0,0,.6))}
.hcard.cant:hover{filter:grayscale(.5) brightness(.72) drop-shadow(0 8px 16px rgba(0,0,0,.8))}
.hcard.playable{filter:drop-shadow(0 6px 10px rgba(0,0,0,.65)) drop-shadow(0 0 7px rgba(201,162,75,.28))}
.hcard.pick-discard{animation:pulse-gold 1s ease-in-out infinite}
.card-svg{display:block;width:100%;height:auto}

/* shop / reward big cards */
.shop-card{width:172px;filter:drop-shadow(0 8px 18px rgba(0,0,0,.8))}
.deck-view-card{width:118px;display:inline-block;margin:4px;filter:drop-shadow(0 4px 8px rgba(0,0,0,.7))}
/* ── css: combatants ── */
/* ---- hero & enemies ---- */
.sprite-svg{display:block;overflow:visible}
.hero-inner{animation:bob 3.4s ease-in-out infinite;transform-box:fill-box;transform-origin:50% 100%}
.arm-weapon{transition:transform .16s ease;transform-box:fill-box;transform-origin:30% 85%}
#hero-sprite.pose-attack .arm-weapon{transform:rotate(-58deg) translate(-4px,-6px)}
#hero-sprite.pose-attack .hero-inner{animation:none;transform:translateX(16px)}
#hero-sprite.pose-hit .hero-inner{animation:none;transform:translateX(-12px) rotate(-5deg)}
#hero-sprite .hit-flash,#enemy-svg .hit-flash{opacity:0;transition:opacity .1s}
#hero-sprite.pose-hit .hit-flash{opacity:.4}

.hpwrap{width:170px;max-width:100%;margin-top:6px;text-align:center}
.hpname{font-family:'Cinzel',serif;font-size:12px;letter-spacing:.12em;color:var(--ink);text-shadow:0 1px 3px #000}
.hpbar{height:10px;border:1px solid #000;background:#1a0605;border-radius:5px;overflow:hidden;margin-top:3px;
  box-shadow:inset 0 2px 3px rgba(0,0,0,.8),0 1px 0 rgba(255,255,255,.06)}
.hpfill{height:100%;background:linear-gradient(180deg,#d4503c,#7e1f14);transition:width .3s ease}
.hpnum{font-size:11px;color:#e8b7a6;margin-top:2px;font-family:'Cinzel',serif}
.chips{display:flex;justify-content:center;gap:4px;margin-top:4px;min-height:18px;flex-wrap:wrap}
.chip{font-size:10px;padding:1px 7px;border-radius:8px;border:1px solid #000;letter-spacing:.04em;font-weight:600}
.chip.block{background:linear-gradient(180deg,#3d6a8f,#22415c);color:#cfe7f7;box-shadow:0 0 8px rgba(95,159,208,.5)}
.chip.burn{background:linear-gradient(180deg,#8f5a1f,#5c2f10);color:#ffd9a0}
.chip.poison{background:linear-gradient(180deg,#4d7a2f,#2c4d1a);color:#d6f7b0}
.chip.stun{background:linear-gradient(180deg,#8f8f8f,#4a4a4a);color:#fff}
.chip.silence{background:linear-gradient(180deg,#5e4a8f,#35275c);color:#dccfff}
.chip.dodge,.chip.ghost{background:linear-gradient(180deg,#4a3f73,#241d38);color:#cfc4f5}

.enemy{position:relative;display:flex;flex-direction:column;align-items:center;transition:transform .15s}
.enemy .sprite-wrap{position:relative}
.enemy.idle-bob .sprite-svg{animation:bob 3.8s ease-in-out infinite}
.enemy.hurt .sprite-svg{animation:shake .38s ease}
.enemy.hurt .hit-flash{opacity:.45}
.enemy.targetable{cursor:crosshair}
.enemy.targetable .sprite-svg{filter:drop-shadow(0 0 14px rgba(212,84,58,.75));animation:targetpulse 1s ease-in-out infinite}
.enemy.dead{filter:grayscale(1) brightness(.4);opacity:.35;transform:scale(.94)}
.intent{position:absolute;top:-26px;left:50%;transform:translateX(-50%);white-space:nowrap;
  font-family:'Cinzel',serif;font-size:13px;color:#ffb0a0;text-shadow:0 0 8px rgba(212,84,58,.6);display:flex;gap:5px;align-items:center}
.intent .i-stun{color:#cfcfcf}
.enemy.boss-foe .sprite-svg{width:280px;height:auto}
.boss-crown{position:absolute;top:-44px;left:50%;transform:translateX(-50%);font-size:22px;color:var(--gold);
  text-shadow:0 0 14px rgba(201,162,75,.9)}
.hero-sprite-wrap{position:relative}
#hero-sprite .sprite-svg{width:190px;height:auto}
.hero-hp{width:190px}
.wisp{animation:wispfloat 3s ease-in-out infinite}
/* ── css: overlay ── */
/* ---- overlays ---- */
#overlay,#howto{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;
  background:rgba(4,3,8,.78);backdrop-filter:blur(3px);animation:fadein .25s ease}
#overlay-panel,#howto-panel{
  max-width:900px;max-height:88vh;overflow-y:auto;background:linear-gradient(180deg,#17131f,#0c0a11);
  border:1px solid #3a3244;border-radius:12px;padding:30px 36px;text-align:center;
  box-shadow:0 20px 60px rgba(0,0,0,.8),inset 0 1px 0 rgba(255,255,255,.06)}
#overlay-panel h2,#howto-panel h3{font-family:'Cinzel',serif;letter-spacing:.12em;font-size:26px;color:var(--gold-hi);
  text-shadow:0 2px 10px #000;margin-bottom:6px}
#overlay-panel .ov-sub{color:var(--ink-dim);font-style:italic;margin-bottom:18px;font-size:13.5px}
#overlay-panel .ov-row{display:flex;gap:26px;justify-content:center;align-items:flex-start;flex-wrap:wrap;margin:16px 0}
#overlay-panel .ov-actions{display:flex;gap:14px;justify-content:center;margin-top:22px;flex-wrap:wrap}
.shop-slot{display:flex;flex-direction:column;align-items:center;gap:8px}
.shop-price{font-family:'Cinzel',serif;font-size:15px;color:var(--gold-hi)}
.shop-price::before{content:'◈ '}
.shop-owned{font-size:10.5px;color:#8f8468;letter-spacing:.12em}
.reward-line{font-size:14px;color:#bfb49a;margin:4px 0}
.reward-line b{color:var(--gold-hi)}
#howto-panel{max-width:560px;text-align:left}
#howto-panel h3{text-align:center}
#howto-panel ul{margin:14px 0 20px 18px;font-size:13.5px;line-height:1.65;color:#c9bfa4}
#howto-panel b{color:var(--gold-hi)}
#howto-panel .btn{display:block;margin:0 auto}
.ov-big-stat{font-family:'Cinzel',serif;font-size:15px;color:var(--ink);margin:6px 0}
.victory-h{color:#a8e0b0 !important}
.death-h{color:#e06a5a !important}
/* ── css: anim ── */
/* ---- animations ---- */
@keyframes fadein{from{opacity:0}to{opacity:1}}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
@keyframes sigilfloat{0%,100%{transform:translateY(0) rotate(-6deg)}50%{transform:translateY(-10px) rotate(6deg)}}
@keyframes fogdrift{from{transform:translate(0,0) scale(1)}to{transform:translate(9vmax,5vmax) scale(1.15)}}
@keyframes shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-9px)}40%{transform:translateX(8px)}60%{transform:translateX(-6px)}80%{transform:translateX(4px)}}
@keyframes floatUp{0%{opacity:0;transform:translate(-50%,6px) scale(.7)}15%{opacity:1;transform:translate(-50%,-6px) scale(1.15)}
  100%{opacity:0;transform:translate(-50%,-52px) scale(1)}}
@keyframes bannerIn{0%{opacity:0;transform:translate(-50%,-50%) scale(.85)}15%{opacity:1;transform:translate(-50%,-50%) scale(1)}
  80%{opacity:1}100%{opacity:0;transform:translate(-50%,-50%) scale(1.04)}}
@keyframes pulse-gold{0%,100%{filter:drop-shadow(0 0 6px rgba(201,162,75,.4))}50%{filter:drop-shadow(0 0 18px rgba(201,162,75,.95))}}
@keyframes targetpulse{0%,100%{filter:drop-shadow(0 0 8px rgba(212,84,58,.5))}50%{filter:drop-shadow(0 0 20px rgba(212,84,58,.95))}}
@keyframes wispfloat{0%,100%{transform:translateY(0);opacity:.5}50%{transform:translateY(-8px);opacity:.9}}

.floater{position:absolute;z-index:80;pointer-events:none;font-family:'Cinzel',serif;font-weight:800;font-size:22px;
  text-shadow:0 2px 4px #000,0 0 12px currentColor;animation:floatUp 1.05s ease-out forwards;transform:translate(-50%,0)}
.floater.f-dmg{color:#ff8a70}.floater.f-heal{color:#8ae09a}.floater.f-gold{color:#ecd28a;font-size:17px}
.floater.f-block{color:#8ec9f5}.floater.f-status{color:#d0a0ff;font-size:15px}
.floater.f-miss{color:#cfcfcf;font-size:16px}.floater.f-energy{color:#7fd4ff;font-size:16px}

#banner{position:absolute;left:50%;top:38%;transform:translate(-50%,-50%);z-index:70;pointer-events:none;
  font-family:'Cinzel',serif;font-weight:800;font-size:38px;letter-spacing:.22em;color:var(--gold-hi);text-align:center;
  text-shadow:0 0 30px rgba(201,162,75,.5),0 4px 10px #000;animation:bannerIn 1.5s ease forwards}
#banner small{display:block;font-size:13px;letter-spacing:.4em;color:var(--ink-dim);margin-top:4px}
#target-hint{position:absolute;top:8px;left:50%;transform:translateX(-50%);z-index:70;
  background:rgba(20,10,8,.85);border:1px solid #7a3a2e;border-radius:6px;padding:6px 16px;
  font-size:12.5px;color:#ffb0a0;letter-spacing:.06em;animation:fadein .2s ease}
</style>
</head>
<body>
<div id="app"><canvas id="fx-bg"></canvas>
<div class="fog fog-a"></div>
<div class="fog fog-b"></div>
<div id="vignette"></div>

<section id="screen-title" class="screen">
  <div class="title-wrap">
    <div class="title-sigil">☾</div>
    <h1>SHADOWS<br><span>of</span> AETHELGARD</h1>
    <p class="tagline">The Void consumes the realm. Descend three floors. Slay its lords. Reclaim the land.</p>
    <div class="menu-col">
      <button id="btn-start" class="btn btn-big">⚔ &nbsp;Begin the Descent</button>
      <button id="btn-how" class="btn">How to Play</button>
    </div>
    <p class="footnote">Roguelite Deckbuilder — Prototype Build</p>
  </div>
</section>

<section id="screen-class" class="screen hidden">
  <h2 class="screen-h">Choose Your Champion</h2>
  <p class="screen-sub">Each champion carries a unique 12-card starter deck.</p>
  <div id="class-row"></div>
  <button id="btn-class-back" class="btn btn-ghost">← Return</button>
</section>

<section id="screen-game" class="screen hidden">
  <header id="topbar">
    <div class="tb-cell tb-left"><span id="tb-class"></span></div>
    <div class="tb-cell tb-mid">
      <span id="tb-floor"></span>
      <span id="tb-pips"></span>
      <span id="tb-boss" class="hidden"></span>
    </div>
    <div class="tb-cell tb-right">
      <span id="tb-gold" title="Gold"></span>
      <button id="btn-sound" class="btn btn-mini" title="Toggle sound">♪</button>
    </div>
  </header>
  <div id="stage">
    <aside id="hand-col">
      <div class="col-label">— HAND —</div>
      <div id="hand"></div>
    </aside>
    <div id="arena">
      <div id="hero-zone"></div>
      <div id="enemy-zone"></div>
      <div id="banner" class="hidden"></div>
      <div id="target-hint" class="hidden">Choose a target — click an enemy · right-click to cancel</div>
    </div>
    <aside id="side-panel">
      <div id="energy-box"></div>
      <div id="pile-box"></div>
      <button id="btn-shop" class="btn">⚖ Shop</button>
      <button id="btn-endturn" class="btn btn-big">End Turn ⌛</button>
    </aside>
  </div>
  <div id="log"></div>
</section>

<div id="overlay" class="hidden"><div id="overlay-panel"></div></div>
<div id="howto" class="hidden">
  <div id="howto-panel">
    <h3>How to Play</h3>
    <ul>
      <li><b>Goal:</b> Clear 3 floors — 3 rooms + a Boss each — without dying.</li>
      <li><b>Energy:</b> Cards cost Energy. You regain only <b>+1 per turn</b> (and start combat at full), so spend wisely.</li>
      <li><b>Cards:</b> Click a card to play it. Red = Attack, Blue = Defense, Purple = Other. Attacks may need a target — click an enemy.</li>
      <li><b>Defense:</b> Block absorbs incoming damage before HP. Most block fades at your next turn.</li>
      <li><b>Statuses:</b> Burn &amp; Poison tick damage each turn and decay. Stun skips an enemy's action. Silence seals boss abilities.</li>
      <li><b>Gold:</b> +5 per kill, +15 per boss. Spend it in the Shop — prices rise with each copy you own (max 5).</li>
      <li><b>End Turn:</b> Enemies strike, then you draw up and regain Energy.</li>
    </ul>
    <button id="btn-how-close" class="btn">Close</button>
  </div>
</div></div>
<script>
'use strict';

// ── const: PAL ──
// ---------- theme & static data ----------
const PAL = {
  bastion:{frame1:'#584c3c',frame2:'#211b14',edge:'#c8a256',gem1:'#ecd28a',gem2:'#8a6a2c',art1:'#2b241b',art2:'#0f0c07',ink:'#f2e7c6'},
  wither:{frame1:'#3a4a2a',frame2:'#121a0d',edge:'#9db35a',gem1:'#d3dd7a',gem2:'#5d6c2b',art1:'#1e2614',art2:'#0a0d05',ink:'#ecedc9'},
  shroud:{frame1:'#3a2d5c',frame2:'#120d20',edge:'#9d86e0',gem1:'#c4b0f2',gem2:'#52408c',art1:'#1e1735',art2:'#090613',ink:'#e6dff8'}
};

// ── const: TYPE_COLOR ──
const TYPE_COLOR = {attack:'#c94f38', defense:'#4f8fc9', other:'#8f6fd0'};

// ── const: TYPE_LABEL ──
const TYPE_LABEL = {attack:'ATTACK', defense:'DEFENSE', other:'OTHER'};

// ── const: CLASSES ──
const CLASSES = {
  bastion:{key:'bastion', name:'The Bastion', title:'Immovable Object', hp:35, energy:3, handSize:4,
    color:'#c8a256', motto:'THE IMMOVABLE',
    desc:'A wall of iron and oath. High defense, crushing blows, unyielding will.'},
  wither:{key:'wither', name:'The Wither', title:'Rot and Burn', hp:30, energy:5, handSize:4,
    color:'#9db35a', motto:'ROT & RUIN',
    desc:'A herald of decay. Burns and poisons sap the life of all who stand.'},
  shroud:{key:'shroud', name:'The Shroud', title:'Shadow and Speed', hp:30, energy:5, handSize:4,
    color:'#9d86e0', motto:"DEATH'S WHISPER",
    desc:'A whisper of death. Swift strikes, evasion, and sudden execution.'}
};

// ── const: FLOOR_STATS ──
const FLOOR_STATS = {1:{hp:20,dmg:3}, 2:{hp:30,dmg:5}, 3:{hp:40,dmg:8}};

// ── const: ENEMY_POOL ──
const ENEMY_POOL = {
  1:[{type:'goblin',names:['Goblin Cutter','Goblin Skulker','Cave Goblin','Goblin Raider']},
     {type:'rat',names:['Sewer Rat','Plague Rat','Crypt Rat','Giant Rat']}],
  2:[{type:'knight',names:['Vampire Knight','Blood Knight','Thirsting Knight']},
     {type:'ogre',names:['Ogre Brute','Hill Ogre','Bonecrusher Ogre']}],
  3:[{type:'voidwarrior',names:['Void Warrior','Nullblade Reaver','Void Reaver']},
     {type:'cultist',names:['Void Cultist','Whisper Acolyte','Dark Celebrant']}]
};

// ── const: BOSSES ──
const BOSSES = {
  1:{type:'gatekeeper', name:'The Gatekeeper', hp:80, dmg:15,
     note:'Wards 5 damage every turn while HP ≥ 20.'},
  2:{type:'corrupted', name:'The Corrupted', hp:80, dmg:15,
     note:'Unleashes 20 damage if you hold less than 3 Energy.'},
  3:{type:'voidlord', name:'The Void Lord', hp:80, dmg:15,
     note:'Regenerates 10 HP each turn. Its blows pierce all Defense.'}
};

// ── const: ROMAN ──
const ROMAN = ['','I','II','III'];

// ── const: CARDS ──
// ---------- the 36 core cards ----------
const CARDS = [
  // THE BASTION
  {id:'BA-A01',cls:'bastion',name:'Slam',type:'attack',cost:2,atk:3,def:0,icon:'hammer',
   text:'Deal 3 damage. If you have 30+ HP, draw 1 card.',fx:{dmg:3,slamDraw:1}},
  {id:'BA-A02',cls:'bastion',name:'Earthquake',type:'attack',cost:3,atk:4,def:0,icon:'quake',
   text:'Deal 4 damage to ALL enemies.',fx:{dmgAll:4}},
  {id:'BA-A03',cls:'bastion',name:'Stone Fist',type:'attack',cost:1,atk:2,def:0,icon:'fist',
   text:'Deal 2 damage.',fx:{dmg:2}},
  {id:'BA-A04',cls:'bastion',name:'Boulder',type:'attack',cost:4,atk:6,def:0,icon:'boulder',
   text:'Deal 6 damage. Costs 4 Energy.',fx:{dmg:6}},
  {id:'BA-D01',cls:'bastion',name:'Shield Wall',type:'defense',cost:0,atk:0,def:4,icon:'shield',
   text:'Gain 4 Defense for this turn.',fx:{block:4}},
  {id:'BA-D02',cls:'bastion',name:'Iron Skin',type:'defense',cost:2,atk:0,def:5,icon:'armor',
   text:'Gain 5 Defense permanently. Discard 1 card.',fx:{perm:5,discardN:1}},
  {id:'BA-D03',cls:'bastion',name:'Stand Firm',type:'defense',cost:1,atk:0,def:2,icon:'bulwark',
   text:'Gain 2 Defense. Blocks 1 incoming damage.',fx:{block:2,soak:1}},
  {id:'BA-D04',cls:'bastion',name:'Taunt',type:'defense',cost:2,atk:0,def:3,icon:'horn',
   text:'Gain 3 Defense. Stun 1 enemy for 1 turn.',fx:{block:3,stun:1}},
  {id:'BA-U01',cls:'bastion',name:'Search',type:'other',cost:2,atk:0,def:0,icon:'eye',
   text:'Draw 1 card. Discard down to 5 if hand is larger.',fx:{draw:1,searchCap:1}},
  {id:'BA-U02',cls:'bastion',name:'Repair',type:'other',cost:3,atk:0,def:0,icon:'mend',
   text:'Heal 6 HP.',fx:{heal:6}},
  {id:'BA-U03',cls:'bastion',name:'Rally',type:'other',cost:1,atk:0,def:0,icon:'banner',
   text:'Draw 1 card. Gain 1 Energy.',fx:{draw:1,energy:1}},
  {id:'BA-U04',cls:'bastion',name:'Loot',type:'other',cost:2,atk:0,def:0,icon:'coins',
   text:'Gain 5 Gold.',fx:{gold:5}},
  // THE WITHER
  {id:'WI-A01',cls:'wither',name:'Fireball',type:'attack',cost:2,atk:3,def:0,icon:'flame',
   text:'Deal 3 damage. Apply 1 Burn to target.',fx:{dmg:3,burn:1}},
  {id:'WI-A02',cls:'wither',name:'Inferno',type:'attack',cost:3,atk:4,def:0,icon:'inferno',
   text:'Deal 4 damage. Apply 3 Burn.',fx:{dmg:4,burn:3}},
  {id:'WI-A03',cls:'wither',name:'Ember',type:'attack',cost:1,atk:2,def:0,icon:'ember',
   text:'Deal 2 damage. Target takes 1 Burn every turn.',fx:{dmg:2,burn:1}},
  {id:'WI-A04',cls:'wither',name:'Pyre',type:'attack',cost:4,atk:5,def:0,icon:'pyre',
   text:'Deal 5 damage. ALL enemies take 1 Burn.',fx:{dmg:5,burnAll:1}},
  {id:'WI-D01',cls:'wither',name:'Scorch Shield',type:'defense',cost:2,atk:0,def:3,icon:'heatshield',
   text:'Gain 3 Defense. Wards off 1 Burn damage.',fx:{block:3}},
  {id:'WI-D02',cls:'wither',name:'Heat Resist',type:'defense',cost:1,atk:0,def:1,icon:'warmth',
   text:'Gain 1 Defense. Gain 1 Energy.',fx:{block:1,energy:1}},
  {id:'WI-D03',cls:'wither',name:'Smoke',type:'defense',cost:2,atk:0,def:4,icon:'smoke',
   text:'Gain 4 Defense. Wards off 1 Burn damage.',fx:{block:4}},
  {id:'WI-D04',cls:'wither',name:'Endure',type:'defense',cost:3,atk:0,def:2,icon:'heart',
   text:'Gain 2 Defense. Heal 2 HP.',fx:{block:2,heal:2}},
  {id:'WI-U01',cls:'wither',name:'Poison',type:'other',cost:2,atk:0,def:0,icon:'vial',
   text:'Apply 1 Poison to target enemy.',fx:{poison:1}},
  {id:'WI-U02',cls:'wither',name:'Burn',type:'other',cost:3,atk:0,def:0,icon:'burnmark',
   text:'Deal 3 damage + 2 Burn to ALL enemies.',fx:{dmgAll:3,burnAll:2}},
  {id:'WI-U03',cls:'wither',name:'Slow',type:'other',cost:1,atk:0,def:0,icon:'hourglass',
   text:'Draw 1 card. Target is Stunned next turn.',fx:{draw:1,stun:1}},
  {id:'WI-U04',cls:'wither',name:'Wither',type:'other',cost:2,atk:0,def:0,icon:'wilt',
   text:"Heal 5 HP. Reduce an enemy's Attack by 1.",fx:{heal:5,atkDown:1}},
  // THE SHROUD
  {id:'SH-A01',cls:'shroud',name:'Backstab',type:'attack',cost:2,atk:4,def:0,icon:'dagger',
   text:'Deal 4 damage. +1 damage if target has no Armor.',fx:{dmg:4,backstab:1}},
  {id:'SH-A02',cls:'shroud',name:'Shadow Bolt',type:'attack',cost:3,atk:5,def:0,icon:'bolt',
   text:'Deal 5 damage. Target cannot block it.',fx:{dmg:5,unblockable:1}},
  {id:'SH-A03',cls:'shroud',name:'Silence',type:'attack',cost:1,atk:3,def:0,icon:'mute',
   text:'Deal 3 damage. Target loses its abilities.',fx:{dmg:3,silence:1}},
  {id:'SH-A04',cls:'shroud',name:'Vengeance',type:'attack',cost:3,atk:6,def:0,icon:'twinblades',
   text:'Deal 6 damage. Gain 2 Energy.',fx:{dmg:6,energy:2}},
  {id:'SH-D01',cls:'shroud',name:'Blur',type:'defense',cost:3,atk:0,def:2,icon:'blur',
   text:'Gain 2 Defense. Ignore the next attack.',fx:{block:2,dodge:1}},
  {id:'SH-D02',cls:'shroud',name:'Step',type:'defense',cost:2,atk:0,def:3,icon:'boot',
   text:'Gain 3 Defense. Gain 1 Energy.',fx:{block:3,energy:1}},
  {id:'SH-D03',cls:'shroud',name:'Evasion',type:'defense',cost:1,atk:0,def:1,icon:'wind',
   text:'Gain 1 Defense. Skip all enemy actions.',fx:{block:1,stunAll:1}},
  {id:'SH-D04',cls:'shroud',name:'Phantom',type:'defense',cost:2,atk:0,def:4,icon:'wraith',
   text:'Gain 4 Defense. Ghost form for 1 turn.',fx:{block:4,ghost:1}},
  {id:'SH-U01',cls:'shroud',name:'Dash',type:'other',cost:1,atk:0,def:0,icon:'dash',
   text:'Gain 1 Energy. Dodge the next attack.',fx:{energy:1,dodge:1}},
  {id:'SH-U02',cls:'shroud',name:'Teleport',type:'other',cost:2,atk:0,def:0,icon:'portal',
   text:'Slip to the back row. Deal 2 damage.',fx:{dmg:2}},
  {id:'SH-U03',cls:'shroud',name:'Hide',type:'other',cost:2,atk:0,def:0,icon:'cloak',
   text:'Gain 1 Defense. Discard 1 card, draw 2.',fx:{block:1,discardN:1,drawAfterDiscard:2}},
  {id:'SH-U04',cls:'shroud',name:'Assassinate',type:'other',cost:3,atk:0,def:0,icon:'skull',
   text:'Deal 8 damage. Cannot be blocked this turn.',fx:{dmg:8,unblockable:1}}
];

// ── const: CARD_MAP ──
const CARD_MAP = Object.fromEntries(CARDS.map(c => [c.id, c]));

// ── function: cardById ──
function cardById(id){ return CARD_MAP[id]; }

// ── function: starterDeck ──
function starterDeck(clsKey){ return CARDS.filter(c => c.cls === clsKey).map(c => c.id); }

// ── function: goldPrice ──
function goldPrice(id, owned){
  const c = CARD_MAP[id];
  const base = 1 + c.cost + (c.atk > 0 ? 1 : 0) + (c.def > 0 ? 1 : 0);
  let p = base + owned;
  if (owned >= 3) p = Math.max(p, 10);
  return Math.min(50, p);
}

// ── function: needsTarget ──
function needsTarget(card){
  const f = card.fx;
  return !!(f.dmg || f.burn || f.poison || f.stun || f.atkDown || f.silence);
}

// ── const: ICONS ──
// ---------- card art glyph icons (24x24 stroke paths) ----------
const ICONS = {
  hammer:'<path d="M5 21 L13 13"/><path d="M9 5 h9 v7 h-9 z"/><path d="M9 5 L7 7"/>',
  quake:'<path d="M3 13 h4 l2 -5 3 9 2 -6 2 3 h5"/>',
  fist:'<rect x="7" y="10" width="10" height="9" rx="2"/><path d="M7 10 V8 a2 2 0 0 1 2 -2 h6 a2 2 0 0 1 2 2 v2 M10.5 10 V6 M14 10 V6"/>',
  boulder:'<circle cx="12" cy="13" r="7"/><path d="M9 10 l3 3 M15 9 l-2 4"/>',
  shield:'<path d="M12 3 l7 3 v6 c0 5 -3 7 -7 9 c-4 -2 -7 -4 -7 -9 V6 z"/>',
  armor:'<path d="M12 3 l7 3 v6 c0 5 -3 7 -7 9 c-4 -2 -7 -4 -7 -9 V6 z"/><path d="M12 6.5 v11"/>',
  bulwark:'<path d="M12 3 l7 3 v6 c0 5 -3 7 -7 9 c-4 -2 -7 -4 -7 -9 V6 z"/><path d="M8 12 h8"/>',
  horn:'<path d="M4 18 v-7 l8 -6 v14 z"/><path d="M12 8 c3 0 6 1 8 3 c-2 2 -5 3 -8 3"/>',
  eye:'<path d="M2 12 c4 -6 16 -6 20 0 c-4 6 -16 6 -20 0 z"/><circle cx="12" cy="12" r="2.6"/>',
  mend:'<circle cx="12" cy="12" r="9"/><path d="M12 8 v8 M8 12 h8"/>',
  banner:'<path d="M6 3 v18"/><path d="M6 4 h11 l-3 3.5 3 3.5 H6"/>',
  coins:'<circle cx="9" cy="9" r="5"/><circle cx="15" cy="15" r="5"/>',
  flame:'<path d="M12 3 c3 4 6 6 6 10 a6 6 0 0 1 -12 0 c0 -4 3 -6 6 -10 z"/>',
  inferno:'<path d="M12 2 c3.5 4.5 7 6.5 7 11 a7 7 0 0 1 -14 0 c0 -4.5 3.5 -6.5 7 -11 z"/><path d="M12 10 c1.5 2 3 3 3 5 a3 3 0 0 1 -6 0 c0 -2 1.5 -3 3 -5 z"/>',
  ember:'<circle cx="12" cy="15" r="4"/><path d="M12 3 v5 M7 5 l2.5 2.5 M17 5 l-2.5 2.5"/>',
  pyre:'<path d="M12 5 c2 3 4 4 4 7 a4 4 0 0 1 -8 0 c0 -3 2 -4 4 -7 z"/><path d="M5 21 l14 -4 M5 17 l14 4"/>',
  heatshield:'<path d="M12 3 l7 3 v6 c0 5 -3 7 -7 9 c-4 -2 -7 -4 -7 -9 V6 z"/><path d="M12 8.5 c1.2 1.6 2.4 2.4 2.4 4 a2.4 2.4 0 0 1 -4.8 0 c0 -1.6 1.2 -2.4 2.4 -4 z"/>',
  warmth:'<circle cx="12" cy="12" r="4"/><path d="M12 2 v3 M12 19 v3 M2 12 h3 M19 12 h3 M5 5 l2 2 M17 17 l2 2 M19 5 l-2 2 M7 17 l-2 2"/>',
  smoke:'<path d="M7 20 c0 -3 4 -3 4 -6 c0 -3 -4 -3 -4 -6 c0 -2 1.5 -3 3 -4"/><path d="M15 22 c0 -3 4 -3 4 -6 c0 -3 -4 -3 -4 -6"/>',
  heart:'<path d="M12 20 c-6 -4 -8 -7 -8 -10 a4 4 0 0 1 8 -2 a4 4 0 0 1 8 2 c0 3 -2 6 -8 10 z"/>',
  vial:'<path d="M10 3 h4"/><path d="M11 3 v5 l-5 9 a3 3 0 0 0 3 4 h6 a3 3 0 0 0 3 -4 l-5 -9 v-5"/><path d="M8.5 15 h7"/>',
  burnmark:'<path d="M12 4 c2.5 3.5 5 5 5 8.5 a5 5 0 0 1 -10 0 c0 -3.5 2.5 -5 5 -8.5 z"/><path d="M4 21 h16"/>',
  hourglass:'<path d="M7 3 h10 M7 21 h10"/><path d="M8 3 c0 5 8 6 8 9 c0 3 -8 4 -8 9"/><path d="M16 3 c0 5 -8 6 -8 9 c0 3 8 4 8 9"/>',
  wilt:'<circle cx="12" cy="9" r="5"/><circle cx="10" cy="8" r="1" fill="currentColor" stroke="none"/><circle cx="14" cy="8" r="1" fill="currentColor" stroke="none"/><path d="M12 14 v7 M9 17 l3 2 3 -2"/>',
  dagger:'<path d="M12 2 l2.5 6 l-2.5 10 l-2.5 -10 z"/><path d="M7 18 h10 M12 18 v4"/>',
  bolt:'<path d="M13 2 L6 13 h5 l-1 9 8 -12 h-5 z"/>',
  mute:'<path d="M4 9 v6 h4 l5 4 V5 L8 9 z"/><path d="M16 9 l5 6 M21 9 l-5 6"/>',
  twinblades:'<path d="M5 21 L16 6 M16 6 l3 -3 M14 4 l6 6"/><path d="M19 21 L8 6 M8 6 L5 3 M10 4 l-6 6" opacity=".6"/>',
  blur:'<path d="M7 3 c4 0 4 18 0 18"/><path d="M12 3 c4 0 4 18 0 18"/><path d="M17 3 c4 0 4 18 0 18"/>',
  boot:'<path d="M8 3 v8 h3 l6 4 v4 H5 v-4 l3 -2 z"/>',
  wind:'<path d="M3 8 h10 a3 3 0 1 0 -3 -3"/><path d="M3 12 h14 a3 3 0 1 1 -3 3"/><path d="M3 16 h7"/>',
  wraith:'<path d="M12 3 a6 6 0 0 1 6 6 v12 l-3 -2.5 -3 2.5 -3 -2.5 -3 2.5 V9 a6 6 0 0 1 6 -6 z"/><circle cx="10" cy="10" r="1" fill="currentColor" stroke="none"/><circle cx="14" cy="10" r="1" fill="currentColor" stroke="none"/>',
  dash:'<path d="M3 6 h8 M2 12 h9 M3 18 h8"/><path d="M14 4 l7 8 -7 8"/>',
  portal:'<ellipse cx="12" cy="12" rx="6" ry="9"/><ellipse cx="12" cy="12" rx="2.4" ry="4.6"/>',
  cloak:'<path d="M12 3 c4 0 6 3 6 6 l3 12 H3 l3 -12 c0 -3 2 -6 6 -6 z"/><path d="M9 12 a3 3 0 0 1 6 0"/>',
  skull:'<path d="M12 3 a7 7 0 0 1 7 7 c0 3 -2 4 -2 6 h-3 v3 h-4 v-3 H7 c0 -2 -2 -3 -2 -6 a7 7 0 0 1 7 -7 z"/><circle cx="9.5" cy="10" r="1.4" fill="currentColor" stroke="none"/><circle cx="14.5" cy="10" r="1.4" fill="currentColor" stroke="none"/>'
};

// ── function: iconSVG ──
function iconSVG(name, color, sw){
  return `<g fill="none" stroke="${color}" stroke-width="${sw || 1.6}" stroke-linecap="round" stroke-linejoin="round">${ICONS[name] || ICONS.skull}</g>`;
}

// ── let: SVG_UID ──
// ---------- SVG card frames (unique per deck type) ----------
let SVG_UID = 0;

// ── function: frameDefs ──
function frameDefs(uid, cls){
  const p = PAL[cls];
  return `<defs>
    <linearGradient id="${uid}-fr" x1="0" y1="0" x2="0.3" y2="1">
      <stop offset="0" stop-color="${p.frame1}"/><stop offset="1" stop-color="${p.frame2}"/></linearGradient>
    <linearGradient id="${uid}-art" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="${p.art1}"/><stop offset="1" stop-color="${p.art2}"/></linearGradient>
    <linearGradient id="${uid}-gem" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="${p.gem1}"/><stop offset="1" stop-color="${p.gem2}"/></linearGradient>
    <pattern id="${uid}-hatch" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
      <line x1="0" y1="0" x2="0" y2="7" stroke="#ffffff" stroke-opacity="0.055" stroke-width="2.4"/></pattern>
  </defs>`;
}

// ── function: frameOrnaments ──
function frameOrnaments(cls, p){
  if (cls === 'bastion'){
    // heavy riveted ironwork
    return `<rect x="3" y="3" width="174" height="234" rx="9" fill="none" stroke="${p.edge}" stroke-width="2.8"/>
      <rect x="8.5" y="8.5" width="163" height="223" rx="5" fill="none" stroke="#000" stroke-opacity=".6" stroke-width="1.4"/>
      <rect x="10" y="38" width="4.5" height="164" rx="2" fill="#000" opacity=".32"/>
      <rect x="165.5" y="38" width="4.5" height="164" rx="2" fill="#000" opacity=".32"/>
      ${[13,167].map(x => [13,227].map(y =>
        `<circle cx="${x}" cy="${y}" r="3" fill="${p.edge}" stroke="#000" stroke-opacity=".6" stroke-width="1"/>`).join('')).join('')}
      <path d="M60 6 h60 l-6 5 h-48 z" fill="${p.edge}" opacity=".55"/>
      <path d="M60 234 h60 l-6 -5 h-48 z" fill="${p.edge}" opacity=".55"/>`;
  }
  if (cls === 'wither'){
    // gnarled thorn vines
    return `<rect x="3" y="3" width="174" height="234" rx="14" fill="none" stroke="${p.edge}" stroke-width="2.4"/>
      <path d="M12 18 C5 52 19 76 10 106 C3 136 19 162 11 192 C7 208 13 220 10 228" fill="none" stroke="${p.edge}" stroke-width="1.7" opacity=".85"/>
      <path d="M168 18 C175 52 161 76 170 106 C177 136 161 162 169 192 C173 208 167 220 170 228" fill="none" stroke="${p.edge}" stroke-width="1.7" opacity=".85"/>
      <path d="M13 58 l8 -3 M11 118 l-8 -4 M12 178 l7 5 M167 74 l8 3 M169 134 l-7 5 M168 196 l-8 -3" stroke="${p.edge}" stroke-width="1.5" opacity=".8"/>
      ${[13,167].map(x => [13,227].map(y =>
        `<circle cx="${x}" cy="${y}" r="3.4" fill="none" stroke="${p.edge}" stroke-width="1.4"/>`).join('')).join('')}
      <circle cx="90" cy="7" r="2.2" fill="${p.edge}" opacity=".7"/>
      <circle cx="90" cy="233" r="2.2" fill="${p.edge}" opacity=".7"/>`;
  }
  // shroud: sleek moonsilver filigree
  return `<rect x="3" y="3" width="174" height="234" rx="10" fill="none" stroke="${p.edge}" stroke-width="2.2"/>
    <rect x="7.5" y="7.5" width="165" height="225" rx="7" fill="none" stroke="${p.edge}" stroke-opacity=".4" stroke-width="1"/>
    <path d="M7 26 A19 19 0 0 1 26 7 M154 7 A19 19 0 0 1 173 26 M173 214 A19 19 0 0 1 154 233 M26 233 A19 19 0 0 1 7 214"
      fill="none" stroke="${p.edge}" stroke-width="1.4" opacity=".9"/>
    ${[[30,10],[150,10],[10,60],[10,180],[170,60],[170,180],[30,230],[150,230],[90,6],[90,234]].map(([x,y]) =>
      `<circle cx="${x}" cy="${y}" r="1.1" fill="#cfd0ff" opacity=".55"/>`).join('')}
    <path d="M86 231 a5 5 0 1 0 8 0 a3.6 3.6 0 1 1 -8 0" fill="${p.edge}" opacity=".8"/>`;
}

// ── function: artWindowPath ──
function artWindowPath(cls){
  if (cls === 'bastion') return 'M28 44 H152 L160 52 V126 L152 134 H28 L20 126 V52 Z';
  if (cls === 'wither')  return 'M32 42 H148 Q164 44 160 58 V120 Q162 136 146 134 H34 Q16 136 20 118 V60 Q16 42 32 42 Z';
  return 'M24 134 V70 C24 46 58 42 90 36 C122 42 156 46 156 70 V134 Z';
}

// ── function: gemShape ──
function gemShape(cls, uid){
  if (cls === 'bastion')
    return `<path d="M22 7 l13 4.5 v9 c0 8.6 -5.6 11.8 -13 14.8 c-7.4 -3 -13 -6.2 -13 -14.8 v-9 z"
      fill="url(#${uid}-gem)" stroke="#17110a" stroke-width="1.5"/>`;
  if (cls === 'wither')
    return `<path d="M22 7 L34 14 V27 L22 35 L10 27 V14 Z"
      fill="url(#${uid}-gem)" stroke="#10130a" stroke-width="1.5"/>`;
  return `<circle cx="22" cy="21" r="13.5" fill="url(#${uid}-gem)" stroke="#100c1a" stroke-width="1.5"/>
    <circle cx="22" cy="21" r="10" fill="none" stroke="#100c1a" stroke-opacity=".4" stroke-width="1"/>`;
}

// ── function: wrapText ──
function wrapText(t, n){
  const words = t.split(' '); const lines = []; let cur = '';
  for (const w of words){
    if ((cur + ' ' + w).trim().length > n){ lines.push(cur.trim()); cur = w; }
    else cur += ' ' + w;
  }
  if (cur.trim()) lines.push(cur.trim());
  return lines.slice(0, 4);
}

// ── function: cardFaceSVG ──
function cardFaceSVG(card){
  const uid = 'c' + (SVG_UID++);
  const p = PAL[card.cls];
  const tc = TYPE_COLOR[card.type];
  const lines = wrapText(card.text, 30);
  return `<svg viewBox="0 0 180 240" class="card-svg" xmlns="http://www.w3.org/2000/svg">
  ${frameDefs(uid, card.cls)}
  <rect x="2" y="2" width="176" height="236" rx="10" fill="url(#${uid}-fr)"/>
  <rect x="2" y="2" width="176" height="236" rx="10" fill="url(#${uid}-hatch)"/>
  ${frameOrnaments(card.cls, p)}
  <!-- art window (placeholder for main card image) -->
  <path d="${artWindowPath(card.cls)}" fill="url(#${uid}-art)" stroke="${p.edge}" stroke-width="1.6"/>
  <path d="${artWindowPath(card.cls)}" fill="url(#${uid}-hatch)"/>
  <circle cx="90" cy="86" r="27" fill="#ffffff" opacity=".045"/>
  <circle cx="90" cy="86" r="27" fill="none" stroke="${p.edge}" stroke-opacity=".35" stroke-width="1"/>
  <g transform="translate(90,86) scale(2.05) translate(-12,-12)">${iconSVG(card.icon, p.edge, 1.5)}</g>
  <text x="90" y="129" text-anchor="middle" font-family="Cinzel,serif" font-size="6" letter-spacing="2.4" fill="#ffffff" opacity=".26">· ART PLACEHOLDER ·</text>
  <!-- name banner -->
  <path d="M30 11 H150 L157 21 L150 31 H30 L23 21 Z" fill="${p.frame2}" stroke="${p.edge}" stroke-width="1.1"/>
  <text x="92" y="25.5" text-anchor="middle" font-family="Cinzel,serif" font-weight="700" font-size="11" letter-spacing=".5" fill="${p.ink}">${card.name.toUpperCase()}</text>
  <!-- cost gem -->
  ${gemShape(card.cls, uid)}
  <text x="22" y="26.5" text-anchor="middle" font-family="Cinzel,serif" font-weight="800" font-size="14" fill="#171006">${card.cost}</text>
  <!-- type ribbon -->
  <path d="M36 141 H144 L150 148.5 L144 156 H36 L30 148.5 Z" fill="${tc}" opacity=".92"/>
  <text x="90" y="152.8" text-anchor="middle" font-family="Cinzel,serif" font-weight="700" font-size="7.6" letter-spacing="3" fill="#f6ecd6">${TYPE_LABEL[card.type]}</text>
  <!-- rules text -->
  <rect x="15" y="161" width="150" height="47" rx="5" fill="#060402" opacity=".82" stroke="${p.edge}" stroke-width=".8"/>
  ${lines.map((l, i) => `<text x="90" y="${173.5 + i * 10.6}" text-anchor="middle" font-family="Spectral,serif" font-style="italic" font-size="8.4" fill="#ddd0ae">${l}</text>`).join('')}
  <!-- stat pips -->
  ${card.atk > 0 ? `<path d="M26 203 L41 221 L26 239 L11 221 Z" fill="#c94f38" stroke="#2a0f08" stroke-width="1.5"/>
    <text x="26" y="226.5" text-anchor="middle" font-family="Cinzel,serif" font-weight="800" font-size="14" fill="#fff" stroke="#000" stroke-width=".4">${card.atk}</text>` : ''}
  ${card.def > 0 ? `<path d="M154 204 l14 5 v9 c0 9.4 -6 13.4 -14 16.4 c-8 -3 -14 -7 -14 -16.4 v-9 z" fill="#4f8fc9" stroke="#0d2233" stroke-width="1.5"/>
    <text x="154" y="227" text-anchor="middle" font-family="Cinzel,serif" font-weight="800" font-size="13" fill="#fff" stroke="#000" stroke-width=".4">${card.def}</text>` : ''}
  <text x="90" y="233.5" text-anchor="middle" font-family="Cinzel,serif" font-size="5.6" letter-spacing="2.4" fill="#ffffff" opacity=".22">COMMON · ${card.id}</text>
  </svg>`;
}

// ── function: cardBackSVG ──
// ---------- SVG card backs (unique per deck type) ----------
function cardBackSVG(cls){
  const uid = 'b' + (SVG_UID++);
  const p = PAL[cls];
  const c = CLASSES[cls];
  let field = '', sigil = '';
  if (cls === 'bastion'){
    // masonry wall + great shield sigil
    field = [34,56,78,100,122,144,166,188].map(y =>
      `<line x1="16" y1="${y}" x2="164" y2="${y}" stroke="#000" stroke-opacity=".35" stroke-width="1.6"/>`).join('') +
      [52,90,128].map(x => `<line x1="${x}" y1="16" x2="${x}" y2="206" stroke="#000" stroke-opacity=".22" stroke-width="1.4"/>`).join('') +
      [[30,45],[150,45],[30,155],[150,155],[90,34],[90,196]].map(([x,y]) =>
        `<circle cx="${x}" cy="${y}" r="2" fill="${p.edge}" opacity=".5"/>`).join('');
    sigil = `<path d="M90 52 l36 15 v26 c0 26 -16 37 -36 46 c-20 -9 -36 -20 -36 -46 v-26 z"
        fill="none" stroke="${p.edge}" stroke-width="3.4"/>
      <path d="M90 66 l24 10 v18 c0 18 -11 26 -24 32 c-13 -6 -24 -14 -24 -32 v-18 z"
        fill="${p.edge}" opacity=".18"/>
      <path d="M90 70 v48 M74 88 l16 12 16 -12" fill="none" stroke="${p.edge}" stroke-width="2.6"/>`;
  } else if (cls === 'wither'){
    // tangled roots + thorn skull sigil
    field = `<path d="M20 30 C60 60 40 120 90 140 C130 156 150 190 160 210" fill="none" stroke="#000" stroke-opacity=".3" stroke-width="2"/>
      <path d="M160 26 C120 70 140 110 90 130 C50 146 30 180 22 206" fill="none" stroke="#000" stroke-opacity=".3" stroke-width="2"/>
      <path d="M16 110 C50 100 70 70 90 34 M164 120 C130 116 110 150 96 206" fill="none" stroke="#000" stroke-opacity=".22" stroke-width="1.6"/>
      ${[[40,60],[140,80],[50,180],[130,170],[90,20]].map(([x,y]) =>
        `<circle cx="${x}" cy="${y}" r="2" fill="${p.edge}" opacity=".45"/>`).join('')}`;
    sigil = `<circle cx="90" cy="98" r="34" fill="none" stroke="${p.edge}" stroke-width="3"/>
      <circle cx="90" cy="98" r="34" fill="${p.edge}" opacity=".1"/>
      <circle cx="79" cy="92" r="5" fill="${p.edge}"/><circle cx="101" cy="92" r="5" fill="${p.edge}"/>
      <path d="M90 98 l-3 8 h6 z" fill="${p.edge}"/>
      <path d="M78 116 l24 0 M82 116 v6 M90 116 v7 M98 116 v6" stroke="${p.edge}" stroke-width="2.4" fill="none"/>
      <path d="M56 78 l-10 -8 M124 78 l10 -8 M56 118 l-10 8 M124 118 l10 8" stroke="${p.edge}" stroke-width="2.2"/>`;
  } else {
    // starfield + eclipsed moon sigil
    field = [[30,40],[60,26],[120,30],[150,52],[36,150],[52,190],[128,188],[152,150],[90,22],[24,100],[156,105],[70,206],[110,206]].map(([x,y]) =>
      `<circle cx="${x}" cy="${y}" r="1.3" fill="#cfd0ff" opacity="${0.3 + (x % 40) / 100}"/>`).join('') +
      `<path d="M16 206 H164 M16 16 H164" stroke="#000" stroke-opacity=".3" stroke-width="1.4"/>`;
    sigil = `<circle cx="90" cy="98" r="36" fill="none" stroke="${p.edge}" stroke-width="2" opacity=".6"/>
      <path d="M104 66 a38 38 0 1 0 0 64 a29 29 0 1 1 0 -64" fill="${p.edge}" opacity=".9"/>
      <path d="M90 78 v40 M82 86 l8 -8 8 8" stroke="${p.edge}" stroke-width="2.4" fill="none" opacity=".85"/>`;
  }
  return `<svg viewBox="0 0 180 240" class="card-svg" xmlns="http://www.w3.org/2000/svg">
  ${frameDefs(uid, cls)}
  <rect x="2" y="2" width="176" height="236" rx="10" fill="url(#${uid}-fr)"/>
  <rect x="2" y="2" width="176" height="236" rx="10" fill="url(#${uid}-hatch)"/>
  <rect x="14" y="14" width="152" height="212" rx="8" fill="url(#${uid}-art)" stroke="${p.edge}" stroke-width="1.2"/>
  ${field}
  ${sigil}
  ${frameOrnaments(cls, p)}
  <text x="90" y="176" text-anchor="middle" font-family="Cinzel,serif" font-weight="700" font-size="12.5" letter-spacing="4.5" fill="${p.edge}">${cls.toUpperCase()}</text>
  <text x="90" y="192" text-anchor="middle" font-family="Cinzel,serif" font-size="6.4" letter-spacing="2.6" fill="#ffffff" opacity=".4">${c.motto}</text>
  <text x="90" y="222" text-anchor="middle" font-family="Cinzel,serif" font-size="5.4" letter-spacing="2" fill="#ffffff" opacity=".22">SHADOWS OF AETHELGARD</text>
  </svg>`;
}

// ── function: heroSVG ──
// ---------- hero sprites (idle / attack / hit poses via CSS classes) ----------
function heroSVG(cls){
  const flash = '<rect class="hit-flash" x="0" y="0" width="160" height="170" fill="#ff3b30"/>';
  if (cls === 'bastion'){
    return `<svg viewBox="0 0 160 170" class="sprite-svg" xmlns="http://www.w3.org/2000/svg">
      <ellipse cx="80" cy="159" rx="44" ry="8" fill="#000" opacity=".5"/>
      <g class="hero-inner">
        <rect x="61" y="118" width="15" height="34" rx="4" fill="#2b2620"/><rect x="84" y="118" width="15" height="34" rx="4" fill="#2b2620"/>
        <rect x="57" y="149" width="21" height="8" rx="3" fill="#1a1611"/><rect x="82" y="149" width="21" height="8" rx="3" fill="#1a1611"/>
        <path d="M53 62 h54 v38 q0 24 -27 24 q-27 0 -27 -24 z" fill="#4a443c" stroke="#191410" stroke-width="2"/>
        <path d="M61 68 h38 v28 q0 17 -19 17 q-19 0 -19 -17 z" fill="#5f574b"/>
        <path d="M80 73 l10 4 v7 c0 7 -4 10 -10 12 c-6 -2 -10 -5 -10 -12 v-7 z" fill="#c8a256" opacity=".95"/>
        <circle cx="50" cy="66" r="13" fill="#6b6152" stroke="#221d16" stroke-width="2"/>
        <circle cx="110" cy="66" r="13" fill="#6b6152" stroke="#221d16" stroke-width="2"/>
        <path d="M61 36 a19 19 0 0 1 38 0 v12 h-38 z" fill="#574e42" stroke="#191410" stroke-width="2"/>
        <rect x="69" y="41" width="22" height="4.5" fill="#ffb347"/>
        <rect x="69" y="41" width="22" height="4.5" fill="#ffb347" opacity=".5" transform="translate(0,-2)"/>
        <path d="M74 22 h12 l-3 12 h-6 z" fill="#8e2f2f"/>
        <g class="arm-shield">
          <path d="M31 84 l21 8 v20 c0 15 -9 22 -21 28 c-12 -6 -21 -13 -21 -28 v-20 z" transform="translate(6,-4)"
            fill="#6e5f42" stroke="#c8a256" stroke-width="2.4"/>
          <circle cx="37" cy="104" r="6" fill="#c8a256" opacity=".85"/>
        </g>
        <g class="arm-weapon" transform="translate(114,78)">
          <rect x="-5" y="-2" width="11" height="24" rx="4" fill="#4a443c"/>
          <rect x="-3" y="-36" width="6" height="36" fill="#3a332a"/>
          <circle cx="0" cy="-40" r="10" fill="#6b6152" stroke="#221d16" stroke-width="2"/>
          <path d="M0 -52 v-6 M-9 -46 l-5 -4 M9 -46 l5 -4" stroke="#8a7d68" stroke-width="3"/>
        </g>
      </g>${flash}</svg>`;
  }
  if (cls === 'wither'){
    return `<svg viewBox="0 0 160 170" class="sprite-svg" xmlns="http://www.w3.org/2000/svg">
      <ellipse cx="80" cy="159" rx="42" ry="8" fill="#000" opacity=".5"/>
      <g class="hero-inner">
        <path d="M57 66 C49 102 45 132 43 156 h74 C115 132 111 102 103 66 C90 57 70 57 57 66 z" fill="#2e3a22" stroke="#10160b" stroke-width="2"/>
        <path d="M57 66 C60 96 58 126 54 156 h10 C66 126 66 96 64 64 z" fill="#3a4a2a" opacity=".7"/>
        <path d="M61 42 a19 21 0 0 1 38 0 l5 27 h-48 z" fill="#37452a" stroke="#10160b" stroke-width="2"/>
        <ellipse cx="80" cy="53" rx="11" ry="9" fill="#0a0d06"/>
        <circle cx="75" cy="52" r="2.2" fill="#e0b64a"/><circle cx="85" cy="52" r="2.2" fill="#e0b64a"/>
        <path d="M64 108 h32" stroke="#10160b" stroke-width="4"/>
        <rect x="66" y="104" width="6" height="9" rx="2" fill="#7a9a3f"/><rect x="88" y="104" width="6" height="9" rx="2" fill="#c97f3a"/>
        <circle class="wisp" cx="52" cy="88" r="3" fill="#9db35a" opacity=".5"/>
        <circle class="wisp" cx="112" cy="120" r="2.4" fill="#d9a441" opacity=".5"/>
        <g class="arm-weapon" transform="translate(112,66)">
          <rect x="-2.5" y="-14" width="5" height="96" rx="2" fill="#4a3b26"/>
          <circle cx="0" cy="-20" r="9" fill="#d9a441" opacity=".92"/>
          <circle cx="0" cy="-20" r="13" fill="none" stroke="#9db35a" stroke-width="1.6" opacity=".8"/>
          <path d="M-7 -32 l7 5 7 -5" fill="none" stroke="#9db35a" stroke-width="1.6"/>
        </g>
        <circle cx="56" cy="76" r="6" fill="#2e3a22" stroke="#10160b" stroke-width="1.6"/>
      </g>${flash}</svg>`;
  }
  // shroud
  return `<svg viewBox="0 0 160 170" class="sprite-svg" xmlns="http://www.w3.org/2000/svg">
    <ellipse cx="80" cy="159" rx="40" ry="8" fill="#000" opacity=".5"/>
    <g class="hero-inner">
      <path d="M59 62 C51 98 49 130 47 156 h66 C111 130 109 98 101 62 C89 55 71 55 59 62 z" fill="#241d38" stroke="#0d0a17" stroke-width="2"/>
      <path d="M59 62 C62 92 60 124 56 156 h9 C67 124 67 92 66 60 z" fill="#312852" opacity=".7"/>
      <path d="M63 40 a18 20 0 0 1 34 0 l6 25 h-46 z" fill="#2f2650" stroke="#0d0a17" stroke-width="2"/>
      <ellipse cx="80" cy="51" rx="10" ry="8.4" fill="#080610"/>
      <circle cx="75.5" cy="50" r="2" fill="#b9a4ee"/><circle cx="84.5" cy="50" r="2" fill="#b9a4ee"/>
      <path d="M62 84 q18 10 36 0" fill="none" stroke="#4a3f73" stroke-width="5" opacity=".9"/>
      <circle class="wisp" cx="50" cy="96" r="2.6" fill="#9d86e0" opacity=".55"/>
      <circle class="wisp" cx="114" cy="84" r="2.2" fill="#b9a4ee" opacity=".5"/>
      <g class="arm-weapon" transform="translate(110,70)">
        <rect x="-4" y="0" width="9" height="20" rx="4" fill="#312852"/>
        <path d="M0 -2 l8 -3 l-3 26 z" fill="#cfd0e8" stroke="#8f8fb0" stroke-width="1"/>
        <rect x="-4" y="-4" width="13" height="3.4" rx="1.6" fill="#8a7d68"/>
      </g>
      <circle cx="54" cy="72" r="5.4" fill="#241d38" stroke="#0d0a17" stroke-width="1.6"/>
    </g>${flash}</svg>`;
}

// ── function: enemySVG ──
// ---------- enemy & boss sprites (idle / hurt via CSS) ----------
function enemySVG(type){
  const flash = '<rect class="hit-flash" x="0" y="0" width="200" height="200" fill="#ff5040"/>';
  const shadow = (rx) => `<ellipse cx="100" cy="186" rx="${rx}" ry="9" fill="#000" opacity=".5"/>`;
  switch (type){
    case 'rat': return `<svg viewBox="0 0 200 200" class="sprite-svg" id="enemy-svg">${shadow(56)}
      <g class="hero-inner">
      <path d="M156 142 q30 6 34 -20" fill="none" stroke="#6b5157" stroke-width="6" stroke-linecap="round"/>
      <ellipse cx="112" cy="134" rx="52" ry="30" fill="#453c33" stroke="#1d1812" stroke-width="2"/>
      <circle cx="66" cy="120" r="24" fill="#4d443a" stroke="#1d1812" stroke-width="2"/>
      <path d="M46 116 l-16 8 16 6 z" fill="#4d443a" stroke="#1d1812" stroke-width="1.6"/>
      <circle cx="31" cy="124" r="3" fill="#c98a8a"/>
      <circle cx="58" cy="97" r="8" fill="#3a332c"/><circle cx="58" cy="97" r="4" fill="#6b5157"/>
      <circle cx="76" cy="95" r="7" fill="#3a332c"/><circle cx="76" cy="95" r="3.4" fill="#6b5157"/>
      <circle cx="58" cy="114" r="3.2" fill="#e03b2f"/><circle cx="72" cy="112" r="2.6" fill="#e03b2f"/>
      <path d="M44 128 l4 4 M48 130 l4 3" stroke="#e8e0d0" stroke-width="1.6"/>
      <rect x="88" y="158" width="10" height="14" rx="3" fill="#3a332c"/><rect x="128" y="158" width="10" height="14" rx="3" fill="#3a332c"/>
      </g>${flash}</svg>`;
    case 'goblin': return `<svg viewBox="0 0 200 200" class="sprite-svg" id="enemy-svg">${shadow(46)}
      <g class="hero-inner">
      <path d="M70 158 q-8 -56 30 -70 q42 -12 46 32 q2 22 -4 38 z" fill="#4d5a33" stroke="#1c2410" stroke-width="2"/>
      <path d="M66 74 l-30 -12 24 26 z" fill="#4d5a33" stroke="#1c2410" stroke-width="1.8"/>
      <path d="M118 72 l30 -14 -22 28 z" fill="#4d5a33" stroke="#1c2410" stroke-width="1.8"/>
      <circle cx="92" cy="72" r="22" fill="#5a6a3c" stroke="#1c2410" stroke-width="2"/>
      <circle cx="83" cy="68" r="3.6" fill="#ffd23f"/><circle cx="101" cy="68" r="3.6" fill="#ffd23f"/>
      <path d="M80 84 q12 8 24 0" fill="none" stroke="#1c2410" stroke-width="2.4"/>
      <path d="M86 86 l2 5 M98 86 l-2 5" stroke="#e8e0d0" stroke-width="1.8"/>
      <path d="M130 108 l18 22" stroke="#5a6a3c" stroke-width="8" stroke-linecap="round"/>
      <path d="M150 128 l12 -8 -2 16 z" fill="#b9bcc4" stroke="#4a4d55" stroke-width="1.4"/>
      <rect x="82" y="156" width="12" height="20" rx="4" fill="#42502c"/><rect x="108" y="156" width="12" height="20" rx="4" fill="#42502c"/>
      </g>${flash}</svg>`;
    case 'knight': return `<svg viewBox="0 0 200 200" class="sprite-svg" id="enemy-svg">${shadow(48)}
      <g class="hero-inner">
      <path d="M62 62 q-16 66 -10 104 h96 q6 -44 -8 -104 z" fill="#3a0f16" stroke="#160608" stroke-width="2"/>
      <rect x="72" y="76" width="56" height="66" rx="9" fill="#3d4350" stroke="#14171e" stroke-width="2"/>
      <path d="M72 96 h56 M72 116 h56" stroke="#14171e" stroke-width="2"/>
      <circle cx="66" cy="80" r="12" fill="#4a5160" stroke="#14171e" stroke-width="2"/>
      <circle cx="134" cy="80" r="12" fill="#4a5160" stroke="#14171e" stroke-width="2"/>
      <path d="M76 40 a24 24 0 0 1 48 0 v20 h-48 z" fill="#454c5a" stroke="#14171e" stroke-width="2"/>
      <rect x="82" y="48" width="36" height="5" fill="#ff2f4d"/>
      <path d="M96 24 h8 l-2 14 h-4 z" fill="#8e2f2f"/>
      <path d="M140 96 l6 44" stroke="#b9bcc4" stroke-width="6" stroke-linecap="round"/>
      <path d="M132 100 h22" stroke="#8a6a2c" stroke-width="5"/>
      <rect x="82" y="142" width="14" height="30" rx="4" fill="#2c313c"/><rect x="106" y="142" width="14" height="30" rx="4" fill="#2c313c"/>
      </g>${flash}</svg>`;
    case 'ogre': return `<svg viewBox="0 0 200 200" class="sprite-svg" id="enemy-svg">${shadow(58)}
      <g class="hero-inner">
      <ellipse cx="100" cy="130" rx="54" ry="42" fill="#5c5142" stroke="#211c14" stroke-width="2"/>
      <circle cx="100" cy="70" r="23" fill="#66593f" stroke="#211c14" stroke-width="2"/>
      <path d="M84 78 l-6 10 9 -2 z M116 78 l6 10 -9 -2 z" fill="#e8e0d0"/>
      <circle cx="91" cy="66" r="3" fill="#e03b2f"/><circle cx="109" cy="66" r="3" fill="#e03b2f"/>
      <path d="M88 84 q12 6 24 0" stroke="#211c14" stroke-width="2.6" fill="none"/>
      <circle cx="52" cy="106" r="15" fill="#66593f" stroke="#211c14" stroke-width="2"/>
      <circle cx="148" cy="106" r="15" fill="#66593f" stroke="#211c14" stroke-width="2"/>
      <path d="M150 96 l22 -34" stroke="#4a3b26" stroke-width="9" stroke-linecap="round"/>
      <circle cx="176" cy="56" r="11" fill="#4a3b26" stroke="#211c14" stroke-width="2"/>
      <rect x="74" y="166" width="16" height="18" rx="5" fill="#4c4335"/><rect x="112" y="166" width="16" height="18" rx="5" fill="#4c4335"/>
      </g>${flash}</svg>`;
    case 'voidwarrior': return `<svg viewBox="0 0 200 200" class="sprite-svg" id="enemy-svg">${shadow(46)}
      <g class="hero-inner">
      <path d="M66 170 l6 -66 10 -24 -14 -6 8 -20 12 10 12 -16 12 16 12 -10 8 20 -14 6 10 24 6 66 z"
        fill="#262032" stroke="#0d0a14" stroke-width="2"/>
      <path d="M84 62 l-16 -26 M116 62 l16 -26" stroke="#262032" stroke-width="7" stroke-linecap="round"/>
      <circle cx="88" cy="52" r="3.4" fill="#b06ae0"/><circle cx="112" cy="52" r="3.4" fill="#b06ae0"/>
      <path d="M92 96 l8 18 -6 16 M110 100 l-8 14 8 20 M84 140 l10 8" fill="none" stroke="#9d5fd0" stroke-width="2" opacity=".85"/>
      <path d="M70 170 h60" stroke="#9d5fd0" stroke-width="1.6" opacity=".5"/>
      <path d="M136 92 l10 50" stroke="#3a324a" stroke-width="6" stroke-linecap="round"/>
      <path d="M146 142 l6 10 -12 0 z" fill="#9d5fd0"/>
      </g>${flash}</svg>`;
    case 'cultist': return `<svg viewBox="0 0 200 200" class="sprite-svg" id="enemy-svg">${shadow(44)}
      <g class="hero-inner">
      <path d="M66 80 C58 118 54 148 52 172 h96 C146 148 142 118 134 80 C118 68 82 68 66 80 z" fill="#2a1e33" stroke="#120c18" stroke-width="2"/>
      <path d="M70 46 a30 32 0 0 1 60 0 l8 34 h-76 z" fill="#33253f" stroke="#120c18" stroke-width="2"/>
      <ellipse cx="100" cy="60" rx="14" ry="11" fill="#0a0610"/>
      <circle cx="94" cy="59" r="2.6" fill="#c07fff"/><circle cx="106" cy="59" r="2.6" fill="#c07fff"/>
      <path d="M78 108 h44" stroke="#c07fff" stroke-width="2" opacity=".5"/>
      <circle class="wisp" cx="66" cy="100" r="3" fill="#c07fff" opacity=".5"/>
      <g transform="translate(146,74)">
        <rect x="-3" y="-8" width="6" height="104" rx="2.6" fill="#241a2e"/>
        <circle cx="0" cy="-14" r="9" fill="#c07fff" opacity=".9"/>
        <circle cx="0" cy="-14" r="14" fill="none" stroke="#c07fff" stroke-width="1.4" opacity=".5"/>
      </g>
      </g>${flash}</svg>`;
    case 'gatekeeper': return `<svg viewBox="0 0 200 200" class="sprite-svg" id="enemy-svg">${shadow(62)}
      <g class="hero-inner">
      <path d="M40 178 h120 l-8 -20 h-104 z" fill="#2c2924"/>
      <rect x="52" y="34" width="96" height="140" rx="10" fill="#3b372f" stroke="#171410" stroke-width="2.4"/>
      <rect x="60" y="42" width="80" height="124" rx="7" fill="none" stroke="#c8a256" stroke-width="1.6" opacity=".5"/>
      ${[70,100,130].map(x => [58,88,150].map(y => `<circle cx="${x}" cy="${y}" r="2.6" fill="#c8a256" opacity=".6"/>`).join('')).join('')}
      <circle cx="100" cy="112" r="15" fill="#171410"/>
      <circle cx="100" cy="112" r="10" fill="#ffb347"/>
      <rect x="97" y="104" width="6" height="16" fill="#171410"/>
      <path d="M52 60 C30 70 30 110 44 128 M148 60 C170 70 170 110 156 128" fill="none" stroke="#57503f" stroke-width="4"/>
      <path d="M84 34 l-6 -14 h44 l-6 14 z" fill="#2c2924" stroke="#171410" stroke-width="2"/>
      </g>${flash}</svg>`;
    case 'corrupted': return `<svg viewBox="0 0 200 200" class="sprite-svg" id="enemy-svg">${shadow(50)}
      <g class="hero-inner">
      <path d="M70 80 q-30 -18 -34 -44 M76 96 q-36 -4 -50 14 M134 82 q28 -22 26 -48 M138 100 q34 -8 46 10"
        fill="none" stroke="#7a2f5f" stroke-width="5" stroke-linecap="round"/>
      <path d="M66 170 l4 -70 12 -26 -12 -8 10 -18 10 12 10 -14 10 14 10 -12 10 18 -12 8 12 26 4 70 z"
        fill="#33202e" stroke="#140a12" stroke-width="2"/>
      <circle cx="88" cy="56" r="4" fill="#ff3ba7"/><circle cx="104" cy="52" r="3" fill="#ff3ba7"/><circle cx="112" cy="60" r="2.4" fill="#ff3ba7"/>
      <path d="M86 96 l10 20 -8 18 M112 100 l-8 16 10 18" fill="none" stroke="#ff3ba7" stroke-width="1.8" opacity=".7"/>
      <path d="M58 104 l-20 24 8 4 6 -8 2 10 8 -6 z" fill="#4a2a40" stroke="#140a12" stroke-width="1.8"/>
      <rect x="80" y="166" width="14" height="18" rx="4" fill="#241420"/><rect x="106" y="166" width="14" height="18" rx="4" fill="#241420"/>
      </g>${flash}</svg>`;
    case 'voidlord': return `<svg viewBox="0 0 200 200" class="sprite-svg" id="enemy-svg">
      <defs><radialGradient id="voidcore" cx=".5" cy=".45" r=".6">
        <stop offset="0" stop-color="#c07fff"/><stop offset=".55" stop-color="#5a2f8f"/><stop offset="1" stop-color="#120a20"/></radialGradient></defs>
      <ellipse cx="100" cy="188" rx="40" ry="7" fill="#000" opacity=".4"/>
      <g class="hero-inner">
      <path d="M100 22 C58 44 70 116 48 176 C78 158 88 168 100 156 C112 168 122 158 152 176 C130 116 142 44 100 22 z"
        fill="#171226" stroke="#4a3f73" stroke-width="2"/>
      <path d="M76 40 l-18 -30 M92 34 l-6 -30 M108 34 l6 -30 M124 40 l18 -30" stroke="#171226" stroke-width="7" stroke-linecap="round"/>
      <path d="M76 40 l-18 -30 M124 40 l18 -30" stroke="#4a3f73" stroke-width="2" stroke-linecap="round" opacity=".7"/>
      <circle cx="100" cy="98" r="17" fill="url(#voidcore)"/>
      <circle cx="100" cy="98" r="24" fill="none" stroke="#9d86e0" stroke-width="1.2" opacity=".45"/>
      <path d="M84 58 l10 4 M116 58 l-10 4" stroke="#b9a4ee" stroke-width="3.4" stroke-linecap="round"/>
      <path d="M62 96 q-20 10 -24 34 M138 96 q20 10 24 34" fill="none" stroke="#4a3f73" stroke-width="4" opacity=".8"/>
      <circle class="wisp" cx="56" cy="140" r="3" fill="#9d86e0" opacity=".6"/>
      <circle class="wisp" cx="148" cy="120" r="2.6" fill="#c07fff" opacity=".6"/>
      </g>${flash}</svg>`;
  }
  return '';
}

// ── let: AC ──
// ---------- audio (tiny synth) ----------
let AC = null;

// ── function: beep ──
function beep(freq, dur, type, vol, slide){
  if (typeof G !== 'undefined' && G.muted) return;
  try {
    AC = AC || new (window.AudioContext || window.webkitAudioContext)();
    const o = AC.createOscillator(), g = AC.createGain();
    o.type = type || 'sine';
    o.frequency.setValueAtTime(freq, AC.currentTime);
    if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, freq + slide), AC.currentTime + dur);
    g.gain.setValueAtTime(vol || 0.07, AC.currentTime);
    g.gain.exponentialRampToValueAtTime(0.0001, AC.currentTime + dur);
    o.connect(g); g.connect(AC.destination);
    o.start(); o.stop(AC.currentTime + dur);
  } catch (e) { /* audio unavailable */ }
}

// ── function: sfx ──
function sfx(n){
  const m = {
    click:[520,.05,'square',.035,0], play:[320,.13,'triangle',.07,-140], hit:[150,.16,'sawtooth',.085,-70],
    block:[230,.1,'square',.055,0], heal:[430,.28,'sine',.06,240], gold:[880,.13,'triangle',.055,440],
    death:[95,.42,'sawtooth',.085,-45], turn:[250,.09,'sine',.045,60], win:[523,.55,'triangle',.075,262],
    lose:[115,.75,'sawtooth',.075,-60], boss:[72,.7,'sawtooth',.09,26], draw:[640,.05,'sine',.028,80],
    burn:[190,.14,'sawtooth',.05,-90], stun:[300,.2,'square',.05,-180]
  };
  const a = m[n]; if (a) beep(a[0], a[1], a[2], a[3], a[4]);
}

// ── function: shuffle ──
// ---------- small utils ----------
function shuffle(a){
  for (let i = a.length - 1; i > 0; i--){
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

// ── function: pick ──
function pick(a){ return a[Math.floor(Math.random() * a.length)]; }

// ── function: clamp ──
function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); }

// ── function: log ──
function log(msg, cls){
  const el = document.getElementById('log');
  if (!el) return;
  const d = document.createElement('div');
  d.className = 'l-new' + (cls ? ' ' + cls : '');
  d.innerHTML = msg;
  el.appendChild(d);
  while (el.children.length > 5) el.removeChild(el.firstChild);
  [...el.children].forEach(c => { if (c !== d) c.classList.remove('l-new'); });
}

// ── function: banner ──
function banner(text, sub){
  const b = document.getElementById('banner');
  b.innerHTML = text + (sub ? `<small>${sub}</small>` : '');
  b.classList.remove('hidden');
  b.style.animation = 'none'; void b.offsetWidth; b.style.animation = '';
  clearTimeout(banner._t);
  banner._t = setTimeout(() => b.classList.add('hidden'), 1550);
}

// ── function: floatAt ──
function floatAt(el, text, cls){
  if (!el) return;
  const arena = document.getElementById('arena');
  const r = el.getBoundingClientRect(), ar = arena.getBoundingClientRect();
  const f = document.createElement('div');
  f.className = 'floater ' + (cls || 'f-dmg');
  f.textContent = text;
  f.style.left = (r.left - ar.left + r.width / 2) + 'px';
  f.style.top = (r.top - ar.top + r.height * 0.22) + 'px';
  arena.appendChild(f);
  setTimeout(() => f.remove(), 1100);
}

// ── function: enemyEl ──
function enemyEl(i){ return document.getElementById('enemy-' + i); }

// ── function: heroEl ──
function heroEl(){ return document.getElementById('hero-sprite'); }

// ── function: hitAnim ──
function hitAnim(i){
  requestAnimationFrame(() => {
    const el = enemyEl(i);
    if (!el) return;
    el.classList.remove('hurt'); void el.offsetWidth; el.classList.add('hurt');
    setTimeout(() => el.classList.remove('hurt'), 420);
  });
}

// ── function: heroPose ──
function heroPose(pose){
  const el = heroEl(); if (!el) return;
  el.classList.remove('pose-attack', 'pose-hit');
  void el.offsetWidth;
  el.classList.add(pose);
  setTimeout(() => el.classList.remove(pose), pose === 'pose-attack' ? 260 : 340);
}

// ── const: G ──
// ---------- game state & flow ----------
const G = {
  cls: null, hero: null, deck: [], owned: {},
  drawPile: [], discard: [], hand: [],
  floor: 1, room: 1, enemies: [], turn: 1,
  phase: 'title', targeting: null, pendingDiscard: null,
  muted: false, kills: 0, goldEarned: 0
};

// ── function: newGame ──
function newGame(clsKey){
  const c = CLASSES[clsKey];
  G.cls = clsKey;
  G.hero = {
    hp: c.hp, maxhp: c.hp, energy: c.energy, maxenergy: c.energy,
    handSize: c.handSize, gold: 0, block: 0, perm: 0, dodge: 0, ghost: 0
  };
  G.deck = starterDeck(clsKey);
  G.owned = {};
  G.deck.forEach(id => G.owned[id] = (G.owned[id] || 0) + 1);
  G.floor = 1; G.room = 1; G.kills = 0; G.goldEarned = 0;
  G.muted = G.muted;
  document.getElementById('screen-class').classList.add('hidden');
  document.getElementById('screen-title').classList.add('hidden');
  document.getElementById('screen-game').classList.remove('hidden');
  log(`<b>${c.name}</b> descends into Aethelgard…`, 'l-good');
  startRoom();
}

// ── function: makeEnemy ──
function makeEnemy(type, name, hp, dmg, boss){
  return { type, name, hp, maxhp: hp, dmg, def: 0,
    burn: 0, poison: 0, stun: 0, silence: 0, atkDown: 0,
    boss: !!boss, dead: false };
}

// ── function: startRoom ──
function startRoom(){
  G.enemies = [];
  const isBoss = G.room === 4;
  if (isBoss){
    const b = BOSSES[G.floor];
    G.enemies.push(makeEnemy(b.type, b.name, b.hp, b.dmg, true));
  } else {
    const count = G.room === 1 ? 1 : (G.room === 2 ? (Math.random() < 0.5 ? 1 : 2) : 2);
    const pool = ENEMY_POOL[G.floor];
    const st = FLOOR_STATS[G.floor];
    for (let i = 0; i < count; i++){
      const e = pick(pool);
      G.enemies.push(makeEnemy(e.type, pick(e.names), st.hp, st.dmg, false));
    }
  }
  // fresh piles
  G.drawPile = shuffle([...G.deck]);
  G.discard = [];
  G.hand = [];
  G.turn = 1;
  G.targeting = null; G.pendingDiscard = null;
  G.hero.block = 0; G.hero.dodge = 0; G.hero.ghost = 0;
  G.hero.energy = G.hero.maxenergy;
  G.phase = 'player';
  hideOverlay();
  if (isBoss){ sfx('boss'); banner('⚔ ' + G.enemies[0].name + ' ⚔', BOSSES[G.floor].note); }
  else banner(`FLOOR ${ROMAN[G.floor]} · ROOM ${G.room}`, 'Slay all foes');
  drawUp();
  renderAll();
}

// ── function: startPlayerTurn ──
function startPlayerTurn(){
  G.turn++;
  const h = G.hero;
  if (G.turn > 1) h.energy = Math.min(h.maxenergy, h.energy + 1);
  h.block = 0;                       // temporary block fades
  h.ghost = 0;                       // ghost form expires
  // boss 1 ward refresh
  const b = G.enemies.find(e => e.boss && !e.dead && e.type === 'gatekeeper');
  if (b) b.def = b.hp >= 20 ? 5 : 0;
  G.phase = 'player';
  drawUp();
  sfx('turn');
  banner('YOUR TURN', `Energy ${h.energy}/${h.maxenergy}`);
  renderAll();
}

// ── function: drawUp ──
function drawUp(){
  const h = G.hero;
  let drew = 0;
  while (G.hand.length < h.handSize){
    if (!G.drawPile.length){
      if (!G.discard.length) break;
      G.drawPile = shuffle(G.discard);
      G.discard = [];
      log('The discard pile is reshuffled.');
    }
    G.hand.push(G.drawPile.pop());
    drew++;
  }
  if (drew) sfx('draw');
}

// ── function: livingEnemies ──
function livingEnemies(){ return G.enemies.filter(e => !e.dead); }

// ── function: checkRoomEnd ──
function checkRoomEnd(){
  if (livingEnemies().length === 0 && G.phase !== 'over'){
    G.phase = 'over';
    setTimeout(roomCleared, 800);
    return true;
  }
  return false;
}

// ── function: roomCleared ──
function roomCleared(){
  const isBoss = G.room === 4;
  sfx('win');
  if (isBoss && G.floor === 3){ showVictory(); return; }
  if (isBoss){
    G.hero.hp = G.hero.maxhp;
    showFloorClear();
  } else {
    const heal = 4;
    G.lastHeal = heal;
    G.hero.hp = Math.min(G.hero.maxhp, G.hero.hp + heal);
    showRoomWin(heal);
  }
  renderAll();
}

// ── function: advanceRoom ──
function advanceRoom(){
  if (G.room === 4){ G.floor++; G.room = 1; }
  else G.room++;
  hideOverlay();
  startRoom();
}

// ── function: canPlay ──
// ---------- card play & effects ----------
function canPlay(card){
  return G.phase === 'player' && !G.pendingDiscard && G.hero.energy >= card.cost;
}

// ── function: handCardClick ──
function handCardClick(idx){
  if (G.phase !== 'player') return;
  const id = G.hand[idx];
  if (!id) return;
  // discard-selection mode (Iron Skin / Hide / Search)
  if (G.pendingDiscard){
    const pd = G.pendingDiscard;
    G.discard.push(G.hand.splice(idx, 1)[0]);
    pd.count--;
    sfx('click');
    log(`Discarded <b>${cardById(id).name}</b>.`);
    if (pd.count <= 0){
      G.pendingDiscard = null;
      if (pd.drawAfter){ drawCards(pd.drawAfter); }
    }
    renderAll();
    return;
  }
  const card = cardById(id);
  if (!canPlay(card)){ sfx('click'); return; }
  const targets = livingEnemies();
  if (needsTarget(card) && targets.length > 1){
    G.targeting = (G.targeting === idx) ? null : idx;
    sfx('click');
    renderAll();
    return;
  }
  resolveCard(idx, needsTarget(card) ? G.enemies.indexOf(targets[0]) : -1);
}

// ── function: drawCards ──
function drawCards(n){
  for (let i = 0; i < n; i++){
    if (!G.drawPile.length){
      if (!G.discard.length) break;
      G.drawPile = shuffle(G.discard); G.discard = [];
    }
    G.hand.push(G.drawPile.pop());
  }
  sfx('draw');
}

// ── function: resolveCard ──
function resolveCard(handIdx, targetIdx){
  const id = G.hand[handIdx];
  const card = cardById(id);
  G.hand.splice(handIdx, 1);
  G.hero.energy -= card.cost;
  G.targeting = null;
  sfx('play');
  log(`Played <b>${card.name}</b>.`);
  if (card.type === 'attack') heroPose('pose-attack');
  applyFx(card, targetIdx >= 0 ? G.enemies[targetIdx] : null);
  G.discard.push(id);
  if (card.fx.slamDraw && G.hero.hp >= 30){ drawCards(1); log('Slam momentum — draw 1 card.', 'l-good'); }
  if (card.fx.searchCap && G.hand.length > 5){
    G.pendingDiscard = { count: G.hand.length - 5, drawAfter: 0 };
    log('Hand too large — discard down to 5.');
  }
  if (card.fx.discardN){
    if (G.hand.length > 0){
      G.pendingDiscard = { count: Math.min(card.fx.discardN, G.hand.length), drawAfter: card.fx.drawAfterDiscard || 0 };
      log(`Choose ${G.pendingDiscard.count} card(s) to discard.`);
    } else if (card.fx.drawAfterDiscard){ drawCards(card.fx.drawAfterDiscard); }
  }
  renderAll();
  checkRoomEnd();
}

// ── function: applyFx ──
function applyFx(card, target){
  const f = card.fx, h = G.hero;
  const tEl = target ? enemyEl(G.enemies.indexOf(target)) : null;
  // self effects
  if (f.block){ h.block += f.block; floatAt(heroEl(), '+' + f.block + ' 🛡', 'f-block'); sfx('block'); }
  if (f.soak){ h.block += f.soak; }
  if (f.perm){ h.perm += f.perm; floatAt(heroEl(), '+' + f.perm + ' 🛡 permanent', 'f-block'); sfx('block'); }
  if (f.heal){
    const amt = Math.min(f.heal, h.maxhp - h.hp);
    h.hp += amt;
    if (amt > 0){ floatAt(heroEl(), '+' + amt, 'f-heal'); sfx('heal'); }
  }
  if (f.energy){
    h.energy = Math.min(h.maxenergy, h.energy + f.energy);
    floatAt(heroEl(), '+' + f.energy + ' ⚡', 'f-energy');
  }
  if (f.gold){ h.gold += f.gold; G.goldEarned += f.gold; floatAt(heroEl(), '+' + f.gold + ' gold', 'f-gold'); sfx('gold'); }
  if (f.dodge){ h.dodge += f.dodge; floatAt(heroEl(), 'DODGE READY', 'f-status'); }
  if (f.ghost){ h.ghost = 1; floatAt(heroEl(), 'GHOST FORM', 'f-status'); }
  if (f.draw) drawCards(f.draw);
  // AoE
  if (f.dmgAll) livingEnemies().forEach(e => damageEnemy(e, f.dmgAll, {}));
  if (f.burnAll) livingEnemies().forEach(e => {
    e.burn += f.burnAll;
    floatAt(enemyEl(G.enemies.indexOf(e)), '🔥 ' + f.burnAll, 'f-status');
  });
  if (f.stunAll) livingEnemies().forEach(e => {
    e.stun += f.stunAll;
    floatAt(enemyEl(G.enemies.indexOf(e)), '💤 STUN', 'f-status');
  });
  // single target
  if (target && !target.dead){
    if (f.dmg){
      let amt = f.dmg;
      if (f.backstab && target.def <= 0) amt += f.backstab;
      damageEnemy(target, amt, { unblockable: !!f.unblockable });
    }
    if (f.burn){ target.burn += f.burn; floatAt(tEl, '🔥 ' + f.burn, 'f-status'); sfx('burn'); }
    if (f.poison){ target.poison += f.poison; floatAt(tEl, '☠ ' + f.poison, 'f-status'); }
    if (f.stun){ target.stun += f.stun; floatAt(tEl, '💤 STUN', 'f-status'); sfx('stun'); }
    if (f.silence){ target.silence += f.silence; floatAt(tEl, 'SILENCED', 'f-status'); sfx('stun'); }
    if (f.atkDown){ target.atkDown += f.atkDown; floatAt(tEl, '-' + f.atkDown + ' ATK', 'f-status'); }
  }
}

// ── function: damageEnemy ──
function damageEnemy(e, amt, opts){
  if (!e || e.dead) return;
  const idx = G.enemies.indexOf(e);
  let dealt = amt;
  if (!opts.unblockable && e.def > 0){
    const absorbed = Math.min(e.def, dealt);
    e.def -= absorbed; dealt -= absorbed;
    if (absorbed > 0) floatAt(enemyEl(idx), 'WARD -' + absorbed, 'f-block');
  }
  e.hp -= dealt;
  floatAt(enemyEl(idx), dealt > 0 ? String(dealt) : '0', 'f-dmg');
  sfx('hit');
  renderEnemies(); renderTop();
  hitAnim(idx);
  if (e.hp <= 0){
    e.hp = 0; e.dead = true;
    const reward = e.boss ? 15 : 5;
    G.hero.gold += reward; G.goldEarned += reward; G.kills++;
    log(`<b>${e.name}</b> is slain. <b>+${reward} gold</b>.`, 'l-gold');
    sfx('death');
    floatAt(enemyEl(idx), '+' + reward + ' GOLD', 'f-gold');
    renderAll();
  }
}

// ── function: damagePlayer ──
function damagePlayer(raw, source, opts){
  const h = G.hero;
  if (h.ghost > 0){
    floatAt(heroEl(), 'PHASED', 'f-miss');
    log(`${source}'s blow passes through your ghost form.`, 'l-good');
    return;
  }
  if (h.dodge > 0 && !(opts && opts.unblockable)){
    h.dodge--;
    floatAt(heroEl(), 'DODGED', 'f-miss');
    log(`You dodge ${source}'s attack.`, 'l-good');
    return;
  }
  let dmg = raw;
  if (!(opts && opts.unblockable)){
    const eff = h.block + h.perm;
    const absorbed = Math.min(eff, dmg);
    // consume temporary block first
    const fromBlock = Math.min(h.block, absorbed);
    h.block -= fromBlock;
    const fromPerm = absorbed - fromBlock;
    h.perm -= fromPerm;
    dmg -= absorbed;
    if (absorbed > 0){ floatAt(heroEl(), 'BLOCKED -' + absorbed, 'f-block'); sfx('block'); }
  }
  if (dmg > 0){
    h.hp -= dmg;
    floatAt(heroEl(), '-' + dmg, 'f-dmg');
    sfx('hit');
    heroPose('pose-hit');
    log(`${source} hits you for <b>${dmg}</b>.`, 'l-dmg');
  }
  renderAll();
  if (h.hp <= 0){
    h.hp = 0;
    G.phase = 'over';
    sfx('lose');
    setTimeout(showGameOver, 900);
  }
}

// ── function: endTurn ──
// ---------- enemy phase & statuses ----------
function endTurn(){
  if (G.phase !== 'player' || G.pendingDiscard) return;
  G.targeting = null;
  G.phase = 'enemy';
  renderAll();
  banner('ENEMY TURN', 'Brace yourself');
  setTimeout(enemyPhase, 650);
}

// ── function: enemyPhase ──
function enemyPhase(){
  if (G.phase !== 'enemy') return;
  // 1) damage-over-time ticks
  for (const e of livingEnemies()){
    let ticked = false;
    if (e.burn > 0){
      e.hp -= e.burn;
      floatAt(enemyEl(G.enemies.indexOf(e)), '🔥' + e.burn, 'f-dmg');
      log(`${e.name} suffers <b>${e.burn}</b> burn damage.`, 'l-dmg');
      e.burn = Math.max(0, e.burn - 1);
      ticked = true;
    }
    if (e.poison > 0 && !e.dead){
      e.hp -= e.poison;
      floatAt(enemyEl(G.enemies.indexOf(e)), '☠' + e.poison, 'f-dmg');
      log(`${e.name} suffers <b>${e.poison}</b> poison damage.`, 'l-dmg');
      e.poison = Math.max(0, e.poison - 1);
      ticked = true;
    }
    if (ticked){ sfx('burn'); renderEnemies(); }
    if (e.hp <= 0 && !e.dead){
      e.hp = 0; e.dead = true;
      const reward = e.boss ? 15 : 5;
      G.hero.gold += reward; G.goldEarned += reward; G.kills++;
      log(`<b>${e.name}</b> succumbs to its wounds. <b>+${reward} gold</b>.`, 'l-gold');
      sfx('death');
    }
  }
  renderAll();
  if (checkRoomEnd()) return;
  // 2) enemy actions, one at a time
  const actors = livingEnemies();
  let i = 0;
  const step = () => {
    if (G.phase !== 'enemy') return;
    if (i >= actors.length){
      setTimeout(() => { if (G.phase === 'enemy') startPlayerTurn(); }, 420);
      return;
    }
    const e = actors[i++];
    if (e.dead){ step(); return; }
    enemyAct(e);
    renderAll();
    if (G.phase !== 'enemy') return;   // player died
    setTimeout(step, 620);
  };
  step();
}

// ── function: enemyAct ──
function enemyAct(e){
  const el = enemyEl(G.enemies.indexOf(e));
  if (e.stun > 0){
    e.stun--;
    floatAt(el, '💤 STUNNED', 'f-miss');
    log(`${e.name} is stunned and cannot act.`, 'l-good');
    return;
  }
  // boss specials (silence seals them)
  let dmg = Math.max(0, e.dmg - e.atkDown);
  let pierce = false;
  if (e.boss && e.silence > 0){
    e.silence--;
    floatAt(el, 'SILENCED', 'f-status');
    log(`${e.name}'s dark gift is sealed.`, 'l-good');
  } else if (e.boss){
    if (e.type === 'corrupted' && G.hero.energy < 3){
      dmg = 20;
      floatAt(el, 'CORRUPT BURST', 'f-status');
      log('The Corrupted feasts on your weakness — <b>20 damage incoming!</b>', 'l-dmg');
    }
    if (e.type === 'voidlord'){
      e.hp = Math.min(e.maxhp, e.hp + 10);
      pierce = true;
      floatAt(el, '+10 REGEN', 'f-heal');
      log('The Void Lord regenerates <b>10 HP</b>.', 'l-dmg');
    }
  }
  // attack animation
  if (el){ el.style.transform = 'translateX(-26px)'; setTimeout(() => el.style.transform = '', 200); }
  log(`${e.name} attacks for <b>${dmg}</b>${pierce ? ' (pierces Defense)' : ''}.`);
  damagePlayer(dmg, e.name, { unblockable: pierce });
}

// ── function: showGameOver ──
function showGameOver(){
  sfx('lose');
  showOverlay(`
    <h2 class="death-h">YOU HAVE FALLEN</h2>
    <p class="ov-sub">The Void claims another soul. Aethelgard dims.</p>
    <div class="ov-big-stat">Reached Floor ${ROMAN[G.floor]} · Room ${Math.min(G.room, 3)}${G.room === 4 ? ' (Boss)' : ''}</div>
    <div class="ov-big-stat">Foes slain: ${G.kills} &nbsp;·&nbsp; Gold earned: ◈ ${G.goldEarned}</div>
    <div class="ov-actions">
      <button class="btn btn-big" data-action="to-class">Rise Again</button>
      <button class="btn" data-action="to-title">Main Menu</button>
    </div>`);
}

// ── function: renderAll ──
// ---------- rendering ----------
function renderAll(){
  renderTop(); renderHero(); renderEnemies(); renderHand(); renderPanel();
  const hint = document.getElementById('target-hint');
  hint.classList.toggle('hidden', G.targeting === null);
  document.getElementById('btn-endturn').disabled =
    G.phase !== 'player' || !!G.pendingDiscard;
}

// ── function: renderTop ──
function renderTop(){
  const c = CLASSES[G.cls];
  document.getElementById('tb-class').innerHTML = c ? `${c.name}` : '';
  const isBoss = G.room === 4;
  document.getElementById('tb-floor').textContent =
    `FLOOR ${ROMAN[G.floor]} · ${isBoss ? 'BOSS' : 'ROOM ' + G.room + ' / 3'}`;
  let pips = '';
  for (let i = 1; i <= 3; i++) pips += i < G.room ? '●' : (i === G.room && !isBoss ? '◉' : '○');
  pips += isBoss ? ' 👑' : ' ♛';
  document.getElementById('tb-pips').textContent = pips;
  document.getElementById('tb-gold').textContent = G.hero ? G.hero.gold : 0;
  const boss = G.enemies.find(e => e.boss);
  const tb = document.getElementById('tb-boss');
  if (boss && !boss.dead){
    tb.classList.remove('hidden');
    tb.textContent = `👑 ${boss.name} ${boss.hp}/${boss.maxhp}`;
  } else tb.classList.add('hidden');
}

// ── function: statusChips ──
function statusChips(e){
  let s = '';
  if (e.def > 0) s += `<span class="chip block">🛡 ${e.def}</span>`;
  if (e.burn > 0) s += `<span class="chip burn">🔥 ${e.burn}</span>`;
  if (e.poison > 0) s += `<span class="chip poison">☠ ${e.poison}</span>`;
  if (e.stun > 0) s += `<span class="chip stun">💤 ${e.stun}</span>`;
  if (e.silence > 0) s += `<span class="chip silence">✖ sealed</span>`;
  if (e.atkDown > 0) s += `<span class="chip poison">−${e.atkDown} atk</span>`;
  return s;
}

// ── function: intentFor ──
function intentFor(e){
  if (e.dead) return '';
  if (e.stun > 0) return `<span class="i-stun">💤 STUNNED</span>`;
  let dmg = Math.max(0, e.dmg - e.atkDown);
  let extra = '';
  if (e.boss && e.silence <= 0){
    if (e.type === 'corrupted' && G.hero.energy < 3){ dmg = 20; extra = ' ⚡'; }
    if (e.type === 'voidlord') extra = ' ⛨✕';
  }
  return `⚔ ${dmg}${extra}`;
}

// ── function: renderEnemies ──
function renderEnemies(){
  const zone = document.getElementById('enemy-zone');
  zone.innerHTML = G.enemies.map((e, i) => `
    <div class="enemy idle-bob ${e.boss ? 'boss-foe' : ''} ${e.dead ? 'dead' : ''} ${G.targeting !== null && !e.dead ? 'targetable' : ''}"
         id="enemy-${i}" data-idx="${i}">
      ${e.boss && !e.dead ? '<div class="boss-crown">👑</div>' : ''}
      ${!e.dead ? `<div class="intent">${intentFor(e)}</div>` : ''}
      <div class="sprite-wrap">${enemySVG(e.type)}</div>
      <div class="hpwrap">
        <div class="hpname">${e.name}</div>
        <div class="hpbar"><div class="hpfill" style="width:${(e.hp / e.maxhp) * 100}%"></div></div>
        <div class="hpnum">${e.hp} / ${e.maxhp}</div>
        <div class="chips">${statusChips(e)}</div>
      </div>
    </div>`).join('');
}

// ── function: renderHero ──
function renderHero(){
  const h = G.hero, c = CLASSES[G.cls];
  let chips = '';
  const effBlock = h.block + h.perm;
  if (effBlock > 0) chips += `<span class="chip block">🛡 ${effBlock}${h.perm ? ' (' + h.perm + '⛨)' : ''}</span>`;
  if (h.dodge > 0) chips += `<span class="chip dodge">💨 dodge ×${h.dodge}</span>`;
  if (h.ghost > 0) chips += `<span class="chip ghost">👻 ghost</span>`;
  document.getElementById('hero-zone').innerHTML = `
    <div class="hero-sprite-wrap" id="hero-sprite">${heroSVG(G.cls)}</div>
    <div class="hpwrap hero-hp">
      <div class="hpname" style="color:${c.color}">${c.name}</div>
      <div class="hpbar"><div class="hpfill" style="width:${(h.hp / h.maxhp) * 100}%"></div></div>
      <div class="hpnum">${h.hp} / ${h.maxhp} HP</div>
      <div class="chips">${chips}</div>
    </div>`;
}

// ── function: renderHand ──
function renderHand(){
  const hand = document.getElementById('hand');
  hand.innerHTML = G.hand.map((id, i) => {
    const card = cardById(id);
    const playable = canPlay(card);
    const cls = ['hcard',
      playable ? 'playable' : 'cant',
      G.targeting === i ? 'selected' : '',
      G.pendingDiscard ? 'pick-discard' : ''].join(' ');
    return `<div class="${cls}" data-idx="${i}" style="z-index:${10 + i}" title="${card.name}">${cardFaceSVG(card)}</div>`;
  }).join('');
}

// ── function: renderPanel ──
function renderPanel(){
  const h = G.hero;
  document.getElementById('energy-box').innerHTML = `
    <div class="e-label">ENERGY</div>
    <div id="energy-num">${h.energy}<small> / ${h.maxenergy}</small></div>
    <div class="e-pips">${Array.from({length: h.maxenergy}, (_, i) =>
      `<span class="e-pip ${i < h.energy ? 'on' : ''}"></span>`).join('')}</div>`;
  document.getElementById('pile-box').innerHTML = `
    <div class="pile"><span class="mini-back">${cardBackSVG(G.cls)}</span>
      <div class="p-num">${G.drawPile.length}</div><div class="p-label">Draw</div></div>
    <div class="pile"><span class="mini-back" style="filter:grayscale(1) brightness(.7)">${cardBackSVG(G.cls)}</span>
      <div class="p-num">${G.discard.length}</div><div class="p-label">Discard</div></div>`;
}

// ── function: showOverlay ──
// ---------- overlays & screens ----------
function showOverlay(html){
  document.getElementById('overlay-panel').innerHTML = html;
  document.getElementById('overlay').classList.remove('hidden');
}

// ── function: hideOverlay ──
function hideOverlay(){ document.getElementById('overlay').classList.add('hidden'); }

// ── function: showClassSelect ──
function showClassSelect(){
  document.getElementById('screen-title').classList.add('hidden');
  document.getElementById('screen-game').classList.add('hidden');
  hideOverlay();
  const row = document.getElementById('class-row');
  row.innerHTML = Object.values(CLASSES).map(c => `
    <div class="class-card" data-class="${c.key}" style="--cls-color:${c.color}">
      <div class="cls-back">${cardBackSVG(c.key)}</div>
      <div class="cls-hero">${heroSVG(c.key)}</div>
      <h3>${c.name}</h3>
      <div class="cls-title">${c.title}</div>
      <p class="cls-desc">${c.desc}</p>
      <div class="cls-stats"><span>HP <b>${c.hp}</b></span><span>Energy <b>${c.energy}</b></span><span>Hand <b>${c.handSize}</b></span></div>
      <button class="btn">Choose ${c.name.replace('The ', 'the ')}</button>
    </div>`).join('');
  document.getElementById('screen-class').classList.remove('hidden');
}

// ── function: openShop ──
function openShop(){
  if (!G.hero) return;
  sfx('click');
  // stock: 3 distinct cards with copies remaining
  const avail = CARDS.filter(c => (G.owned[c.id] || 0) < 5);
  shuffle(avail);
  G.shopStock = avail.slice(0, 3).map(c => c.id);
  renderShop();
}

// ── function: renderShop ──
function renderShop(){
  const h = G.hero;
  const slots = G.shopStock.map(id => {
    const c = cardById(id);
    const owned = G.owned[id] || 0;
    const price = goldPrice(id, owned);
    const afford = h.gold >= price;
    const maxed = owned >= 5;
    return `<div class="shop-slot">
      <div class="shop-card">${cardFaceSVG(c)}</div>
      <div class="shop-price">${price}</div>
      <div class="shop-owned">OWNED ${owned} / 5 ${owned >= 3 ? '· premium' : ''}</div>
      <button class="btn" data-action="buy" data-id="${id}" ${!afford || maxed ? 'disabled' : ''}>
        ${maxed ? 'Maxed Out' : afford ? 'Buy Card' : 'Too Poor'}</button>
    </div>`;
  }).join('');
  showOverlay(`
    <h2>⚖ THE WANDERING MERCHANT</h2>
    <p class="ov-sub">"Relics for the descent, traveler. Gold now — ◈ <b style="color:var(--gold-hi)">${h.gold}</b> — prices rise with every copy you hoard."</p>
    <div class="ov-row">${slots}</div>
    <div class="ov-actions">
      <button class="btn btn-big" data-action="close-shop">${G.phase === 'over' ? 'Back' : 'Return to Battle'}</button>
    </div>`);
}

// ── function: buyCard ──
function buyCard(id){
  const h = G.hero;
  const owned = h ? (G.owned[id] || 0) : 0;
  const price = goldPrice(id, owned);
  if (owned >= 5 || h.gold < price) return;
  h.gold -= price;
  G.owned[id] = owned + 1;
  G.deck.push(id);
  G.discard.push(id);   // joins the deck via discard pile
  sfx('gold');
  log(`Bought <b>${cardById(id).name}</b> for ◈ ${price}.`, 'l-gold');
  renderShop();
  renderAll();
}

// ── function: showRoomWin ──
function showRoomWin(heal){
  showOverlay(`
    <h2>ROOM CLEARED</h2>
    <p class="ov-sub">The shadows recoil… for now.</p>
    <div class="reward-line">You catch your breath: <b>+${heal} HP</b> (now ${G.hero.hp}/${G.hero.maxhp})</div>
    <div class="reward-line">Purse: <b>◈ ${G.hero.gold}</b></div>
    <div class="ov-actions">
      <button class="btn" data-action="open-shop">⚖ Visit Shop</button>
      <button class="btn btn-big" data-action="next-room">${G.room === 3 ? 'Face the Boss 👑' : 'Next Room →'}</button>
    </div>`);
}

// ── function: showFloorClear ──
function showFloorClear(){
  showOverlay(`
    <h2 class="victory-h">FLOOR ${ROMAN[G.floor]} CLEARED</h2>
    <p class="ov-sub">${BOSSES[G.floor].name} crumbles to dust. The descent continues.</p>
    <div class="reward-line">Your wounds knit closed: <b>fully healed</b> (${G.hero.hp}/${G.hero.maxhp})</div>
    <div class="reward-line">Purse: <b>◈ ${G.hero.gold}</b></div>
    <div class="ov-actions">
      <button class="btn" data-action="open-shop">⚖ Visit Shop</button>
      <button class="btn btn-big" data-action="next-room">Descend to Floor ${ROMAN[G.floor + 1]} ↓</button>
    </div>`);
}

// ── function: showVictory ──
function showVictory(){
  showOverlay(`
    <h2 class="victory-h">✦ AETHELGARD RECLAIMED ✦</h2>
    <p class="ov-sub">The Void Lord shatters like black glass. Dawn returns to the realm —<br>the ${CLASSES[G.cls].name} walks out of the dark, unbowed.</p>
    <div class="ov-big-stat">Foes slain: ${G.kills} · Gold earned: ◈ ${G.goldEarned} · HP remaining: ${G.hero.hp}</div>
    <div class="ov-actions">
      <button class="btn btn-big" data-action="to-class">Play Again</button>
      <button class="btn" data-action="to-title">Main Menu</button>
    </div>`);
  sfx('win');
}

// ── function: toTitle ──
function toTitle(){
  hideOverlay();
  document.getElementById('screen-class').classList.add('hidden');
  document.getElementById('screen-game').classList.add('hidden');
  document.getElementById('screen-title').classList.remove('hidden');
  G.phase = 'title';
}

// ── function: initParticles ──
// ---------- ambient particles (embers & ash) ----------
function initParticles(){
  const cv = document.getElementById('fx-bg');
  const ctx = cv.getContext('2d');
  let W, H;
  const resize = () => { W = cv.width = window.innerWidth; H = cv.height = window.innerHeight; };
  resize();
  window.addEventListener('resize', resize);
  const parts = Array.from({length: 64}, () => ({
    x: Math.random(), y: Math.random(),
    r: 0.6 + Math.random() * 2.1,
    vy: -(0.06 + Math.random() * 0.22) / 100,
    vx: (Math.random() - 0.5) * 0.02 / 100,
    sway: Math.random() * Math.PI * 2,
    ember: Math.random() < 0.4,
    a: 0.15 + Math.random() * 0.5
  }));
  (function tick(){
    ctx.clearRect(0, 0, W, H);
    for (const p of parts){
      p.sway += 0.012;
      p.x += p.vx + Math.sin(p.sway) * 0.0004;
      p.y += p.vy;
      if (p.y < -0.02){ p.y = 1.02; p.x = Math.random(); }
      if (p.x < -0.02) p.x = 1.02; if (p.x > 1.02) p.x = -0.02;
      const tw = 0.65 + 0.35 * Math.sin(p.sway * 2.3);
      ctx.beginPath();
      ctx.arc(p.x * W, p.y * H, p.r, 0, Math.PI * 2);
      ctx.fillStyle = p.ember
        ? `rgba(224,150,60,${(p.a * tw).toFixed(3)})`
        : `rgba(150,140,160,${(p.a * 0.5 * tw).toFixed(3)})`;
      ctx.fill();
    }
    requestAnimationFrame(tick);
  })();
}

// ── main ──
// ---------- startup & event wiring ----------
initParticles();

const $ = id => document.getElementById(id);

$('btn-start').addEventListener('click', () => { sfx('click'); showClassSelect(); });
$('btn-how').addEventListener('click', () => { sfx('click'); $('howto').classList.remove('hidden'); });
$('btn-how-close').addEventListener('click', () => { sfx('click'); $('howto').classList.add('hidden'); });
$('btn-class-back').addEventListener('click', () => { sfx('click'); toTitle(); });
$('btn-endturn').addEventListener('click', () => { sfx('click'); endTurn(); });
$('btn-shop').addEventListener('click', () => {
  if (!G.hero || G.phase === 'enemy') return;
  G.returnTo = null;
  openShop();
});
$('btn-sound').addEventListener('click', () => {
  G.muted = !G.muted;
  $('btn-sound').textContent = G.muted ? '✕' : '♪';
  if (!G.muted) sfx('click');
});

$('class-row').addEventListener('click', e => {
  const card = e.target.closest('.class-card');
  if (!card) return;
  sfx('play');
  newGame(card.dataset.class);
});

$('hand').addEventListener('click', e => {
  const el = e.target.closest('.hcard');
  if (!el) return;
  handCardClick(parseInt(el.dataset.idx, 10));
});

$('enemy-zone').addEventListener('click', e => {
  const el = e.target.closest('.enemy');
  if (!el || G.targeting === null) return;
  const idx = parseInt(el.dataset.idx, 10);
  if (G.enemies[idx] && !G.enemies[idx].dead) resolveCard(G.targeting, idx);
});

$('arena').addEventListener('contextmenu', e => {
  if (G.targeting !== null){
    e.preventDefault();
    G.targeting = null;
    renderAll();
  }
});

$('overlay').addEventListener('click', e => {
  const b = e.target.closest('[data-action]');
  if (!b) return;
  const a = b.dataset.action;
  sfx('click');
  if (a === 'buy') buyCard(b.dataset.id);
  else if (a === 'close-shop'){
    if (G.returnTo === 'room') showRoomWin(G.lastHeal || 4);
    else if (G.returnTo === 'floor') showFloorClear();
    else hideOverlay();
  }
  else if (a === 'open-shop'){ G.returnTo = (G.room === 4 ? 'floor' : 'room'); openShop(); }
  else if (a === 'next-room'){ G.returnTo = null; advanceRoom(); }
  else if (a === 'to-class'){ G.returnTo = null; showClassSelect(); }
  else if (a === 'to-title'){ G.returnTo = null; toTitle(); }
});

document.addEventListener('keydown', e => {
  if (e.key === 'Escape'){
    if (!$('howto').classList.contains('hidden')){ $('howto').classList.add('hidden'); return; }
    if (G.targeting !== null){ G.targeting = null; renderAll(); return; }
    if (!$('overlay').classList.contains('hidden') && G.returnTo === null &&
        G.phase !== 'over'){ hideOverlay(); }
  }
  if ((e.key === ' ' || e.key === 'Enter') &&
      !$('screen-game').classList.contains('hidden') &&
      $('overlay').classList.contains('hidden')){
    e.preventDefault();
    endTurn();
  }
});
</script>
</body>
</html>