/* ==========================================================================
   PetWars — the whole player-facing surface.
   Phone first: designed at 360x780 portrait; desktop is the same screens, wider.
   Nothing here holds a rule — every number on screen comes from sim.js (see ui.js).
   ========================================================================== */

:root {
  /* DESIGN.md §11 — the class palette is shared by the UI, the card frames and the art. */
  --dog: #f2a03d;
  --cat: #a97bd6;
  --bird: #4fc3f7;
  --fish: #3ec9a7;
  --reptile: #7cc46a;
  --neutral: #c8b394;

  /* A happy pet shop: warm kraft + cream, high contrast, no dark-fantasy greys. */
  --paper: #fff6e8;
  --paper-2: #ffeed6;
  --card: #fffdf8;
  --ink: #3b2b1f;
  --ink-soft: #7a6350;
  --line: #e3cfb3;
  --shelf: #d9b98c;
  --shelf-dark: #b8905f;
  --good: #3ea55f;
  --warn: #e2683f;
  --gold: #f0b429;

  --r-sm: 8px;
  --r: 14px;
  --r-lg: 22px;
  --tap: 44px;              /* the minimum touch target, everywhere */
  --shadow: 0 2px 0 rgba(120, 86, 52, .25), 0 6px 14px rgba(120, 86, 52, .18);
  --shadow-sm: 0 1px 0 rgba(120, 86, 52, .25);

  --ui-font: "Nunito", "Quicksand", "Trebuchet MS", "Segoe UI", system-ui, -apple-system, sans-serif;

  --fan-overlap: 0px;       /* set by ui.js so a 10-card hand never scrolls sideways */

  /* THE VIEWPORT HEIGHT, and it has to be dvh. `100vh` and `height:100%` both resolve to the
     LARGE viewport on a phone — the height with the URL bar hidden — so a layout sized by them
     extends UNDERNEATH the browser chrome and its bottom rows are invisible while the page
     insists everything fits. That is precisely how the End Turn button went missing on a real
     handset and could not be reached by rotating. `dvh` tracks the viewport as the chrome comes
     and goes; the vh line stays as the fallback for anything that lacks it. */
  --vh: 100vh;
  --cls: var(--neutral);    /* per-element class colour; every card face sets its own */
}

/* Per-class helper: any element can carry data-cls and get the right colour. */
[data-cls="dog"]      { --cls: var(--dog); }
[data-cls="cat"]      { --cls: var(--cat); }
[data-cls="bird"]     { --cls: var(--bird); }
[data-cls="fish"]     { --cls: var(--fish); }
[data-cls="reptile"]  { --cls: var(--reptile); }
[data-cls="neutral"]  { --cls: var(--neutral); }

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow: hidden;                    /* the app scrolls in panes, the page never does */
  overscroll-behavior: none;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--ui-font);
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

body {
  /* The warm shop wall behind everything. Pure CSS: no art file is required to boot. */
  background:
    radial-gradient(120% 60% at 50% 0%, #fff3dd 0%, rgba(255, 243, 221, 0) 70%),
    repeating-linear-gradient(90deg, #ffefd8 0 22px, #ffe9cd 22px 44px);
}

.app {
  position: relative;
  width: 100%;
  height: var(--vh);
  max-width: 720px;                     /* desktop = the same screens, gently widened */
  margin: 0 auto;
  overflow: hidden;
  background: var(--paper);
  box-shadow: 0 0 40px rgba(120, 86, 52, .16);
}

@supports (height: 100dvh) {
  :root { --vh: 100dvh; }
}

/* ---------------------------------------------------------------- screens */

.screen {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  padding-top: env(safe-area-inset-top, 0);
  padding-bottom: env(safe-area-inset-bottom, 0);
}
.screen.on { display: flex; }

.topbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  min-height: 56px;
  background: linear-gradient(180deg, var(--shelf) 0%, var(--shelf-dark) 100%);
  color: #fff;
  border-bottom: 3px solid #96723f;
  flex: 0 0 auto;
}
.topbar h2 {
  flex: 1;
  margin: 0;
  font-size: 19px;
  letter-spacing: .2px;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .25);
}
.bottombar {
  flex: 0 0 auto;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0));
  background: var(--paper-2);
  border-top: 2px solid var(--line);
}
.scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  padding: 10px;
}

/* ---------------------------------------------------------------- buttons */

.btn {
  font: inherit;
  font-weight: 800;
  color: var(--ink);
  background: linear-gradient(180deg, #fff 0%, #ffe9c9 100%);
  border: 2px solid var(--shelf-dark);
  border-radius: var(--r);
  min-height: var(--tap);
  min-width: var(--tap);
  padding: 10px 16px;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: transform .08s ease, box-shadow .08s ease, filter .12s ease;
  touch-action: manipulation;
}
.btn:active:not(:disabled) { transform: translateY(2px); box-shadow: var(--shadow-sm); }
.btn:disabled { opacity: .45; cursor: default; box-shadow: none; }
.btn-big { display: block; width: 100%; font-size: 19px; padding: 14px; }
.btn-play {
  background: linear-gradient(180deg, #ffd280 0%, var(--gold) 100%);
  border-color: #b9821b;
}
.btn-ghost { background: rgba(255, 255, 255, .5); border-color: var(--line); font-weight: 700; }
.btn-back {
  font-size: 26px;
  line-height: 1;
  padding: 0 12px;
  background: rgba(255, 255, 255, .85);
}
.icon-btn {
  font: inherit;
  font-size: 18px;
  font-weight: 800;
  min-width: 40px;
  min-height: 40px;
  border-radius: 12px;
  border: 2px solid rgba(255, 255, 255, .5);
  background: rgba(255, 255, 255, .22);
  color: #fff;
  cursor: pointer;
  touch-action: manipulation;
}

:where(.btn, .icon-btn, .card, .pet, .rival, [tabindex]):focus-visible {
  outline: 3px solid #1f6fd0;
  outline-offset: 2px;
}

.purse {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 900;
  color: #4a3413;
  background: linear-gradient(180deg, #ffe9a8, #f3c64a 100%);
  border: 2px solid #b9821b;
  border-radius: 999px;
  padding: 6px 12px;
  white-space: nowrap;
}
.purse .coin {
  width: 16px; height: 16px; border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #fff6c9, #e2a417 70%);
  border: 1px solid #a9761a;
  display: inline-block;
}

/* ---------------------------------------------------------------- title */

#screen-title { align-items: center; justify-content: center; }
.title-scene {
  position: absolute;
  inset: 0;
  /* All three are stated because `paintArt()` only ever sets `background-image` inline — the
     sizing lives here and must survive that. */
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  /* .5 was tuned for a flat gradient. Real hero art at half opacity is the "boring and blank"
     complaint itself, so art gets to be seen; the scrim below buys the text its contrast back
     instead of taking it out of the picture. */
  opacity: .5;
  transition: opacity 700ms ease;
}
.title-scene.has-art { opacity: .95; }
.title-scene.fading { opacity: 0; }
/* Legibility scrim: sits OVER the art and UNDER the menu, so the hero image stays vivid at the
   top where there is no text and the buttons keep a solid ground at the bottom. */
.title-scrim {
  position: absolute; inset: 0; pointer-events: none;
  /* Weighted to where the TEXT is. The top third carries the logo over a solid-ish plate of its
     own, so the art can run nearly clear there; the buttons lower down have their own opaque
     backgrounds and need less help than a flat 0.86 was giving them. */
  background: linear-gradient(180deg,
    rgba(255, 247, 230, .04) 0%, rgba(255, 247, 230, .30) 46%,
    rgba(255, 247, 230, .62) 76%, rgba(255, 247, 230, .80) 100%);
}
/* `background-image`, NOT the `background` SHORTHAND.
   The shorthand resets every background longhand it does not mention, so writing the gradients
   with it silently reset size to `auto`, position to `0% 0%` and repeat to `repeat`. Being two
   classes and later in the file, it beat `.title-scene` above; `paintArt()` then set the PNG as
   an inline `background-image`, replacing the gradients but INHERITING those resets — so the
   title art rendered at natural size, pinned top-left, and TILED. The longhand cannot do that,
   because it is incapable of touching anything but the image. Same family as the `.motif`
   compositing bug below: a fallback that does not fully stand down when the real art arrives. */
.title-scene.css-fallback {
  background-image:
    radial-gradient(circle at 20% 85%, rgba(242, 160, 61, .35) 0 18%, transparent 19%),
    radial-gradient(circle at 82% 78%, rgba(169, 123, 214, .32) 0 14%, transparent 15%),
    radial-gradient(circle at 50% 92%, rgba(62, 201, 167, .3) 0 16%, transparent 17%),
    linear-gradient(180deg, #fff7e6 0%, #ffe6c0 100%);
}
.title-inner {
  position: relative;
  width: 100%;
  padding: 20px 22px calc(24px + env(safe-area-inset-bottom, 0));
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: center;
}
.logo { display: flex; flex-direction: column; align-items: center; gap: 6px; margin: 0 0 2px; }
.logo-mark {
  width: 92px; height: 92px; border-radius: 26px;
  background: linear-gradient(150deg, var(--dog) 0%, var(--cat) 45%, var(--fish) 100%);
  border: 4px solid #fff;
  box-shadow: var(--shadow);
  background-size: cover;
  background-position: center;
  position: relative;
}
.logo-mark::after {
  /* paw motif, pure CSS — art is optional everywhere, including the logo */
  content: "";
  position: absolute; inset: 0;
  background:
    radial-gradient(circle at 50% 62%, #fff 0 22%, transparent 23%),
    radial-gradient(circle at 30% 34%, #fff 0 9%, transparent 10%),
    radial-gradient(circle at 47% 26%, #fff 0 9%, transparent 10%),
    radial-gradient(circle at 65% 32%, #fff 0 9%, transparent 10%);
  opacity: .92;
}
.logo-mark.has-art::after { content: none; }
.logo-word { font-size: 44px; font-weight: 900; letter-spacing: -1px; line-height: 1; }
.logo-pet { color: var(--ink); }
.logo-wars { color: #d4720f; }
.tagline { margin: 0 0 8px; color: var(--ink-soft); text-align: center; font-size: 14px; max-width: 300px; }
.menu { width: 100%; max-width: 340px; display: flex; flex-direction: column; gap: 10px; }
.title-foot {
  display: flex; align-items: center; gap: 10px; margin-top: 8px;
  width: 100%; max-width: 340px; justify-content: space-between;
}

/* starter class pick */
.crests { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.crest {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 12px 8px; min-height: 120px;
  border: 3px solid var(--cls); border-radius: var(--r-lg);
  background: linear-gradient(180deg, #fff, color-mix(in srgb, var(--cls) 18%, #fff));
  cursor: pointer; font: inherit; color: var(--ink);
}
.crest .crest-art {
  width: 56px; height: 56px; border-radius: 18px;
  background: var(--cls) center/cover;
  border: 3px solid #fff; box-shadow: var(--shadow-sm);
  position: relative; overflow: hidden;
}
.crest .crest-name { font-weight: 900; font-size: 16px; }
.crest .crest-sub { font-size: 11px; color: var(--ink-soft); text-align: center; line-height: 1.25; }
.crest[aria-pressed="true"] { box-shadow: 0 0 0 4px color-mix(in srgb, var(--cls) 50%, #fff); }

/* ---------------------------------------------------------------- ladder */

.rival {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  padding: 10px; margin-bottom: 10px;
  background: var(--card);
  border: 3px solid var(--cls);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  font: inherit; color: var(--ink); cursor: pointer;
  min-height: 84px;
}
.rival .portrait {
  width: 64px; height: 64px; flex: 0 0 64px; border-radius: 18px;
  background: var(--cls) center/cover; border: 3px solid #fff;
  position: relative; overflow: hidden;
}
.rival .r-body { flex: 1 1 auto; min-width: 0; }
.rival .r-name { font-weight: 900; font-size: 17px; }
.rival .r-shop { font-size: 12px; color: var(--ink-soft); }
.rival .r-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.tag {
  font-size: 11px; font-weight: 800; padding: 3px 8px; border-radius: 999px;
  background: color-mix(in srgb, var(--cls) 22%, #fff);
  border: 1px solid color-mix(in srgb, var(--cls) 60%, #fff);
}
.tag.bonus { background: #fff2c4; border-color: #e0b73c; color: #7a5605; }
.tag.beaten { background: #e7f6e9; border-color: #93cf9f; color: #2f6b3d; }
.pips { display: inline-flex; gap: 3px; vertical-align: middle; }
.pips i {
  width: 8px; height: 8px; border-radius: 50%; display: inline-block;
  background: #d9c6ac; border: 1px solid #b79b78;
}
.pips i.on { background: var(--cls); border-color: color-mix(in srgb, var(--cls) 60%, #000); }

/* ------------------------------------------------------------ lobby (online) */

/* `hidden` MUST actually hide. `.btn-big { display: block }` is an AUTHOR rule and the browser's
   own `[hidden] { display: none }` is a USER-AGENT one, so the author rule wins and the online
   button would sit there for every signed-out player — exactly the entry point that can only ever
   produce an error. One line, and it holds for every future `hidden` in the app. */
[hidden] { display: none !important; }

/* Two buttons where there used to be one. Bars that still hold a single button are unchanged:
   one flex child with `flex: 1 1 0` fills the row exactly as `width: 100%` did. */
.bottombar { display: flex; gap: 8px; }
.bottombar > .btn { flex: 1 1 0; min-width: 0; }
.btn-online { background: linear-gradient(180deg, #fff 0%, #e8f4ff 100%); border-color: #6aa9d8; }
.btn-refresh { font-size: 20px; line-height: 1; padding: 0 12px; background: rgba(255, 255, 255, .85); }

.lobby-note { margin: 0 0 10px; font-size: 13px; color: var(--ink-soft); line-height: 1.4; }
.lobby-err { margin: 0 0 10px; font-size: 13px; font-weight: 800; color: var(--warn); }
.lobby-empty { margin: 20px 6px; text-align: center; font-size: 14px; font-weight: 700; color: var(--ink-soft); }
.lobby-card {
  background: var(--card); border: 3px solid var(--line); border-radius: var(--r-lg);
  padding: 14px; margin-bottom: 10px; box-shadow: var(--shadow); text-align: center;
}
.lobby-card.hosting { border-color: var(--gold); }
.lc-title { font-weight: 900; font-size: 17px; }
.lc-sub { margin: 4px 0 10px; font-size: 13px; line-height: 1.4; color: var(--ink-soft); }
.lc-go {
  flex: 0 0 auto; margin-left: auto; font-weight: 900; font-size: 14px;
  padding: 8px 12px; border-radius: 999px;
  background: color-mix(in srgb, var(--cls) 30%, #fff);
  border: 2px solid var(--cls);
}

/* The wait has to look alive or the player assumes it has broken. Decoration only: under
   reduced-motion the dots hold still and the sentence above them still says what is happening. */
.lc-dots { display: flex; gap: 6px; justify-content: center; margin-bottom: 10px; }
.lc-dots i {
  width: 9px; height: 9px; border-radius: 50%; background: var(--shelf-dark);
  animation: lcPulse 1.2s ease-in-out infinite;
}
.lc-dots i:nth-child(2) { animation-delay: .18s; }
.lc-dots i:nth-child(3) { animation-delay: .36s; }
@keyframes lcPulse {
  0%, 100% { opacity: .3; transform: scale(.85); }
  50%      { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .lc-dots i { animation: none; opacity: .7; } }

/* An open match IS a rival row — same shape, same 44px-plus touch target, one extra chip. A second
   card component would drift from that one the first time either was touched. */
.match-row { min-height: 76px; }

/* ---------------------------------------------------------------- battle */

/* The board's backdrop. `pen_backdrop` and `shop_counter` were generated months ago and
   referenced by nothing — the battle screen has been flat cream this whole time.
   Deliberately faint and NON-INTERACTIVE: this sits under a whole match, so the moment it
   competes with a card for attention it is worse than the cream it replaced. `position:fixed`
   rather than absolute so it does not scroll with the (scrollable) battle grid, and z-index 0
   with the grid's own children above it. */
.battle-bg {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background-size: cover; background-position: center; background-repeat: no-repeat;
  opacity: 0; transition: opacity .8s ease;
}
.battle-bg.has-art { opacity: .18; }
/* Everything the player actually reads sits above it. */
#screen-battle > *:not(.battle-bg) { position: relative; z-index: 1; }

#screen-battle {
  display: none;
  /* minmax(0, ...) rather than minmax(96px, ...): a hard 96px floor on both pens, plus the
     strips, the action bar and a 132px hand, summed to ~580px of UNSHRINKABLE rows. Inside an
     `overflow:hidden` app that is not a squeeze, it is a CLIP — the last rows simply cease to
     exist, which is what hid End Turn. Rows that can yield let the layout fit first and scroll
     only as a last resort. */
  grid-template-rows: auto minmax(0, 1.1fr) auto minmax(0, 1.1fr) auto auto auto;
  /* The last resort, and the second half of the bug report: if the content still does not fit
     — a landscape phone is ~360px tall — there must be a way to reach it. */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  gap: 4px;
  padding: env(safe-area-inset-top, 0) 6px calc(4px + env(safe-area-inset-bottom, 0));
  background:
    radial-gradient(90% 45% at 50% 42%, #fffaf0 0%, rgba(255, 250, 240, 0) 72%),
    linear-gradient(180deg, #f6e3c6 0%, #ffefd6 40%, #ffe7c4 100%);
}
#screen-battle.on { display: grid; }

.battle-top { display: flex; align-items: center; gap: 6px; padding: 4px 0; }

.shop-strip {
  flex: 1 1 auto;
  /* min-width: 0 is REQUIRED, not tidiness. A flex item will not shrink below its content's
     min-content width by default, and this one contains "Barkley of The Kennel" — so the row
     refused to fit a 375px phone and pushed ~21px of itself (the timer, the Heart chip, the edge
     of End Turn) past the right edge, where the app's overflow:hidden silently cut it off. It
     also means the `text-overflow: ellipsis` already set on .r-name below never engaged: an
     ellipsis cannot happen in a box that is never asked to be narrower than its text. */
  min-width: 0;
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px;
  min-height: 56px;
  border-radius: var(--r-lg);
  border: 3px solid var(--cls);
  background: linear-gradient(180deg, #fff, color-mix(in srgb, var(--cls) 14%, #fff));
  box-shadow: var(--shadow-sm);
}
.shop-strip .portrait {
  width: 42px; height: 42px; flex: 0 0 42px; border-radius: 14px;
  background: var(--cls) center/cover; border: 2px solid #fff; overflow: hidden; position: relative;
}
.shop-strip .s-body { flex: 1 1 auto; min-width: 0; }
.shop-strip .s-name {
  font-weight: 900; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0;
}
.shop-strip .s-sub { font-size: 11px; color: var(--ink-soft); }
.stat {
  display: inline-flex; align-items: center; gap: 4px;
  font-weight: 900; font-size: 16px; padding: 2px 8px; border-radius: 999px;
  border: 2px solid #d06a6a; background: #ffe9e9; color: #8d2c2c;
}
.stat.armor { border-color: #7aa3c8; background: #e8f2fb; color: #245680; }
.stat .glyph { font-size: 13px; }

.timer {
  flex: 0 0 auto; width: 46px; height: 46px; border-radius: 50%;
  display: grid; place-items: center; font-weight: 900; font-size: 15px;
  background: conic-gradient(var(--gold) calc(var(--t, 1) * 360deg), #e6d3b6 0);
  border: 3px solid #b9821b; color: #4a3413;
}
.timer > span {
  width: 34px; height: 34px; border-radius: 50%; background: #fff8e6;
  display: grid; place-items: center;
}
.timer.low { border-color: var(--warn); }
.timer.hidden { visibility: hidden; }

.pen {
  min-height: 0;   /* without this a grid item will not shrink below its content */
  display: flex; align-items: center; justify-content: center;
  gap: 4px; padding: 4px;
  border-radius: var(--r-lg);
  border: 2px dashed #dcc09a;
  background: rgba(255, 255, 255, .42) center/cover;
  min-height: 96px;
  overflow: hidden;
}
.pen.pen-me { border-style: solid; border-color: #e0c39c; background-color: rgba(255, 255, 255, .62); }
.pen.drop-live { border-color: var(--good); background-color: #f0fbef; }
.pen-empty { color: var(--ink-soft); font-size: 12px; font-weight: 700; opacity: .75; }

.pet {
  position: relative;
  flex: 0 1 auto;
  width: 54px; min-width: 44px; height: 84px;
  border-radius: 12px;
  border: 3px solid var(--cls);
  background: linear-gradient(180deg, #fff, color-mix(in srgb, var(--cls) 22%, #fff));
  box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column; align-items: center;
  padding: 2px; font: inherit; color: var(--ink); cursor: pointer;
  touch-action: none;
}
.pet .p-art {
  width: 100%; flex: 1 1 auto; border-radius: 8px; overflow: hidden;
  background: color-mix(in srgb, var(--cls) 30%, #fff) center/cover; position: relative;
}
.pet .p-name {
  font-size: 8px; font-weight: 800; line-height: 1.05; text-align: center;
  width: 100%; max-height: 18px; overflow: hidden; margin-top: 1px;
}
.pet .p-stats {
  position: absolute; left: -3px; right: -3px; bottom: -6px;
  display: flex; justify-content: space-between; pointer-events: none;
}
.chip {
  min-width: 20px; height: 20px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 900; color: #fff;
  border: 2px solid #fff; box-shadow: var(--shadow-sm);
}
.chip.bite { background: #e08a2c; }
.chip.heart { background: #d8544f; }
.chip.heart.hurt { background: #b03b36; }
.chip.cost { background: #4b8fd6; }
.badges {
  position: absolute; top: -6px; left: -4px; right: -4px;
  display: flex; flex-wrap: wrap; gap: 2px; justify-content: center; pointer-events: none;
}
.badge {
  font-size: 9px; font-weight: 900; line-height: 1;
  padding: 2px 4px; border-radius: 6px;
  background: #fff; border: 1.5px solid var(--cls); color: var(--ink);
  box-shadow: var(--shadow-sm);
}
.badge.kw-guard { background: #dfeaff; border-color: #5a86c9; }
.badge.kw-flight { background: #e2f6ff; border-color: #4fc3f7; }
.badge.kw-bubble { background: #e6fffa; border-color: #3ec9a7; }
.badge.kw-venom { background: #eaf8e4; border-color: #7cc46a; }
.badge.kw-shed { background: #f0f7e8; border-color: #98c07c; }
.badge.kw-nine_lives { background: #f4ebff; border-color: #a97bd6; }
.badge.kw-sleepy { background: #efe9df; border-color: #a5917a; }
.badge.gear { background: #fff3dd; border-color: #d9a441; }

.pet.ready::after {
  content: ""; position: absolute; inset: -5px; border-radius: 15px;
  border: 3px solid var(--good); pointer-events: none;
  animation: readyPulse 1.6s ease-in-out infinite;
}
.pet.sel { transform: translateY(-6px); }
.pet.sel::after {
  content: ""; position: absolute; inset: -6px; border-radius: 16px;
  border: 4px solid #1f6fd0; pointer-events: none;
}
.pet.legal { animation: legalPulse 1.1s ease-in-out infinite; }
.pet.legal::before {
  content: ""; position: absolute; inset: -6px; border-radius: 16px;
  border: 4px dashed var(--warn); pointer-events: none;
}
.pet.dim { opacity: .42; filter: saturate(.5); }
.pet.hit { animation: shake .28s ease; }
.pet.arriving { animation: pop .3s cubic-bezier(.2, 1.4, .4, 1); }
.shop-strip.legal { box-shadow: 0 0 0 4px var(--warn); }
.shop-strip.hit { animation: shake .3s ease; }

.slot-marker {
  flex: 0 0 auto; width: 20px; height: 74px; border-radius: 10px;
  border: 3px dashed var(--good); background: rgba(62, 165, 95, .12);
  display: none; place-items: center; cursor: pointer;
  color: var(--good); font-weight: 900; font-size: 14px;
  touch-action: none;
}
.placing .slot-marker { display: grid; }

.rail {
  display: flex; align-items: center; gap: 8px; padding: 0 6px; min-height: 26px;
}
.ticker {
  flex: 1 1 auto; font-size: 12px; font-weight: 700; color: var(--ink-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.turn-flag {
  flex: 0 0 auto; font-size: 12px; font-weight: 900;
  padding: 3px 10px; border-radius: 999px;
  background: #fff; border: 2px solid var(--line);
}
.turn-flag.mine { background: #eafaee; border-color: var(--good); color: #256b3a; }
/* Online, the pause belongs to a person rather than to a 650 ms timer, and it can last as long as
   they need. It gets its own colour so "nothing is happening" never reads as "nothing works". */
.turn-flag.waiting { background: #fff6e0; border-color: var(--gold); color: #7a5605; }

.actionbar {
  display: flex; align-items: center; gap: 8px; padding: 2px 2px 0;
}
.hero-btn, .end-btn { flex: 0 0 auto; min-height: var(--tap); padding: 8px 12px; font-size: 14px; }
.hero-btn {
  display: flex; flex-direction: column; align-items: center; gap: 1px; line-height: 1.1;
  border-color: var(--cls); background: linear-gradient(180deg, #fff, color-mix(in srgb, var(--cls) 25%, #fff));
  max-width: 40%;
}
.hero-btn .hp-name { font-size: 12px; font-weight: 900; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.hero-btn .hp-cost { font-size: 11px; color: var(--ink-soft); }
.end-btn { margin-left: auto; background: linear-gradient(180deg, #ffd280, var(--gold)); border-color: #b9821b; }
.end-btn.waiting { background: linear-gradient(180deg, #eee, #ddd); border-color: #bbb; }

.treats {
  /* A ROW, not a column. The stacked version added ~14px to the action bar, which on a short
     handset is the difference between End Turn being on screen and not. */
  flex: 1 1 auto; display: flex; flex-wrap: wrap; align-items: center;
  justify-content: center; gap: 3px 8px; min-width: 0;
}
.treat-pips { display: flex; flex-wrap: wrap; gap: 3px; justify-content: center; }
/* The numeral is the primary readout and the pips are the at-a-glance texture, so it is sized
   like a stat rather than like a caption — tabular figures so it does not jitter as it counts. */
.treat-count {
  display: flex; align-items: baseline; gap: 1px; line-height: 1;
  font-variant-numeric: tabular-nums; letter-spacing: -0.3px;
}
.treat-have { font-size: 19px; font-weight: 800; color: #e8a020; }
.treat-sep  { font-size: 13px; color: var(--ink-soft); }
.treat-max  { font-size: 13px; font-weight: 700; color: var(--ink-soft); }
.treats.spent .treat-have { color: var(--ink-soft); }
.treat {
  width: 13px; height: 13px; border-radius: 4px 4px 6px 6px;
  background: #e9dcc4; border: 1.5px solid #c6ad86;
}
.treat.full { background: radial-gradient(circle at 35% 30%, #ffe9a8, #e8a020); border-color: #a9761a; }
.treat.temp { background: radial-gradient(circle at 35% 30%, #d9f7e6, #3ec9a7); border-color: #2b9d81; }

.hand {
  display: flex; justify-content: center; align-items: flex-end;
  min-height: 132px; padding: 6px 2px 2px;
  overflow: visible;
}
.hand .card { margin-left: var(--fan-overlap); }
.hand .card:first-child { margin-left: 0; }

/* ---------------------------------------------------------------- card face */

.card {
  position: relative;
  width: 88px; height: 124px; flex: 0 0 88px;
  border-radius: var(--r);
  border: 3px solid var(--cls);
  background: linear-gradient(180deg, var(--card) 0%, color-mix(in srgb, var(--cls) 16%, var(--card)) 100%);
  box-shadow: var(--shadow);
  padding: 3px;
  display: flex; flex-direction: column;
  font: inherit; color: var(--ink); text-align: left; cursor: pointer;
  transition: transform .12s ease;
  touch-action: none;
  transform-origin: bottom center;
}
.card .c-art {
  width: 100%; height: 46px; border-radius: 9px; overflow: hidden;
  background: color-mix(in srgb, var(--cls) 26%, #fff) center/cover;
  position: relative; flex: 0 0 auto;
}
.card .c-name {
  font-size: 9.5px; font-weight: 900; line-height: 1.1; margin-top: 3px;
  max-height: 22px; overflow: hidden;
}
.card .c-text {
  font-size: 8px; line-height: 1.15; color: var(--ink-soft);
  flex: 1 1 auto; overflow: hidden; margin-top: 2px;
}
/* DECORATIONS SIT ABOVE THE ART. `cardFace()` appends the cost chip and the rarity gem BEFORE
   the art box, and all three are `position:absolute` with `z-index:auto` — so they stack in DOM
   order and the art, being last, paints straight over the cost. It hid on desktop only because
   the chip hangs outside the frame there; the phone rule that tucks decorations INSIDE the card
   moves it under the art's `inset:0` box, where it vanished. Ordering by z-index rather than by
   append order also means a future reshuffle of cardFace() cannot silently re-bury it. */
.card .c-art, .card .art-img, .card .motif { z-index: 0; }
.card .c-cost, .card .c-gem, .card .c-stats, .card .badges { z-index: 2; }
.card .c-cost { position: absolute; top: -7px; left: -7px; width: 24px; height: 24px; font-size: 13px; }
.card .c-cost.cheap { background: #2f8a54; }
.card .c-cost.dear { background: #b0483f; }
.card .c-stats { position: absolute; bottom: -7px; left: -5px; right: -5px; display: flex; justify-content: space-between; }
.card .c-gem {
  position: absolute; top: -6px; right: -6px; width: 15px; height: 15px;
  border-radius: 4px; transform: rotate(45deg); border: 2px solid #fff; box-shadow: var(--shadow-sm);
}
.gem-common { background: #b9b2a6; }
.gem-rare { background: #4b8fd6; }
.gem-epic { background: #a55bd6; }
.gem-legendary { background: linear-gradient(140deg, #ffd76a, #e0821c); }
.card .c-kw {
  position: absolute; bottom: 16px; left: 2px; right: 2px;
  display: flex; flex-wrap: wrap; gap: 2px; pointer-events: none;
}
.card.playable { box-shadow: 0 0 0 3px rgba(62, 165, 95, .55), var(--shadow); }
.card.unplayable { filter: grayscale(.45) brightness(.96); opacity: .8; }
.card.sel { transform: translateY(-16px) scale(1.06); z-index: 5; }
.card.dragging { opacity: .25; }
.card.wide { width: 100%; height: auto; min-height: 92px; flex-direction: row; gap: 8px; padding: 8px 10px 8px 14px; }
.card.wide .c-art { width: 56px; height: 56px; flex: 0 0 56px; }
.card.wide .c-body { flex: 1 1 auto; min-width: 0; }
.card.wide .c-name { font-size: 14px; max-height: none; margin-top: 0; }
.card.wide .c-text { font-size: 11px; }
.card.wide .c-stats { position: static; margin-top: 4px; justify-content: flex-start; gap: 6px; }
.card.wide .c-cost { top: 8px; left: -10px; }

/* The CSS card face — used whenever art is missing. Not a placeholder: a real face. */
.motif { position: absolute; inset: 0; display: block; }

/* ONCE THE PNG HAS PAINTED, THE MOTIF MUST GO.
 *
 * This was a real, shipped compositing bug and the mechanism is worth stating, because the code
 * reads as though it already handles it. `paintArt()` sets the artwork as a CSS
 * `background-image` ON THE CONTAINER, while the motif is a CHILD element of that same
 * container — and a background paints BEHIND its element's children. So the fallback's
 * `fill: var(--cls); opacity: .3` full-bleed rect, plus its 0.85-opacity blob, were drawn ON TOP
 * of every piece of card art in the game: a translucent class-coloured shape over the picture,
 * purple on Cats, amber on Dogs, aqua on Fish.
 *
 * `has-art` is added only after the image actually decodes, so hiding the motif under it keeps
 * the graceful-degradation contract exactly as it was: no art, or art that fails to load, and
 * the motif is still the face. */
.has-art > .motif { display: none; }
.art-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* ---------------------------------------------------------------- collection */

.filters { display: flex; gap: 6px; padding: 8px 10px 4px; overflow-x: auto; flex: 0 0 auto; }
.filters::-webkit-scrollbar { height: 0; }
.chip-btn {
  font: inherit; font-size: 12px; font-weight: 800;
  padding: 8px 12px; min-height: 38px; border-radius: 999px;
  border: 2px solid var(--line); background: #fff; color: var(--ink);
  white-space: nowrap; cursor: pointer;
}
.chip-btn[aria-pressed="true"] {
  background: color-mix(in srgb, var(--cls) 32%, #fff);
  border-color: var(--cls);
}
.completion { padding: 2px 12px 6px; font-size: 12px; color: var(--ink-soft); font-weight: 700; flex: 0 0 auto; }
.bar {
  height: 8px; border-radius: 999px; background: #e8d9c0; overflow: hidden; margin-top: 4px;
  border: 1px solid var(--line);
}
.bar > i { display: block; height: 100%; background: linear-gradient(90deg, var(--gold), #e8802a); }

.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 10px 8px; align-content: start; }
.cell { position: relative; display: flex; flex-direction: column; align-items: center; gap: 3px; }
.cell .owned { font-size: 11px; font-weight: 900; color: var(--ink-soft); }
.cell.locked .card { filter: grayscale(1) brightness(.86); opacity: .55; }
.cell.locked .owned { color: #a08e7a; }
.cell .card { width: 100%; flex: 1 1 auto; height: 128px; }

/* ---------------------------------------------------------------- shop */

.shop-body { display: flex; flex-direction: column; gap: 12px; align-items: center; }
/* NOT `.pack`. Keyword names are game data and `pack` is one of the 14 keywords, so a component
   claiming that name is a collision waiting to happen from either side — it already fired once
   (see ui.js, the badge that grew to 190x250 in the enemy pen). Named for what it is instead. */
.booster {
  width: 190px; height: 250px; border-radius: 20px;
  border: 4px solid #b9821b;
  background:
    radial-gradient(circle at 50% 34%, #fff6d8 0 22%, transparent 23%),
    linear-gradient(160deg, var(--dog) 0%, var(--cat) 38%, var(--bird) 66%, var(--fish) 100%);
  background-size: cover; background-position: center;
  box-shadow: var(--shadow); position: relative; display: grid; place-items: center;
  color: #fff; font-weight: 900; font-size: 22px; text-shadow: 0 2px 4px rgba(0, 0, 0, .35);
}
.booster.shake { animation: wiggle .5s ease-in-out infinite; }
.shop-note { font-size: 13px; color: var(--ink-soft); text-align: center; max-width: 320px; line-height: 1.4; }
.ribbon {
  background: linear-gradient(180deg, #ffe9a8, #f3c64a); border: 2px solid #b9821b;
  border-radius: 999px; padding: 6px 14px; font-weight: 900; color: #6b4a06;
}
.odds { width: 100%; max-width: 340px; border-collapse: collapse; font-size: 13px; }
.odds td { padding: 5px 8px; border-bottom: 1px solid var(--line); }
.odds td:last-child { text-align: right; font-weight: 900; }

/* pack opening */
.reveal { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.reveal-row { display: flex; gap: 6px; justify-content: center; }
.reveal-slot {
  width: 46px; height: 62px; border-radius: 9px; border: 2px dashed #c9ad86;
  background: rgba(255, 255, 255, .4);
}
.reveal-slot.done { border-style: solid; border-color: var(--cls); background: color-mix(in srgb, var(--cls) 30%, #fff); }
.reveal-stage { min-height: 210px; display: grid; place-items: center; }
.reveal-stage .card { width: 132px; height: 186px; }
.reveal-stage .card .c-art { height: 74px; }
.reveal-stage .card .c-name { font-size: 13px; max-height: 30px; }
.reveal-stage .card .c-text { font-size: 10px; }
.reveal-stage .card.flip { animation: flipIn .42s cubic-bezier(.2, 1.3, .4, 1); }
.reveal-stage .card.rare-hit { box-shadow: 0 0 0 5px #4b8fd6, var(--shadow); }
.reveal-stage .card.epic-hit { box-shadow: 0 0 0 6px #a55bd6, 0 0 30px 6px rgba(165, 91, 214, .6); }
.reveal-stage .card.legendary-hit {
  box-shadow: 0 0 0 6px #f0b429, 0 0 42px 10px rgba(240, 180, 41, .7);
  animation: flipIn .42s cubic-bezier(.2, 1.3, .4, 1), legendary 1.6s ease-in-out .4s infinite;
}
.reveal-label { font-weight: 900; font-size: 14px; min-height: 20px; }
.reveal-coins {
  width: 132px; height: 186px; border-radius: var(--r); display: grid; place-items: center;
  border: 3px solid #b9821b; background: linear-gradient(180deg, #fff6d8, #f3c64a);
  font-weight: 900; font-size: 20px; color: #6b4a06; text-align: center; padding: 10px;
}
.new-flag {
  position: absolute; top: 4px; left: 50%; transform: translateX(-50%);
  background: var(--good); color: #fff; font-size: 9px; font-weight: 900;
  padding: 2px 6px; border-radius: 999px; border: 2px solid #fff;
}

/* ---------------------------------------------------------------- deckbuilder */

.deck-body { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.deck-list { padding: 10px; overflow-y: auto; flex: 1 1 auto; }
.deck-row {
  display: flex; align-items: center; gap: 10px; padding: 10px; margin-bottom: 8px;
  background: var(--card); border: 3px solid var(--cls); border-radius: var(--r);
  box-shadow: var(--shadow-sm);
}
.deck-row .d-body { flex: 1 1 auto; min-width: 0; cursor: pointer; }
.deck-row .d-name { font-weight: 900; }
.deck-row .d-sub { font-size: 12px; color: var(--ink-soft); }
.deck-row .d-badge { font-size: 11px; font-weight: 900; padding: 3px 8px; border-radius: 999px; }
.d-badge.ok { background: #e7f6e9; color: #2f6b3d; border: 1px solid #93cf9f; }
.d-badge.bad { background: #ffe9e9; color: #8d2c2c; border: 1px solid #d8a2a2; }
.d-badge.active { background: #fff2c4; color: #7a5605; border: 1px solid #e0b73c; }

.edit-head { padding: 8px 10px; background: var(--paper-2); border-bottom: 2px solid var(--line); flex: 0 0 auto; }
.curve { display: flex; align-items: flex-end; gap: 3px; height: 46px; margin-top: 6px; }
.curve .col { flex: 1 1 0; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.curve .col > i {
  display: block; width: 100%; border-radius: 4px 4px 0 0;
  background: linear-gradient(180deg, var(--cls), color-mix(in srgb, var(--cls) 55%, #7a5a2c));
  min-height: 2px;
}
.curve .col > span { font-size: 9px; color: var(--ink-soft); font-weight: 800; }
.deck-count-line { display: flex; align-items: baseline; gap: 8px; font-weight: 900; }
.deck-count-line .n { font-size: 22px; }
.deck-count-line .n.bad { color: var(--warn); }
.deck-count-line .n.ok { color: var(--good); }
.deck-problems { font-size: 11px; color: var(--warn); font-weight: 700; margin-top: 4px; }

.pool { flex: 1 1 auto; overflow-y: auto; padding: 8px; min-height: 0; }
.pool-row {
  display: flex; align-items: center; gap: 8px; padding: 6px 8px; margin-bottom: 6px;
  background: #fff; border: 2px solid var(--cls); border-radius: var(--r-sm);
  border-left-width: 8px;
}
.pool-row .pr-cost {
  width: 26px; height: 26px; flex: 0 0 26px; border-radius: 50%; background: #4b8fd6;
  color: #fff; font-weight: 900; display: grid; place-items: center; font-size: 13px;
}
.pool-row .pr-body { flex: 1 1 auto; min-width: 0; cursor: pointer; }
.pool-row .pr-name { font-weight: 800; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pool-row .pr-sub { font-size: 10.5px; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pool-row.maxed { opacity: .55; }
.stepper { display: flex; align-items: center; gap: 4px; }
.stepper button { width: 40px; height: 40px; padding: 0; font-size: 20px; border-radius: 10px; }
.stepper .n { min-width: 26px; text-align: center; font-weight: 900; }

/* ---------------------------------------------------------------- game over */

#screen-over { align-items: center; justify-content: center; }
.over-inner { text-align: center; padding: 24px; display: flex; flex-direction: column; gap: 14px; align-items: center; }
.over-title { font-size: 40px; font-weight: 900; margin: 0; line-height: 1; }
.over-title.win { color: #2f8a54; }
.over-title.loss { color: #b0483f; }
.over-title.draw { color: #7a6350; }
.over-sub { color: var(--ink-soft); max-width: 300px; line-height: 1.4; }
.coin-line {
  display: flex; align-items: center; gap: 8px; font-size: 24px; font-weight: 900; color: #6b4a06;
  background: linear-gradient(180deg, #ffe9a8, #f3c64a); border: 3px solid #b9821b;
  border-radius: 999px; padding: 8px 20px;
}

/* ---------------------------------------------------------------- overlays */

.overlays { position: fixed; inset: 0; pointer-events: none; z-index: 50; }
.sheet {
  position: absolute; inset: 0; pointer-events: auto;
  background: rgba(59, 43, 31, .55);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 16px; gap: 12px;
  animation: fadeIn .18s ease;
}
.sheet-panel {
  background: var(--paper); border-radius: var(--r-lg); border: 3px solid var(--shelf-dark);
  box-shadow: var(--shadow); padding: 14px; width: 100%; max-width: 380px;
  /* --vh, not vh: `88vh` on a phone is 88% of the LARGE viewport (URL bar hidden), so the panel
     itself extends past the fold and its own `overflow-y: auto` cannot save the content at the
     bottom — you can scroll the panel, but the panel's end is off-screen. Exactly the fault that
     hid End Turn, in a second place. Found by the device lab on its first run: the starter-class
     picker had Reptiles clipped at 375x560. */
  max-height: calc(var(--vh) - 32px); overflow-y: auto;
  overscroll-behavior: contain;
}
.sheet-title { font-size: 20px; font-weight: 900; margin: 0 0 4px; }
.sheet-sub { font-size: 13px; color: var(--ink-soft); margin: 0 0 10px; line-height: 1.35; }
.sheet-actions { display: flex; gap: 8px; margin-top: 12px; }
.sheet-actions .btn { flex: 1 1 0; }
.pick-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
.pick-row .card { width: 100px; height: 142px; }
.pick-row .card .c-art { height: 54px; }
.pick-row .card.kept { box-shadow: 0 0 0 4px var(--good), var(--shadow); }
.pick-row .card.tossed { opacity: .55; transform: translateY(6px) rotate(-2deg); }
.mull-hint { font-size: 12px; font-weight: 800; color: var(--ink-soft); }
.setting-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 2px; border-bottom: 1px solid var(--line);
}
.switch {
  width: 58px; height: 34px; border-radius: 999px; border: 2px solid var(--shelf-dark);
  background: #e6d3b6; position: relative; cursor: pointer; flex: 0 0 auto;
}
.switch::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 26px; height: 26px;
  border-radius: 50%; background: #fff; box-shadow: var(--shadow-sm); transition: left .15s ease;
}
.switch[aria-pressed="true"] { background: var(--good); }
.switch[aria-pressed="true"]::after { left: 26px; }
input[type="text"] {
  font: inherit; padding: 10px; min-height: var(--tap); width: 100%;
  border: 2px solid var(--line); border-radius: var(--r-sm); background: #fff; color: var(--ink);
}

.toast {
  position: fixed; left: 50%; bottom: calc(18px + env(safe-area-inset-bottom, 0));
  transform: translateX(-50%) translateY(20px);
  background: #3b2b1f; color: #fff6e8; padding: 10px 16px; border-radius: 999px;
  font-weight: 800; font-size: 13px; opacity: 0; pointer-events: none;
  z-index: 90; max-width: 92vw; text-align: center; transition: opacity .2s ease, transform .2s ease;
}
.toast.on { opacity: 1; transform: translateX(-50%) translateY(0); }

.drag-layer, .fx-layer { position: fixed; inset: 0; pointer-events: none; z-index: 70; overflow: hidden; }
.drag-ghost { position: absolute; transform: translate(-50%, -55%) scale(1.05); filter: drop-shadow(0 8px 12px rgba(0, 0, 0, .3)); }
.float {
  position: absolute; font-weight: 900; font-size: 18px; color: #fff;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .35); animation: floatUp .9s ease-out forwards;
  transform: translate(-50%, -50%); white-space: nowrap;
}
.float.dmg { color: #ff6b5e; }
.float.heal { color: #6fe0a0; }
.float.info { color: #ffe9a8; font-size: 14px; }
.float.home { color: #ffd9a0; font-size: 14px; }

/* ---------------------------------------------------------------- keyframes */

@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
@keyframes pop { from { transform: scale(.4); opacity: 0 } to { transform: scale(1); opacity: 1 } }
@keyframes shake {
  0%, 100% { transform: translateX(0) }
  25% { transform: translateX(-4px) }
  75% { transform: translateX(4px) }
}
@keyframes floatUp {
  0% { opacity: 0; transform: translate(-50%, -30%) scale(.7) }
  20% { opacity: 1; transform: translate(-50%, -60%) scale(1.15) }
  100% { opacity: 0; transform: translate(-50%, -160%) scale(1) }
}
@keyframes readyPulse { 0%, 100% { opacity: .35 } 50% { opacity: .95 } }
@keyframes legalPulse { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(-3px) } }
@keyframes wiggle { 0%, 100% { transform: rotate(-2deg) } 50% { transform: rotate(2deg) } }
@keyframes flipIn { from { transform: rotateY(80deg) scale(.7); opacity: 0 } to { transform: none; opacity: 1 } }
@keyframes legendary {
  0%, 100% { box-shadow: 0 0 0 6px #f0b429, 0 0 30px 6px rgba(240, 180, 41, .55) }
  50% { box-shadow: 0 0 0 6px #ffd76a, 0 0 52px 14px rgba(240, 180, 41, .85) }
}

/* Reduced motion: keep every state change, drop every movement. The game must be fully
   legible with animation off — that is an accessibility floor, not a preference. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}
body.no-motion *, body.no-motion *::before, body.no-motion *::after {
  animation-duration: .001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
}

/* ---------------------------------------------------------------- wider screens */

@media (min-width: 480px) {
  .card { width: 100px; height: 142px; flex-basis: 100px; }
  .card .c-art { height: 56px; }
  .card .c-name { font-size: 11px; }
  .card .c-text { font-size: 9px; }
  .pet { width: 68px; height: 100px; }
  .pet .p-name { font-size: 9px; }
  .crests { grid-template-columns: repeat(3, 1fr); }
  .hand { min-height: 152px; }
}
@media (min-height: 860px) {
  .pen { min-height: 120px; }
}


/* ═══════════════════════════════════════════════════════════ SMALL SCREENS
 *
 * Everything above this point sizes the game for a comfortable phone and then only ever scales
 * UP (min-width: 480, min-height: 860). Nothing scaled DOWN, and the battle screen's seven rows
 * summed to roughly 580px of minimums — more than a 360x640 handset has left after browser
 * chrome, and far more than a landscape phone's ~360px. Inside `overflow:hidden` that is not a
 * squeeze but a CLIP: the action bar and hand stopped existing, with no way to scroll to them.
 *
 * Three defences, in order of preference:
 *   1. FIT — shrink the pieces so the layout does not overflow in the first place.
 *   2. REACH — pin the action bar to the bottom of the scroll area, so End Turn is on screen
 *      whatever else is happening.
 *   3. SCROLL — the battle screen scrolls as a last resort (set on #screen-battle above).
 */

/* --- short: most handsets in portrait once the URL bar is showing ----------------- */
@media (max-height: 780px) {
  .card { width: 76px; height: 108px; flex-basis: 76px; }
  .hand { min-height: 112px; padding: 4px 2px 2px; }
  .pen { min-height: 76px; }
  .shop-strip { min-height: 48px; }
  .battle-top { padding: 2px 0; }
  #screen-battle { gap: 3px; }
}

/* --- very short: small handsets, and any phone in landscape ----------------------- */
@media (max-height: 620px) {
  .card { width: 64px; height: 92px; flex-basis: 64px; }
  .hand { min-height: 94px; }
  .pen { min-height: 62px; }
  .shop-strip { min-height: 42px; }
  .treat { width: 10px; height: 10px; }
  .hero-btn, .end-btn { min-height: 40px; padding: 6px 10px; font-size: 13px; }
  .treat-have { font-size: 17px; }
  /* THE ACTION BAR IS PINNED. Once the layout can genuinely overflow, End Turn must not be
     something you have to find — it is the one control a turn cannot end without. `order` moves
     it below the hand so it pins to the true bottom, and the backdrop keeps it legible over
     whatever scrolls beneath. */
  #screen-battle .actionbar {
    order: 1;
    position: sticky;
    bottom: 0;
    z-index: 6;
    padding: 4px 2px calc(4px + env(safe-area-inset-bottom, 0));
    background: linear-gradient(180deg, rgba(255, 231, 196, .82), var(--paper-2) 55%);
    backdrop-filter: blur(6px);
    border-top: 2px solid var(--line);
    border-radius: 12px 12px 0 0;
  }
}

/* --- landscape: vertical space is the scarce resource, horizontal is plentiful ----- */
@media (orientation: landscape) and (max-height: 560px) {
  /* The rail is commentary; it is the first thing to go when there is no room. */
  #screen-battle .rail { display: none; }
  .hand { min-height: 86px; }
  .card { width: 60px; height: 86px; flex-basis: 60px; }
  .app { max-width: none; }         /* use the whole width rather than boxing it at 720px */
}

/* --- a real safety net: if the viewport is so short that even the compact layout cannot fit,
 *     stop trying to fill it and let the page scroll properly. Without this the grid keeps
 *     distributing a height it does not have. ------------------------------------------------ */
@media (max-height: 480px) {
  #screen-battle {
    grid-template-rows: auto auto auto auto auto auto auto;
    align-content: start;
  }
  .pen { min-height: 56px; }
}


/* ---------------------------------------------------------------- accounts */
.acct-box {
  background: var(--paper-2); border: 2px solid var(--line); border-radius: var(--r);
  padding: 10px 12px; margin: 8px 0;
}
.acct-h { margin: 0 0 6px; font-size: 15px; }
.acct-row { display: flex; justify-content: space-between; gap: 10px; padding: 3px 0; font-size: 13px; }
.acct-k { color: var(--ink-soft); }
.acct-v { font-weight: 800; font-variant-numeric: tabular-nums; }
.acct-form { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
.acct-field { display: flex; flex-direction: column; gap: 3px; }
.acct-field input {
  font: inherit; font-size: 16px;            /* 16px: anything smaller makes iOS ZOOM on focus */
  padding: 10px 12px; border-radius: var(--r-sm);
  border: 2px solid var(--line); background: #fff; color: var(--ink);
  min-height: var(--tap);
}
.acct-field input:focus { outline: 3px solid var(--gold); outline-offset: 1px; }
.acct-msg { color: var(--warn); font-size: 13px; margin: 4px 0 0; min-height: 1.2em; }
#btn-account.signed-in { border-color: var(--good); color: var(--good); font-weight: 800; }


/* --- narrow phones: 375px is the common floor, and four 88px cards plus their overhanging
 *     cost bubbles and rarity gems do not fit it. Shrink the card rather than let the fan
 *     stack them into unreadable slivers. --------------------------------------------------- */
@media (max-width: 430px) {
  .card { width: 72px; height: 102px; flex-basis: 72px; }
  .card .c-art { height: 40px; }
  .card .c-cost { width: 21px; height: 21px; font-size: 12px; top: -6px; left: -6px; }
  .card .c-gem { width: 13px; height: 13px; top: -5px; right: -5px; }
  .hand { min-height: 106px; }
}


/* The battle top bar must never be the reason the layout overflows: it holds a name of unknown
 * length. Belt and braces alongside the min-width:0 above. */
.battle-top { min-width: 0; max-width: 100%; }
.battle-top > * { min-width: 0; }


/* On a phone, TUCK THE CARD DECORATIONS INSIDE the card. The cost bubble and rarity gem are
 * positioned outside the box on purpose — they read better overlapping the frame on a desktop —
 * but anything hanging outside a fanned card is width the fan maths does not own, and it ends up
 * clipped by the app's overflow:hidden with no scrollbar to hint that it happened. Inside the
 * bounds, the widest thing on screen is exactly the card, which is a number the fan already
 * fits. Measured: this is what took the last 60px of clipping to zero. */
@media (pointer: coarse), (max-width: 430px) {
  .card .c-cost { top: 2px; left: 2px; }
  .card .c-gem  { top: 3px; right: 3px; }
  .card .c-stats { bottom: 2px; left: 3px; right: 3px; }
}


/* ═══════════════════════════════════════════════════════════════════════ FX
 *
 * The particle canvas sits above the board and below the sheets, and is inert to input. It is a
 * SIBLING of #fx-layer's DOM effects rather than a replacement: floating numbers stay as DOM so
 * they inherit the page's font and remain selectable by a screen reader, while particles go to
 * canvas because five hundred DOM nodes a second is how you drop frames on a phone. */
.fx-canvas {
  position: fixed; inset: 0; pointer-events: none; z-index: 69;
  /* `lighter` compositing happens INSIDE the canvas; the canvas itself blends normally so the
     board underneath is not washed out. */
}

/* SQUASH AND STRETCH. The overshoot is the whole point: scaling straight to 1.0 reads as a jump,
 * while passing 1.0 and settling back reads as impact. --punch scales the effect so a big hit
 * visibly hits harder than a small one. */
@keyframes fxPunch {
  0%   { transform: scale(1); }
  35%  { transform: scale(calc(1 + 0.10 * var(--punch, 1))) rotate(calc(-1deg * var(--punch, 1))); }
  70%  { transform: scale(calc(1 - 0.04 * var(--punch, 1))); }
  100% { transform: scale(1); }
}

/* The rising number already existed; this makes it POP first, which is what makes it readable
 * during a busy burst instead of getting lost in the sparks. */
@keyframes floatUpPop {
  0%   { transform: translate(-50%, -50%) scale(.6); opacity: 0; }
  18%  { transform: translate(-50%, -60%) scale(1.25); opacity: 1; }
  32%  { transform: translate(-50%, -64%) scale(1); opacity: 1; }
  100% { transform: translate(-50%, -140%) scale(.95); opacity: 0; }
}
.float { animation: floatUpPop .95s cubic-bezier(.2, 1.2, .3, 1) forwards; }
.float.dmg  { color: #ffd9cf; text-shadow: 0 0 10px rgba(255, 90, 60, .9), 0 2px 0 rgba(0,0,0,.4); }
.float.heal { color: #d8ffe6; text-shadow: 0 0 10px rgba(60, 220, 120, .9), 0 2px 0 rgba(0,0,0,.4); }
.float.crit { font-size: 26px; }

/* MOTION SICKNESS IS A REAL CONSTRAINT, not a checkbox. Under reduced-motion the canvas keeps
 * drawing (fx.js already shortens lives and cuts counts) but nothing TRANSFORMS: no punch, no
 * overshoot, no travel on the numbers. The information stays; the movement goes. */
@media (prefers-reduced-motion: reduce) {
  @keyframes fxPunch { 0%, 100% { transform: none; } }
  .float { animation: floatUpPop .6s linear forwards; }
  @keyframes floatUpPop {
    0% { transform: translate(-50%, -50%); opacity: 0; }
    20%, 70% { transform: translate(-50%, -50%); opacity: 1; }
    100% { transform: translate(-50%, -50%); opacity: 0; }
  }
}

/* ==================================================================== living surfaces
 *
 * Three effects, all of them GPU-composited CSS rather than a WebGL context. That is a
 * deliberate choice, not a shortcut: these are layered gradients with blend modes, which is
 * what a fragment shader would be doing anyway, and CSS can blend BETWEEN DOM layers — a GL
 * canvas cannot without stacking more canvases. No second RAF loop, no third context on a
 * phone that is already running the game and the particle layer.
 *
 * Every one of them stands down completely under prefers-reduced-motion. An effect that cannot
 * be turned off is not an effect, it is a symptom.
 */

/* --- 1. HOLOGRAPHIC FOIL --------------------------------------------------------------------
 *
 * Rarity was a 15px diamond in the corner and nothing else, so the difference between a common
 * and a legendary — the entire point of a collection — was a colour most players never looked
 * at. A foil says it without a word, and it says it in the one place the eye already is.
 *
 * Two moving layers, because one is a stripe and two is an interference pattern: a fast, tight
 * rainbow sweep in `color-dodge` for the highlight, and a slow, wide hue field in `overlay` for
 * the shift underneath. Their periods are deliberately coprime-ish (7s / 11s) so the pair never
 * settles into a visible loop.
 *
 * COMMONS GET NOTHING. If everything shimmers, shimmering stops meaning "rare" and starts
 * meaning "this game has an effect", and the card you actually pulled lands flat.
 */
/* ON THE WHOLE CARD, and with PLAIN ALPHA. Two rewrites got us here and both failures are worth
   keeping, because they are the same failure in different clothing:

     1. `color-dodge` — divides by (1 - blend), so on bright pastel art it clips to white and
        changes nothing. Identical in kind to additive particles on the 247/255 board (QUAL-257).
     2. `overlay` on `.c-art` — better, but the in-game art strip is 46px TALL. A subtle sheen
        across 46 pixels is not subtle, it is absent.

   So: the sweep now crosses the ENTIRE card (88x124 in hand, and the whole face in the
   collection), and it composites NORMALLY. A blend mode makes the effect depend on what is
   underneath it; straight alpha does not, and "reads on any backdrop" is worth more here than
   physical plausibility. Verified by diffing screenshots with the layer on and off, not by
   reading computed styles — a rule that applies and paints nothing looks identical to a working
   one in an inspector. */
.card[data-rarity="rare"]::after,
.card[data-rarity="epic"]::after,
.card[data-rarity="legendary"]::after {
  content: '';
  position: absolute; inset: 0; z-index: 3;      /* over art AND decorations, under nothing */
  pointer-events: none;
  border-radius: inherit;
  background: linear-gradient(105deg,
    transparent 30%,
    rgba(255,255,255,.28) 42%,
    rgba(120,235,255,.42) 47%,
    rgba(255,140,225,.42) 52%,
    rgba(255,235,130,.42) 57%,
    rgba(255,255,255,.28) 62%,
    transparent 74%);
  background-size: 300% 100%;
  opacity: var(--foil-strength, .5);
  animation: foilSweep 4.5s ease-in-out infinite;
}
/* A CONSTANT BASE, under the moving sweep. The sweep alone spends most of its cycle off the card,
   so rarity read as "nothing" most of the time and as a highlight occasionally — which is a worse
   signal than the gem it was meant to improve on. Real foil cards work the same way: a permanent
   iridescent tint that says "this one is special" whenever you look, plus a travelling highlight
   that says it again when it catches the light. The tint is what makes rarity legible at a glance
   in a hand of six; the sweep is what makes it feel alive. */
.card[data-rarity="rare"],
.card[data-rarity="epic"],
.card[data-rarity="legendary"] {
  background-image:
    linear-gradient(150deg, var(--foil-a) 0%, transparent 38%, transparent 62%, var(--foil-b) 100%);
}
.card[data-rarity="rare"]      { --foil-a: rgba(90,175,255,.22);  --foil-b: rgba(150,220,255,.26); }
.card[data-rarity="epic"]      { --foil-a: rgba(180,110,255,.26); --foil-b: rgba(255,140,230,.28); }
.card[data-rarity="legendary"] { --foil-a: rgba(255,190,60,.34);  --foil-b: rgba(255,120,70,.3); }

.card[data-rarity="rare"]      { --foil-strength: .45; }
.card[data-rarity="epic"]      { --foil-strength: .7; }
.card[data-rarity="legendary"] { --foil-strength: .95; }

/* The legendary earns a second, slower pass on the opposite diagonal, so the two cross. */
.card[data-rarity="legendary"]::before {
  content: '';
  position: absolute; inset: 0; z-index: 3; pointer-events: none; border-radius: inherit;
  background: linear-gradient(255deg, transparent 38%, rgba(255,215,106,.5) 50%, transparent 62%);
  background-size: 300% 100%;
  animation: foilSweep 7s ease-in-out infinite reverse;
}
@keyframes foilSweep { 0% { background-position: 150% 0 } 60%, 100% { background-position: -50% 0 } }

/* --- 2. THE BOARD BREATHES -------------------------------------------------------------------
 *
 * The pens are two large flat rectangles that never change, which is most of the screen for most
 * of a match. A very slow warm drift underneath the backdrop art keeps them from reading as dead
 * space. Kept under 4% alpha and on a 24s period: the goal is that a player never NOTICES it,
 * only that the board does not feel like a screenshot. Anything you can consciously see here is
 * competing with the cards, which is the thing that must never happen. */
.pen::before {
  content: '';
  position: absolute; inset: 0; z-index: 0; pointer-events: none; border-radius: inherit;
  background:
    radial-gradient(120% 80% at 20% 15%, rgba(255,196,120,.05), transparent 60%),
    radial-gradient(110% 70% at 80% 85%, rgba(150,200,255,.045), transparent 60%);
  background-size: 180% 180%, 200% 200%;
  animation: penBreathe 24s ease-in-out infinite alternate;
}
@keyframes penBreathe {
  0%   { background-position:   0% 0%, 100% 100%; }
  100% { background-position: 100% 60%,   0%  40%; }
}

/* --- 3. THE ACTIONABLE CONTROL SHIMMERS ------------------------------------------------------
 *
 * Only when it is actually actionable, which is the entire point. A button that always shimmers
 * is decoration; a button that shimmers exactly when pressing it would do something is a HINT,
 * and it costs the player nothing to learn. `End Turn` on your turn, `Buy Pack` when you can
 * afford one. The sweep is a single translated highlight, so it composites on one layer. */
/* TARGETED BY ID, and that is the point. The first draft used `.btn-play`, which is the class on
   Host a match, Back to the lobby, Buy Pack and more — shimmering all of them would have made the
   effect mean "this is a button" instead of "pressing this does something now", and a hint that is
   always on is not a hint. `.btn-buy` did not exist at all: the pack button is `.btn-play` with an
   id, so that half of the rule selected nothing. Both of these controls already set `disabled`
   precisely when they are not actionable, so `:not(:disabled)` is the real signal, already
   maintained by the code that owns the state. */
#btn-end:not(:disabled)::after,
#btn-buy-pack:not(:disabled)::after {
  content: '';
  position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background: linear-gradient(105deg, transparent 35%, rgba(255,255,255,.42) 50%, transparent 65%);
  background-size: 250% 100%;
  animation: btnShimmer 3.4s ease-in-out infinite;
}
#btn-end, #btn-buy-pack { position: relative; overflow: hidden; }
@keyframes btnShimmer { 0% { background-position: 140% 0 } 55%, 100% { background-position: -40% 0 } }

/* --- the opt-out, and it is total ------------------------------------------------------------
 * Not "slower" or "subtler" — off. A player who asks the OS for less motion is not asking for a
 * tasteful amount of it. The foil keeps a STATIC sheen so a legendary still reads as special;
 * it simply stops moving. */
@media (prefers-reduced-motion: reduce) {
  .card[data-rarity]::after,
  .card[data-rarity="legendary"]::before { animation: none; }
  .card[data-rarity]::after { opacity: calc(var(--foil-strength, .5) * .55); background-position: 40% 0; }
  .pen::before, #btn-end:not(:disabled)::after, #btn-buy-pack:not(:disabled)::after {
    animation: none; display: none;
  }
}
