* { box-sizing: border-box; }
html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #2b3a4a;
  font-family: 'Segoe UI', 'Baloo 2', system-ui, -apple-system, sans-serif;
  user-select: none;
}

#world {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  cursor: pointer;
}

/* ---------- Bottom bar ---------- */
#bottomBar {
  position: fixed;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  width: min(720px, 94vw);
  height: 56px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 10px;
  background: linear-gradient(#5b7fa6, #3c5c80);
  border: 3px solid #274a68;
  border-radius: 16px;
  box-shadow: 0 6px 0 rgba(0,0,0,0.25), 0 10px 24px rgba(0,0,0,0.35);
  z-index: 20;
}
#bottomBar.hidden { display: none; }

#chatInput {
  flex: 1;
  height: 38px;
  border-radius: 19px;
  border: none;
  outline: none;
  padding: 0 16px;
  font-size: 15px;
  background: #fff;
  color: #24313f;
}
#chatInput::placeholder { color: #8a97a5; }

.bar-icon-btn {
  flex: 0 0 auto;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: none;
  background: #f2c94c;
  font-size: 19px;
  cursor: pointer;
  box-shadow: 0 3px 0 #b8901f;
  transition: transform 0.06s ease;
}
.bar-icon-btn:active { transform: translateY(2px); box-shadow: 0 1px 0 #b8901f; }

/* ---------- Login overlay ---------- */
#loginOverlay {
  position: fixed;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 40%, #6fa6d6, #2b3a4a 80%);
  z-index: 30;
}
#loginOverlay.visible { display: flex; }

.login-card {
  background: #fffef8;
  border-radius: 20px;
  padding: 34px 38px 30px;
  width: 320px;
  text-align: center;
  box-shadow: 0 14px 0 rgba(0,0,0,0.15), 0 20px 40px rgba(0,0,0,0.35);
}
.login-title {
  font-size: 30px;
  font-weight: 800;
  color: #2f6690;
  margin-bottom: 4px;
}
.login-sub {
  font-size: 13px;
  color: #7a8794;
  margin-bottom: 20px;
}
#nameInput, #passwordInput {
  width: 100%;
  height: 44px;
  border-radius: 12px;
  border: 2px solid #d7dee5;
  outline: none;
  font-size: 16px;
  padding: 0 14px;
  margin-bottom: 6px;
  text-align: center;
}
#nameInput:focus, #passwordInput:focus { border-color: #56b4e9; }

.color-picker {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin: 14px 0 6px;
}
.color-swatch {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 3px solid transparent;
  cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.12);
  padding: 0;
}
.color-swatch.selected {
  border-color: #2f6690;
  box-shadow: 0 0 0 2px #fff, inset 0 0 0 1px rgba(0,0,0,0.12);
}
#loginError {
  color: #e05a5a;
  font-size: 12.5px;
  min-height: 18px;
  margin-bottom: 6px;
}
#joinBtn {
  width: 100%;
  height: 46px;
  border-radius: 12px;
  border: none;
  background: #f2c94c;
  color: #4a3400;
  font-size: 17px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 0 #b8901f;
  margin-top: 8px;
}
#joinBtn:active { transform: translateY(3px); box-shadow: 0 1px 0 #b8901f; }
#joinBtn:disabled { opacity: 0.6; cursor: default; }

.panel-close-btn {
  width: 100%;
  height: 40px;
  border-radius: 12px;
  border: none;
  background: #e4ebf1;
  color: #3a4653;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  margin-top: 4px;
}
.panel-close-btn:active { transform: translateY(1px); }

/* ---------- Modal overlays (map / menu / character) ---------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 22, 30, 0.55);
  z-index: 28;
}
.modal-overlay.hidden { display: none; }

.map-card {
  position: relative;
  background: #fffef8;
  border-radius: 18px;
  padding: 26px 28px;
  width: 300px;
  box-shadow: 0 14px 0 rgba(0,0,0,0.12), 0 20px 40px rgba(0,0,0,0.35);
}
/* The little (i) rules/gold-reward button every minigame card gets, top
   right. See openGameInfo. Positioned relative to whichever .map-card
   (or subclass, e.g. .minefield-card) it's placed inside. */
.game-info-btn {
  position: absolute;
  top: 14px;
  left: 14px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid #d7dee5;
  background: #fff;
  color: #2f6690;
  font-size: 14px;
  font-weight: 800;
  font-style: italic;
  font-family: Georgia, serif;
  cursor: pointer;
  padding: 0;
  line-height: 1;
}
.game-info-btn:hover { border-color: #56b4e9; }
.game-info-body {
  font-size: 14px;
  line-height: 1.5;
  color: #3a4653;
  margin-bottom: 16px;
}
.game-info-body p { margin: 0 0 10px; }
.game-info-body p:last-child { margin-bottom: 0; }
.game-info-body ul { margin: 0 0 10px; padding-left: 20px; }
.map-title {
  font-size: 20px;
  font-weight: 800;
  color: #2f6690;
  text-align: center;
  margin-bottom: 16px;
}
.room-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 14px;
}
.room-btn {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  height: 46px;
  padding: 0 16px;
  border-radius: 12px;
  border: 2px solid #d7dee5;
  background: #fff;
  color: #2f3a44;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.room-btn:hover { border-color: #56b4e9; }
.room-btn.current {
  background: #eaf6ff;
  border-color: #56b4e9;
  cursor: default;
}
.room-btn .room-tag {
  font-size: 11px;
  font-weight: 700;
  color: #56b4e9;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* ---------- World map ---------- */
.world-map-card { width: min(94vw, 680px); }
.world-map-canvas {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 640 / 520;
  border-radius: 14px;
  margin-bottom: 14px;
  cursor: default;
}

/* ---------- Controls popup ---------- */
.controls-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 14px;
}
.controls-row {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 14px;
  font-weight: 600;
  color: #2f3a44;
}
.key-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 28px;
  padding: 0 8px;
  border-radius: 8px;
  border: 2px solid #d7dee5;
  background: #fff;
  font-size: 13px;
  font-weight: 800;
  color: #2f6690;
}

/* ---------- Bulletin board ---------- */
.bulletin-content {
  min-height: 90px;
  padding: 12px 14px;
  border-radius: 12px;
  background: #f7f2e4;
  border: 2px solid #e5d9b8;
  margin-bottom: 12px;
}
.bulletin-date {
  font-size: 12px;
  font-weight: 800;
  color: #a8894a;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 6px;
}
.bulletin-text {
  font-size: 14px;
  font-weight: 600;
  color: #2f3a44;
  line-height: 1.4;
}
.bulletin-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-bottom: 14px;
}
.bulletin-nav-btn {
  width: 36px;
  height: 36px;
  border-radius: 10px;
  border: 2px solid #d7dee5;
  background: #fff;
  font-size: 15px;
  font-weight: 700;
  color: #2f6690;
  cursor: pointer;
}
.bulletin-nav-btn:hover { border-color: #56b4e9; }
.bulletin-nav-btn:disabled {
  opacity: 0.35;
  cursor: default;
  border-color: #d7dee5;
}
.bulletin-page {
  font-size: 13px;
  font-weight: 700;
  color: #6b7784;
  min-width: 40px;
  text-align: center;
}

/* ---------- Character popup ---------- */
.character-card {
  background: #fffef8;
  border-radius: 18px;
  padding: 26px 28px;
  width: min(90vw, 620px);
  box-shadow: 0 14px 0 rgba(0,0,0,0.12), 0 20px 40px rgba(0,0,0,0.35);
}
.character-name {
  font-size: 20px;
  font-weight: 800;
  color: #2f6690;
  text-align: center;
  margin-bottom: 14px;
}

/* ---------- Account ---------- */
.account-playtime {
  font-size: 15px;
  font-weight: 600;
  color: #3a4653;
  text-align: center;
  margin-bottom: 18px;
}
.account-stats-header {
  font-size: 13px;
  font-weight: 700;
  color: #7a8794;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 8px;
}
.account-stats {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 18px;
}
.account-stat-row {
  display: flex;
  justify-content: space-between;
  font-size: 14px;
  font-weight: 600;
  color: #3a4653;
  background: #f4f7fa;
  border-radius: 8px;
  padding: 8px 12px;
}
/* Only rows with a real per-difficulty breakdown get a title attribute
   (see activityRowHtml) — the help cursor is the hint that hovering shows
   more detail, rather than every row looking hoverable. */
.account-stat-row[title] { cursor: help; }

/* ---------- Commands popup ---------- */
.commands-card { width: min(94vw, 440px); }
.commands-entry { margin-bottom: 18px; }
.commands-syntax {
  font-family: ui-monospace, monospace;
  font-size: 15px;
  font-weight: 700;
  color: #2f6690;
  background: #eaf6ff;
  border-radius: 8px;
  padding: 8px 12px;
  margin-bottom: 6px;
}
.commands-desc { font-size: 13px; color: #5a6b78; padding: 0 2px; }

/* ---------- Chat log popup (T key — see openChatLog) ---------- */
.chatlog-card { width: min(94vw, 440px); }
.chatlog-list {
  max-height: 60vh;
  overflow-y: auto;
  margin-bottom: 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.chatlog-item {
  font-size: 13px;
  line-height: 1.4;
  color: #2f3a44;
  word-break: break-word;
}
.chatlog-name { font-weight: 700; }
/* Server system messages (see logChatMessage's isServer) get their own
   colour throughout, same amber the shop's own price text uses, so they
   read as distinct from ordinary player chat at a glance. */
.chatlog-item.server { color: #b8901f; font-weight: 600; }
/* Your own messages get their own colour too, same "distinct at a glance"
   reasoning as server messages, just green instead of gold. */
.chatlog-item.own { color: #3f8f4a; font-weight: 600; }
.chatlog-empty-hint { font-size: 13px; color: #8a97a3; text-align: center; padding: 12px 0; }
.character-row {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  margin-bottom: 16px;
}
.character-avatar-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}
#characterCanvas {
  width: 160px;
  height: 160px;
  border-radius: 14px;
  background: linear-gradient(#eef5fa, #dfeaf2);
}
.character-gold {
  font-size: 14px;
  font-weight: 800;
  color: #b8901f;
}
.closet-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.closet-tabs {
  display: flex;
  gap: 10px;
  margin-bottom: 8px;
  border-bottom: 2px solid #eef1f4;
}
.closet-tab {
  flex: 1;
  border: none;
  background: none;
  padding: 0 0 10px;
  font-size: 14px;
  font-weight: 700;
  color: #9aa5b0;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  white-space: nowrap;
}
.closet-tab.active {
  color: #2f6690;
  border-bottom-color: #56b4e9;
}
.closet-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px;
  min-width: 0;
  margin-bottom: 10px;
}
.closet-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  height: 44px;
  padding: 0 12px;
  border-radius: 12px;
  border: 2px solid #d7dee5;
  background: #fff;
  color: #2f3a44;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.closet-item .closet-icon { font-size: 18px; flex: 0 0 auto; }
/* Guards against a future longer item name — truncate with an ellipsis
   rather than overflow the button or wrap awkwardly. */
.closet-item span:last-child {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  min-width: 0;
}
.closet-item:hover { border-color: #56b4e9; }
/* Your Seeds panel — a plain inventory readout, not a picker (see
   renderSeedsList/the plant-picker popup, which is the actual interactive
   one) — no hover affordance since clicking one does nothing. */
.closet-item.readonly { cursor: default; }
.closet-item.readonly:hover { border-color: #d7dee5; }
.closet-item.worn {
  background: #eaf6ff;
  border-color: #56b4e9;
}
/* A furniture type with nothing left available to place (every owned copy
   is already down somewhere) — still shown, per its own (0) count, just
   greyed out and inert rather than dropped from the list entirely. */
.closet-item.unavailable {
  opacity: 0.45;
  cursor: not-allowed;
}
.closet-item.unavailable:hover { border-color: #d7dee5; }
/* A seed row carries more than a plain furniture/pet one (name+count plus
   its own grow time) — taller, and its text wrapper opts back out of the
   generic single-line ellipsis rule above to stack two lines instead. */
.closet-item.seed-item { height: 56px; }
.closet-item.seed-item .seed-item-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 2px;
  overflow: hidden;
  white-space: normal;
  min-width: 0;
}
.seed-item-name {
  width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.seed-item-time {
  font-size: 11px;
  font-weight: 500;
  color: #6b7785;
}
/* Shown in place of the grid itself (see renderSeedsList) when there's
   nothing owned yet to list. */
.closet-empty-hint {
  grid-column: 1 / -1;
  padding: 16px 4px;
  color: #6b7785;
  font-size: 13px;
  text-align: center;
}

/* ---------- Decorate (Your Furniture) ---------- */
/* Wider than the plain .map-card (300px) it otherwise shares with Map/
   Controls, same idea as .shop-card/.character-card, and wider still —
   more real columns actually fit at once instead of the grid collapsing
   into what looked like a single-column list. */
.furniture-card { width: min(94vw, 820px); }
/* Fixed 4 columns rather than auto-fill — a wide card with only a handful
   of items in a category otherwise stretched each button far wider than
   it needed to be. */
.furniture-list { grid-template-columns: repeat(4, 1fr); margin-bottom: 18px; }

/* One outfit slot per line (3 rows, stacked), each row itself horizontal
   — the apply button and its save icon side by side, not stacked on top
   of each other. The apply button fills the rest of its own row's width
   (flex: 1) rather than sizing to its own text. */
#outfitsList.hidden { display: none; }
/* Same "hidden" class setClosetTab already toggled on these two, but
   neither ever had its own display:none rule (every hideable element here
   needs one — there's no generic .hidden fallback) — so switching to the
   Outfits tab never actually hid the previous tab's grid/pager, just left
   them rendered underneath the outfits list. */
#closetList.hidden, #closetPager.hidden { display: none; }
.outfits-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.outfit-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
.outfit-apply-btn {
  flex: 1;
  min-width: 0;
  height: 44px;
  padding: 0 14px;
  border-radius: 12px;
  border: 2px solid #d7dee5;
  background: #fff;
  color: #2f3a44;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.outfit-apply-btn:hover { border-color: #56b4e9; }
.outfit-save-btn, .outfit-rename-btn {
  width: 36px;
  height: 36px;
  flex: 0 0 auto;
  border-radius: 10px;
  border: 2px solid #d7dee5;
  background: #fff;
  font-size: 16px;
  font-weight: 700;
  color: #6b7784;
  cursor: pointer;
  padding: 0;
}
.outfit-save-btn:hover, .outfit-rename-btn:hover { border-color: #56b4e9; }

/* ---------- Visit a Home ---------- */
#visitNameInput, #outfitNameInput {
  width: 100%;
  height: 44px;
  border-radius: 12px;
  border: 2px solid #d7dee5;
  outline: none;
  font-size: 14px;
  padding: 0 14px;
  margin-bottom: 10px;
  text-align: center;
  box-sizing: border-box;
}
#visitNameInput:focus, #outfitNameInput:focus { border-color: #56b4e9; }
.visit-send-btn {
  width: 100%;
  height: 44px;
  border-radius: 12px;
  border: none;
  background: #f2c94c;
  color: #4a3400;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  margin-bottom: 4px;
}
/* Two-line label (see openFisherman) — .visit-send-btn's own fixed 44px
   height is sized for a single line, so this grows to fit instead of
   clipping the second one. */
#fishermanBuyBtn {
  height: auto;
  min-height: 44px;
  padding: 6px 0;
  line-height: 1.4;
}
.visit-send-btn:active { transform: translateY(1px); }
.visit-status {
  min-height: 18px;
  font-size: 12.5px;
  color: #7a8794;
  text-align: center;
  margin-bottom: 6px;
}

/* ---------- HUD ---------- */
#hud {
  position: fixed;
  top: 10px;
  right: 14px;
  display: flex;
  gap: 10px;
  color: rgba(255,255,255,0.55);
  font-size: 11px;
  font-family: monospace;
  z-index: 15;
  pointer-events: none;
}

/* ---------- Gold balance ---------- */
#goldBar {
  position: fixed;
  top: 10px;
  left: 14px;
  background: rgba(20,28,36,0.7);
  color: #f2c94c;
  font-size: 15px;
  font-weight: 700;
  padding: 6px 14px;
  border-radius: 14px;
  /* Above .modal-overlay's own z-index (28) so its dark backdrop never
     dims the gold count while a menu/game/shop is open. pointer-events
     stays none, so it never blocks clicks meant for the overlay under it. */
  z-index: 29;
  pointer-events: none;
}
#goldBar.hidden { display: none; }

/* ---------- Decorate/Seeds/Collection buttons (own home/own farm/the
   Arena — all mutually exclusive rooms, so they always share this one
   spot) ---------- */
#decorateBtn, #seedsBtn, #collectionBtn {
  position: fixed;
  top: 34px;
  right: 14px;
  background: #f2c94c;
  color: #4a3400;
  font-size: 13px;
  font-weight: 700;
  padding: 7px 14px;
  border: none;
  border-radius: 14px;
  box-shadow: 0 3px 0 #b8901f;
  cursor: pointer;
  z-index: 15;
}
#decorateBtn:active, #seedsBtn:active, #collectionBtn:active { transform: translateY(2px); box-shadow: 0 1px 0 #b8901f; }
#decorateBtn.hidden, #seedsBtn.hidden, #collectionBtn.hidden { display: none; }

/* Own farm only / the Arena only / own home while Edit mode is on
   (mutually exclusive the same way the row above is — see
   updateEditButtons for #furnitureBtn's own visibility), stacked directly
   under the row above (same spot, shifted down its own height + a gap)
   rather than a third slot of its own — see updateFarmSlotButton for
   #buyFarmSlotBtn's own text/disabled state. */
#buyFarmSlotBtn, #queueBtn, #furnitureBtn {
  position: fixed;
  top: 74px;
  right: 14px;
  background: #f2c94c;
  color: #4a3400;
  font-size: 13px;
  font-weight: 700;
  padding: 7px 14px;
  border: none;
  border-radius: 14px;
  box-shadow: 0 3px 0 #b8901f;
  cursor: pointer;
  z-index: 15;
}
#buyFarmSlotBtn:active, #queueBtn:active, #furnitureBtn:active { transform: translateY(2px); box-shadow: 0 1px 0 #b8901f; }
#buyFarmSlotBtn:disabled { background: #e4ebf1; color: #9aa7b3; box-shadow: 0 3px 0 #b7c2cc; cursor: not-allowed; }
#buyFarmSlotBtn.hidden, #queueBtn.hidden, #furnitureBtn.hidden { display: none; }

/* The Arena only, stacked directly under Queue (same spot, shifted down
   its own height + a gap) — see updateArenaButtons for its visibility. */
#packsBtn {
  position: fixed;
  top: 114px;
  right: 14px;
  background: #f2c94c;
  color: #4a3400;
  font-size: 13px;
  font-weight: 700;
  padding: 7px 14px;
  border: none;
  border-radius: 14px;
  box-shadow: 0 3px 0 #b8901f;
  cursor: pointer;
  z-index: 15;
}
#packsBtn:active { transform: translateY(2px); box-shadow: 0 1px 0 #b8901f; }
#packsBtn.hidden { display: none; }

/* ---------- Memory Match ---------- */
.memory-card { width: 340px; }
.memory-status {
  text-align: center;
  font-size: 13px;
  font-weight: 700;
  color: #7a8794;
  margin-bottom: 10px;
}
.memory-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin-bottom: 12px;
}
.memory-tile {
  aspect-ratio: 1;
  border-radius: 10px;
  border: 2px solid #4a90c2;
  background: #56b4e9;
  color: transparent;
  font-size: 20px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.memory-tile:disabled { cursor: default; }
.memory-tile.revealed {
  background: #fff;
  color: #2f6690;
  border-color: #56b4e9;
}
.memory-tile.matched {
  background: #eaf6ff;
  color: #56b4e9;
  border-color: #b7e0f5;
  opacity: 0.7;
}
.memory-result {
  min-height: 20px;
  text-align: center;
  font-size: 14px;
  font-weight: 700;
  color: #2f9e4f;
  margin-bottom: 6px;
}

/* ---------- 15 Puzzle ---------- */
.puzzle15-card { width: min(90vw, 420px); }
.puzzle15-grid {
  display: grid;
  /* grid-template-columns is set inline per-board in buildPuzzle15Grid
     (see game.js) since the column count depends on which size (4x4 or
     5x5) is currently in play, not a fixed value. */
  gap: 8px;
  margin-bottom: 12px;
}
.puzzle15-tile {
  aspect-ratio: 1;
  border-radius: 10px;
  border: 2px solid #4a90c2;
  background: #56b4e9;
  color: #fff;
  font-size: 18px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.puzzle15-size-row {
  display: flex;
  gap: 8px;
  margin-bottom: 8px;
}
.puzzle15-size-row .visit-send-btn { flex: 1; margin-bottom: 0; }
/* The blank slot — same grid cell, but invisible and unclickable rather
   than a removed element, so the grid never reflows around it. */
.puzzle15-tile.blank {
  background: none;
  border: none;
  cursor: default;
}

/* ---------- Minefield ---------- */
/* The grid is a fixed square footprint (width capped by viewport, height
   locked to match via aspect-ratio) — every board is square (rows ===
   cols in every MINEFIELD_CONFIGS entry), so 1fr columns/rows divide
   that fixed square evenly into square cells on their own, no per-size
   pixel math needed in JS at all (see buildMinefieldGrid, which now
   only sets the row/column *counts*). Small boards get chunkier cells,
   Large gets denser ones, but the modal's own footprint never changes. */
.minefield-card { width: fit-content; max-width: 96vw; max-height: 96vh; overflow: auto; }
.minefield-grid {
  display: grid;
  /* grid-template-columns/rows are set inline in buildMinefieldGrid
     (see game.js) since the row/column count depends on which size is
     currently in play — only the counts, never a pixel size. */
  gap: 2px;
  margin: 0 auto 12px;
  width: min(70vw, 60vh, 460px);
  aspect-ratio: 1;
}
.minefield-cell {
  min-width: 0;
  min-height: 0;
  border-radius: 4px;
  border: 1px solid #4a90c2;
  background: #56b4e9;
  color: #0d2b3a;
  font-size: 13px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  user-select: none;
}
.minefield-cell.revealed {
  background: #eaf6ff;
  border-color: #cfe0ea;
  cursor: default;
}
.minefield-cell.flagged { background: #f7c948; }
.minefield-cell.mine { background: #e05c5c; }

/* ---------- Cascade ---------- */
/* Same CSS-grid-does-the-sizing approach as Minefield's own grid — see its
   comment above. Board size never actually changes (see server.js's
   CASCADE_ROWS/COLS), but this stays generic rather than hardcoding it. */
.cascade-grid {
  display: grid;
  gap: 2px;
  margin: 0 auto 12px;
  width: min(70vw, 60vh, 460px);
  aspect-ratio: 1;
}
.cascade-cell {
  min-width: 0;
  min-height: 0;
  border-radius: 4px;
  border: none;
  padding: 0;
  cursor: pointer;
  /* Duration must match game.js's CASCADE_FADE_MS — the delay before the
     server's settled board actually replaces this is timed to let this
     transition finish first. */
  transition: opacity 220ms ease-out;
}
.cascade-cell:disabled { cursor: default; }
/* Applied to a clicked group's tiles the instant a valid clear is
   detected locally (see onCascadeTileClick) — fades out before the
   post-gravity board actually replaces them, so it's obvious what just
   got cleared instead of the board just silently rearranging. */
.cascade-clearing { opacity: 0; }

/* ---------- Ripple ---------- */
/* Reuses .cascade-grid for the board itself (same CSS-grid-does-the-
   sizing approach) — only the per-cell look differs, since Ripple's cells
   are a plain div (not clickable) rather than a button. */
.ripple-cell {
  min-width: 0;
  min-height: 0;
  border-radius: 2px;
  box-sizing: border-box;
}
/* The owned region gets a visible ring so it reads as "your territory"
   distinct from same-colored tiles elsewhere that aren't actually
   connected to it yet. */
.ripple-cell.owned { box-shadow: inset 0 0 0 2px rgba(0,0,0,0.8); }
.ripple-color-row {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-bottom: 14px;
}
.ripple-color-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 2px solid rgba(0,0,0,0.15);
  cursor: pointer;
  padding: 0;
}
.ripple-color-btn:hover { border-color: rgba(0,0,0,0.4); }
.ripple-color-btn:active { transform: translateY(1px); }

/* ---------- Shop ---------- */
.shop-card { width: min(90vw, 640px); }
.shop-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 8px 10px;
  margin-bottom: 14px;
}
.shop-row {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 46px;
  padding: 0 12px;
  border-radius: 12px;
  border: 2px solid #d7dee5;
  background: #fff;
  min-width: 0;
  cursor: pointer;
}
.shop-row:hover { border-color: #56b4e9; }
.shop-row.owned { opacity: 0.55; }
/* Guards against a longer item name (e.g. "Walking Stick") now that a
   row's column can be narrower than the old single-column layout. */
.shop-label {
  flex: 1;
  font-size: 14px;
  font-weight: 600;
  color: #2f3a44;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  min-width: 0;
}
.shop-price { font-size: 13px; font-weight: 700; color: #b8901f; }
.shop-buy-btn {
  height: 32px;
  padding: 0 14px;
  border-radius: 10px;
  border: none;
  background: #f2c94c;
  color: #4a3400;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}
.shop-buy-btn:disabled {
  background: #e4ebf1;
  color: #9aa7b3;
  cursor: not-allowed;
}

/* ---------- Buy confirmation popup ---------- */
.buy-confirm-card { width: 270px; text-align: center; }
#buyConfirmCanvas {
  display: block;
  width: 200px;
  height: 210px;
  margin: 0 auto 10px;
  border-radius: 14px;
  background: linear-gradient(#eef5fa, #dfeaf2);
}
.buy-confirm-price { text-align: center; font-size: 16px; margin-bottom: 4px; }

/* ---------- Collection / card detail (see openCollection/openCardDetail) ---------- */
.collection-card { width: min(90vw, 480px); }
/* Wide enough that the 220px canvas plus .map-card's own 28px side
   padding actually fits inside it — at the old 250px there wasn't enough
   room left over for margin:auto to center the canvas at all, so it just
   overflowed off the right edge instead of looking centered. */
.card-detail-card { width: 300px; text-align: center; }
#cardDetailCanvas {
  display: block;
  width: 220px;
  height: 310px;
  margin: 0 auto 16px;
}
/* Seed-only (see renderBuyConfirmPreview's kind===2 branch) — grow time
   and sell value, shown right under the price so it's read together with
   it before deciding how many to buy. */
#buyConfirmSeedInfo { font-size: 12px; color: #5a6b78; margin-bottom: 12px; }
#buyConfirmSeedInfo.hidden { display: none; }
#buyConfirmFurnitureInfo { font-size: 12px; color: #5a6b78; margin-bottom: 12px; }
#buyConfirmFurnitureInfo.hidden { display: none; }
.buy-confirm-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.buy-confirm-actions .shop-buy-btn,
.buy-confirm-actions .panel-close-btn {
  flex: 1 1 100px;
  min-width: 0;
  width: auto;
  height: 40px;
  font-size: 14px;
  margin-top: 0;
}
/* Bulk-buy (seeds only, see openBuyConfirm) — hidden by default since a
   one-off ITEMS/FURNITURE purchase has no notion of quantity. */
#buyConfirmBuy5Btn.hidden, #buyConfirmBuy10Btn.hidden { display: none; }

/* ---------- Packs (see openPackSelect/startPackOpenAnimation) ---------- */
.pack-open-card { width: min(94vw, 640px); text-align: center; }
.pack-select-view.hidden { display: none; }
.pack-select-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: 24px;
  margin: 10px 0 18px;
}

/* A pack sprite — a plain gradient box rather than real art, same
   "canvas/CSS art, no image assets" rule everything else in this game
   follows. The shine sweep and idle bounce are both purely to invite a
   click; neither means anything once the pack's actually been opened. */
.pack-sprite {
  position: relative;
  width: 180px;
  height: 240px;
  margin: 0;
  border-radius: 16px;
  background: linear-gradient(160deg, #f2c94c, #e8871e 55%, #c0392b);
  box-shadow: 0 10px 0 rgba(0,0,0,0.18), 0 16px 28px rgba(0,0,0,0.3);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  cursor: pointer;
  overflow: hidden;
  animation: packIdleBounce 2.4s ease-in-out infinite;
}
.pack-sprite-icon { font-size: 56px; }
.pack-sprite-label { font-size: 15px; font-weight: 800; color: #fffef8; text-shadow: 0 1px 3px rgba(0,0,0,0.35); }
.pack-sprite-price { font-size: 13px; font-weight: 700; color: rgba(255,255,255,0.9); text-shadow: 0 1px 3px rgba(0,0,0,0.35); }
/* The diagonal shine sweeping across the pack — a wide, rotated bar swept
   from off the left edge to off the right, same "just a gradient and a
   keyframe" trick as everywhere else in this file that fakes a highlight. */
.pack-sprite::after {
  content: '';
  position: absolute;
  top: -20%;
  left: -60%;
  width: 40%;
  height: 140%;
  background: rgba(255,255,255,0.35);
  transform: rotate(20deg);
  animation: packShineSweep 2.6s ease-in-out infinite;
}
@keyframes packIdleBounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}
@keyframes packShineSweep {
  0% { left: -60%; }
  40%, 100% { left: 130%; }
}
/* Played once, in place of the idle bounce/shine above, the moment a pack
   is actually clicked — see startPackOpenAnimation. Snappy rather than
   lingering, since the reveal view is what should actually hold attention
   next, not this. */
.pack-sprite.pack-opening {
  animation: packBurst 0.5s ease-in forwards;
}
.pack-sprite.pack-opening::after { animation: none; opacity: 0; }
@keyframes packBurst {
  0% { transform: scale(1) rotate(0deg); opacity: 1; }
  60% { transform: scale(1.15) rotate(-4deg); opacity: 1; }
  100% { transform: scale(1.4) rotate(6deg); opacity: 0; }
}
.pack-open-hint { font-size: 13px; color: #5a6b78; margin-bottom: 4px; }

/* The Premium pack's own extra decoration, layered on top of everything
   .pack-sprite above already gives it — a richer gold-to-violet gradient,
   a slow pulsing glow, and a corner ribbon (clipped by the sprite's own
   overflow:hidden into the classic ribbon look, not a separate cutout) —
   so it visibly reads as the better option before a single card is ever
   drawn, not just a higher number on its price tag. */
.pack-sprite-premium {
  background: linear-gradient(160deg, #f2c94c, #9b59b6 55%, #3a2a5c);
  box-shadow: 0 10px 0 rgba(0,0,0,0.18), 0 16px 28px rgba(0,0,0,0.3), 0 0 20px rgba(155,89,182,0.5);
  animation: packIdleBounce 2.4s ease-in-out infinite, packPremiumGlow 2.2s ease-in-out infinite;
}
.pack-sprite-premium::before {
  content: '★ PREMIUM ★';
  position: absolute;
  top: 16px;
  right: -34px;
  width: 140px;
  transform: rotate(40deg);
  background: #f2c94c;
  color: #4a3400;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-align: center;
  padding: 3px 0;
  box-shadow: 0 2px 4px rgba(0,0,0,0.25);
}
.pack-sprite-premium.pack-opening::before { opacity: 0; transition: opacity 0.15s; }
@keyframes packPremiumGlow {
  0%, 100% { box-shadow: 0 10px 0 rgba(0,0,0,0.18), 0 16px 28px rgba(0,0,0,0.3), 0 0 16px rgba(155,89,182,0.4); }
  50% { box-shadow: 0 10px 0 rgba(0,0,0,0.18), 0 16px 28px rgba(0,0,0,0.3), 0 0 32px rgba(155,89,182,0.85); }
}
.pack-reveal-view.hidden { display: none; }
.pack-cards-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin: 14px 0;
}
/* Classic flip-card technique: two faces back to back in 3D space, only
   ever one of them actually facing the viewer at a time — see
   startPackOpenAnimation's own click handler for what toggles .flipped. */
.pack-card-flip {
  width: 100px;
  height: 140px;
  perspective: 800px;
  cursor: pointer;
}
.pack-card-flip.flipped { cursor: default; }
.pack-card-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.3, 1);
  transform-style: preserve-3d;
}
.pack-card-flip.flipped .pack-card-inner { transform: rotateY(180deg); }
.pack-card-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  border-radius: 10px;
  overflow: hidden;
}
/* Same colours/shine idea as the closed pack itself, just smaller and
   without its own idle animation — a whole row of independently bouncing
   card backs would read as chaotic rather than inviting. */
.pack-card-back {
  background: linear-gradient(160deg, #f2c94c, #e8871e 55%, #c0392b);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 30px;
  box-shadow: 0 4px 0 rgba(0,0,0,0.15);
  transition: box-shadow 0.15s ease-out;
}
/* A rarity-tinted glow while still face-down — --rarity-glow is set per
   card in JS (see renderPackCards), so this one rule covers every rarity
   without knowing which one it is. Not shown once flipped (.flipped
   overrides cursor to default and there's nothing left to preview at that
   point), and not shown before hover at all — only a hint you get by
   actually mousing over a card, matching a real pack's own "no peeking
   until you check" idea. */
.pack-card-flip:not(.flipped):hover .pack-card-back {
  box-shadow: 0 4px 0 rgba(0,0,0,0.15), 0 0 18px 3px var(--rarity-glow, rgba(255,255,255,0.6));
}
.pack-card-front {
  transform: rotateY(180deg);
  background: #fffef8;
  box-shadow: 0 4px 0 rgba(0,0,0,0.15);
}
.pack-card-front canvas { width: 100%; height: 100%; display: block; }
/* A brief glow the instant a card lands face-up — makes the flip itself
   feel like the moment something happened, not just a rotation. */
.pack-card-flip.flipped .pack-card-front {
  animation: packCardRevealGlow 0.6s ease-out;
}
@keyframes packCardRevealGlow {
  0% { box-shadow: 0 4px 0 rgba(0,0,0,0.15), 0 0 0 rgba(242,201,76,0); }
  35% { box-shadow: 0 4px 0 rgba(0,0,0,0.15), 0 0 26px rgba(242,201,76,0.9); }
  100% { box-shadow: 0 4px 0 rgba(0,0,0,0.15), 0 0 0 rgba(242,201,76,0); }
}
#packDoneBtn.hidden { display: none; }

/* ---------- Game board mockup (see openGameBoard) ---------- */
/* Fills the whole .modal-overlay itself rather than sitting inside a
   centred .map-card the way every other popup does — a card game's board
   is a full scene, not a small dialog, so this is the one overlay that
   deliberately doesn't use that shared layout at all. */
.game-board {
  position: relative;
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: 90px 1fr 90px;
  background: radial-gradient(ellipse at center, #33465a 0%, #172230 100%);
  overflow: hidden;
}
.game-board-close-btn {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 6;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: none;
  background: rgba(255,255,255,0.12);
  color: #fffef8;
  font-size: 15px;
  cursor: pointer;
}
.game-board-close-btn:hover { background: rgba(255,255,255,0.22); }

/* Turn indicator/timer/end-turn strip — hidden until a real match state
   comes in (see onCardDuelState), since it has nothing true to show
   before then. */
.card-duel-top-bar.hidden { display: none; }
.card-duel-top-bar {
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 14px;
  background: rgba(0,0,0,0.35);
  border: 1px solid rgba(255,255,255,0.2);
  border-radius: 999px;
  padding: 8px 18px;
  transition: background 0.25s ease, border-color 0.25s ease;
  /* The pill's own bounding box is a plain rectangle regardless of its
     rounded corners — without this, the invisible corners of that
     rectangle would still intercept clicks aimed at whatever sits behind
     it (the opponent's hero, see .board-center's own top padding), even
     over the parts of the box that render as empty space. */
  pointer-events: none;
}
.card-duel-top-bar > * { pointer-events: auto; }
.card-duel-turn-label { color: #fffef8; font-size: 14px; font-weight: 700; transition: color 0.25s ease; }
/* Set by renderCardDuelBoard from state.isMyTurn — a plain "Your turn"
   text label buried in a small pill was easy to miss entirely; a
   gently pulsing green glow plus bold gold caps is the same "you're not
   waiting on anything, look here" language the end-turn button's own
   colour already half-suggests, just impossible to miss now. */
.card-duel-top-bar.my-turn {
  background: rgba(63,145,66,0.55);
  border-color: rgba(255,255,255,0.55);
  animation: card-duel-turn-pulse 1.6s ease-in-out infinite;
}
.card-duel-top-bar.my-turn .card-duel-turn-label {
  color: #f2c94c;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.card-duel-top-bar.opponent-turn { background: rgba(0,0,0,0.45); }
@keyframes card-duel-turn-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(63,145,66,0.55); }
  50% { box-shadow: 0 0 20px 7px rgba(63,145,66,0.55); }
}
.card-duel-timer { color: #f2c94c; font-size: 14px; font-weight: 800; min-width: 32px; text-align: center; }
.card-duel-end-turn-btn {
  border: none;
  border-radius: 999px;
  padding: 6px 16px;
  font-size: 13px;
  font-weight: 800;
  color: #fffef8;
  background: linear-gradient(160deg, #4fae5e, #2f7d3c);
  cursor: pointer;
}
.card-duel-end-turn-btn:hover { filter: brightness(1.1); }
.card-duel-end-turn-btn:disabled { background: #4a5560; cursor: not-allowed; filter: none; opacity: 0.6; }

/* Mana, left column — a diamond per point of GAME_BOARD_MAX_MANA, filled
   ones lit blue. column-reverse so the lit gems stack up from the bottom,
   the same direction a real mana bar reads. */
.board-mana-col {
  grid-column: 1;
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  justify-content: center;
  gap: 7px;
}
.board-mana-crystal {
  width: 24px;
  height: 24px;
  border-radius: 6px;
  transform: rotate(45deg);
  background: #2a3a4a;
  border: 2px solid rgba(255,255,255,0.15);
}
.board-mana-crystal.filled {
  background: linear-gradient(160deg, #7fd8f0, #2f8fc9);
  border-color: #bfeaf7;
  box-shadow: 0 0 10px rgba(111,214,245,0.7);
}

/* Deck, right column — opponent's copy at the top, mine at the bottom,
   same top/bottom split the board rows and hand/hero bars below use for
   "theirs vs. mine". */
.board-deck-col {
  grid-column: 3;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  padding: 28px 0;
}
.board-deck { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.board-deck-card {
  width: 58px;
  height: 82px;
  border-radius: 8px;
  background: linear-gradient(160deg, #8a8f99, #5a6270);
  border: 2px solid rgba(255,255,255,0.3);
  box-shadow: 0 3px 0 rgba(0,0,0,0.3), 2px 5px 6px rgba(0,0,0,0.25);
}
.board-deck-count { color: #fffef8; font-size: 13px; font-weight: 800; text-shadow: 0 1px 2px rgba(0,0,0,0.4); }

/* Centre column — opponent's bar/hand at the top, the two board rows in
   the middle, my own hero bar and hand at the bottom. */
.board-center {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  height: 100%;
  /* Extra top padding (vs. the 6px the bottom gets) clears
     .card-duel-top-bar's own height so the pill never visually sits over
     the opponent's hero — the actual attack target, and the whole reason
     this needed clearing (see that class's own pointer-events comment for
     the hit-testing half of the same fix). */
  padding: 70px 0 6px;
}
.board-opponent-bar, .board-my-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
}
.board-hero {
  position: relative;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #f2c94c, #b8901f);
  border: 3px solid #fffef8;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  box-shadow: 0 3px 8px rgba(0,0,0,0.4);
  flex: 0 0 auto;
}
/* The hero's own HP — a plain number, not "30/30", since max HP is fixed
   and never shown anywhere on its own, so there's nothing a fraction
   would actually be telling you here yet. */
.board-hero-hp {
  position: absolute;
  bottom: -6px;
  right: -6px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #c0392b;
  color: #fffef8;
  font-size: 12px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid #fffef8;
  box-shadow: 0 2px 4px rgba(0,0,0,0.35);
}

/* Opponent's hand — plain overlapping card backs, no fan/rotation (see
   .board-hand-mine-card for why mine gets the fancier treatment): nobody
   but them ever sees what's actually in it, so there's nothing here worth
   making a bigger visual moment out of. */
.board-hand-opponent { display: flex; }
.board-hand-card-back {
  width: 32px;
  height: 46px;
  border-radius: 5px;
  background: linear-gradient(160deg, #f2c94c, #e8871e 55%, #c0392b);
  border: 1px solid rgba(255,255,255,0.4);
  margin-left: -12px;
}
.board-hand-card-back:first-child { margin-left: 0; }

.board-rows { display: flex; flex-direction: column; gap: 18px; }
.board-row { display: flex; gap: 14px; justify-content: center; }
.board-slot {
  position: relative;
  width: 145px;
  height: 203px;
  border-radius: 10px;
  border: 2px dashed rgba(255,255,255,0.25);
  background: rgba(255,255,255,0.04);
}
.board-slot.filled { border-style: solid; border-color: rgba(255,255,255,0.35); background: none; }
.board-slot canvas { width: 100%; height: 100%; border-radius: 6px; display: block; }
/* Keyword/selection glows on a live minion slot — taunt/shield are pure
   status indicators (not clickable states of their own), can-attack marks
   what's legal to arm as an attacker this turn, and selected marks the
   one currently armed (see onCardDuelMinionClick). A glow alone makes you
   memorise "red = taunt, blue = shield" to tell them apart at a glance —
   the badges below (see .board-slot-badge) are what actually spell it
   out, the glow is just reinforcement once you already know which is
   which. */
.board-slot-taunt { box-shadow: 0 0 0 4px #c0392b, 0 0 16px rgba(192,57,43,0.7); }
.board-slot-shield { box-shadow: 0 0 0 4px #5dade2, 0 0 16px rgba(93,173,226,0.8); }
/* Both at once (a taunt minion that also has divine shield) needs its own
   combined ring rather than one glow silently overriding the other. */
.board-slot-taunt.board-slot-shield { box-shadow: 0 0 0 4px #c0392b, 0 0 0 8px #5dade2, 0 0 16px rgba(93,173,226,0.8); }
.board-slot-can-attack { cursor: pointer; box-shadow: 0 0 10px rgba(242,201,76,0.7); }
.board-slot-selected { box-shadow: 0 0 0 3px #f2c94c, 0 0 16px rgba(242,201,76,0.85); }

/* Unmistakable icon badges for the same two keywords the glow above
   reinforces — pinned to the top corners so they read the same regardless
   of how small the card art underneath gets. */
.board-slot-badge {
  position: absolute;
  top: -12px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  border: 2px solid #fffef8;
  box-shadow: 0 2px 6px rgba(0,0,0,0.45);
  z-index: 4;
}
.board-slot-badge-taunt { left: -12px; background: #c0392b; }
.board-slot-badge-shield { right: -12px; background: #5dade2; }
/* An empty slot of mine, while a minion from hand is waiting to be placed
   (see cardDuelPendingMinionHandIndex) — solid instead of the ordinary
   dashed outline, same "this is a real target now" language a taunt/
   shield glow already uses, just green for "you can drop it here" rather
   than a status on an existing minion. */
.board-slot-placeable {
  cursor: pointer;
  border-style: solid;
  border-color: #3f9142;
  box-shadow: 0 0 10px rgba(63,145,66,0.6);
}
/* The attacker's own lunge toward whatever it just hit and back — see
   animateCardDuelAttack for how --lunge-x/--lunge-y get set per attack.
   transform never affects layout, so this never disturbs the row's other
   slots mid-animation. */
@keyframes board-attack-lunge {
  0% { transform: translate(0, 0); }
  45% { transform: translate(var(--lunge-x, 0px), var(--lunge-y, 0px)); }
  100% { transform: translate(0, 0); }
}
/* Duration matches game.js's own CARD_DUEL_ANIM_MS (500ms) — that constant
   is what onCardDuelState waits for before re-rendering the board out from
   under this animation, so the two must stay in sync. */
.board-attack-lunge { animation: board-attack-lunge 0.5s ease-in-out; z-index: 8; }

/* My hand — see renderMyHand for how --fan-rotate/--fan-y are set per
   card; the hover rule below has to reference the same two variables
   (rather than just overriding `transform` outright) so hovering a card
   lifts it further from its own fan position instead of snapping it back
   to upright. */
.board-hand-mine {
  display: flex;
  justify-content: center;
  padding-bottom: 4px;
}
.board-hand-mine-card {
  width: 130px;
  height: 182px;
  margin: 0 -20px;
  cursor: pointer;
  transform-origin: bottom center;
  transform: rotate(var(--fan-rotate, 0deg)) translateY(var(--fan-y, 0px));
  transition: transform 0.15s ease-out;
}
/* A clear pop on hover, not just a nudge — this is how you actually read a
   card's text/art in a hand this small otherwise. The extra lift (-40px,
   vs. the base fan's own ~10-20px) clears the rest of the hand rather
   than just growing in place and clipping into its neighbours. */
.board-hand-mine-card:hover {
  transform: rotate(var(--fan-rotate, 0deg)) translateY(calc(var(--fan-y, 0px) - 40px)) scale(1.35);
  z-index: 5;
}
.board-hand-mine-card canvas {
  width: 100%;
  height: 100%;
  border-radius: 10px;
  box-shadow: 0 4px 10px rgba(0,0,0,0.4);
  display: block;
}
/* A card too expensive to play right now — still clickable (so the
   server's own rejection is what actually stops it, same "client
   predicts, server re-validates" rule as everywhere else), just dimmed
   so it reads as unaffordable before you try. */
.board-hand-card-unaffordable canvas { filter: grayscale(0.6) brightness(0.6); }
/* The one hand card currently waiting on a second click to finish playing
   it — an empty board slot (a minion, see cardDuelPendingMinionHandIndex)
   or one of my own minions (a targeted spell, see
   cardDuelPendingSpellHandIndex). Lifted and glowing rather than just
   left in its ordinary fan position, so it reads as "already picked up",
   not just another card in the hand. */
/* Overrides the base fan transform outright (rather than going through
   --fan-y, which JS already sets inline per card, taking priority over
   anything a class rule could set for that same variable) — the :hover
   variant is repeated here too, since without it hovering an armed card
   would fall back to the ordinary hover lift instead of staying armed. */
.board-hand-card-armed, .board-hand-card-armed:hover {
  transform: translateY(-24px) scale(1.06);
  z-index: 5;
}
.board-hand-card-armed canvas {
  box-shadow: 0 0 0 3px #f2c94c, 0 6px 14px rgba(0,0,0,0.5);
}

/* ---------- Destroy-crop confirmation (see openDestroyConfirm) ---------- */
.destroy-confirm-text {
  font-size: 14px;
  color: #2f3a44;
  text-align: center;
  margin: 4px 0 16px;
  line-height: 1.4;
}
/* Red rather than the shop's usual gold — this is the one destructive
   action on the farm, and it should look like one. */
.destroy-btn { background: #d43c2e; color: #fff; }

/* ---------- PvP (High Card) ---------- */
.pvp-card { width: 340px; }
.pvp-opponent {
  text-align: center;
  font-size: 13px;
  font-weight: 700;
  color: #7a8794;
  margin-bottom: 4px;
}
.pvp-score {
  text-align: center;
  font-size: 22px;
  font-weight: 800;
  color: #2f3a44;
  margin-bottom: 8px;
}
.pvp-status {
  min-height: 20px;
  text-align: center;
  font-size: 14px;
  font-weight: 700;
  color: #2f9e4f;
  margin-bottom: 14px;
}
.pvp-hand {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-bottom: 6px;
}
.pvp-card-btn {
  width: 46px;
  height: 64px;
  border-radius: 8px;
  border: 2px solid #4a90c2;
  background: #fff;
  color: #2f6690;
  font-size: 22px;
  font-weight: 800;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.pvp-card-btn:hover:not(:disabled) { background: #eaf6ff; }
.pvp-card-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

/* ---------- PvP (Connect Four) ---------- */
.pvp-board {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
  margin-bottom: 6px;
}
.c4-cell {
  aspect-ratio: 1;
  border-radius: 50%;
  border: 2px solid #2f6690;
  background: #1c4f73;
  padding: 0;
  cursor: pointer;
}
.c4-cell:disabled { cursor: default; }
.c4-cell.mine { background: #f2c94c; border-color: #c99a1e; }
.c4-cell.opponent { background: #eb5757; border-color: #b93f3f; }

/* Stacks multiple independent toasts (see showToast) rather than one
   shared element every call overwrote — newest at the bottom, each with
   its own fade in/out and timer, so two messages landing close together
   (a rare pet drop alongside its own harvest/catch toast, say) are both
   actually readable instead of the second silently erasing the first. */
#toastContainer {
  position: fixed;
  top: 18px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  pointer-events: none;
  /* Above .modal-overlay's own z-index (28), same reasoning as #goldBar —
     a toast should stay fully visible/bright even with a menu open, not
     dimmed behind its dark backdrop. */
  z-index: 29;
}
.toast-item {
  background: rgba(20,28,36,0.85);
  color: #fff;
  padding: 8px 18px;
  border-radius: 10px;
  font-size: 14px;
  opacity: 0;
  transition: opacity 0.25s ease;
  white-space: nowrap;
}
.toast-item.visible { opacity: 1; }

/* ---------- World hotspot tooltip ---------- */
#worldTooltip {
  position: fixed;
  background: rgba(20,28,36,0.9);
  color: #fff;
  padding: 4px 10px;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 600;
  pointer-events: none;
  z-index: 16;
  white-space: nowrap;
}
#worldTooltip.hidden { display: none; }
