/*
 * css/game.css — styles for the game views owned by the frontend-game-agent:
 * battle arena, team cards, Pokémon detail, PC boxes, Poké Mart, Bag and Pokédex,
 * plus the variant (shiny / golden / dark) presentation used everywhere a sprite
 * is drawn. Loaded by index.html after base.css and layout.css, so it builds on the
 * base tokens (--panel, --accent, --hp …) and utility classes (.panel, .btn, .hpbar …).
 *
 * Look: "a touch of" Pokémon Scarlet/Violet menus — dark translucent navy panels,
 * rounded corners, a muted Pokéball-red selection colour (phase 2, DECISIONS.md 45)
 * — but modern and clean, not pixel-retro.
 * Sprites are the animated Gen 5 gifs and are always drawn with
 * image-rendering: pixelated so upscaling keeps the pixel look crisp.
 */

/* ------------------------------------------------------------------------- */
/* 1. Sprites and variants                                                    */
/* ------------------------------------------------------------------------- */

/*
 * .sprite-wrap is the positioning box around every Pokémon sprite so badges
 * (the shiny sparkle) can be anchored to its corner. Views add the variant class
 * to the wrapper (for the badge) and the sprite helper adds it to the <img>
 * (for the colour filter).
 */
.sprite-wrap {
  position: relative;
  display: inline-block;
  line-height: 0;
  vertical-align: middle;
}
.sprite-wrap img,
.sprite,
.dex-tile img,
.pc-tile img,
.team-card img,
.battle-sprite img {
  image-rendering: pixelated;
}

/* Golden, dark, frozen and shadow reuse the normal gif with a colour filter (DECISIONS.md #11:
 * no separate artwork). All four walk the same pipeline from css/base.css: grayscale wipes the
 * species' own colour, sepia lays down a neutral brightness base, saturate + hue-rotate dye it
 * and brightness/contrast close the curve. One form therefore looks the same on every species
 * (before, a golden Gyarados kept a blue cast and a dark one turned black on dark gifs).
 * Shiny uses the official shiny gif and gets a sparkle. Golden is amber gold, not lemon
 * yellow (owner, 22 September, DECISIONS 181). */
.variant-golden { filter: var(--form-golden-tint); }
.variant-dark   { filter: var(--form-dark-tint) var(--form-dark-glow); }
.variant-shiny  { /* no filter: the shiny folder already has the alternate palette */ }
/* Frozen (DECISIONS 149, recipe DECISIONS 181): the same pipeline, hue turned to ice, under a
   two-layer frost glow - not a hue rotation of the original colours, which turned a Gyarados
   pink and a Cyndaquil green. */
.variant-frozen { filter: var(--form-frozen-tint) var(--form-frozen-glow); }
/* Shadow (owner, 24 September, DECISIONS 330; Argent since the rename): the NORMAL sprite in
   black and white, lifted into silver rather than crushed to black, with a pale rim glow so it
   still reads as the Shadow form of Pokémon Colosseum / XD on a dark background. */
.variant-shadow { filter: var(--form-shadow-tint) var(--form-shadow-glow); }
/* Crystal / Prism (task 8): the NORMAL sprite lifted to a pale, cool glass tone - a light
   grayscale lift, not a hue rotation, so every species keeps its shapes and only gains the
   icy prism light the overlay below adds. */
.variant-crystal { filter: var(--form-crystal-tint) var(--form-crystal-glow); }

/* Never stack the filter twice when both the wrapper and the <img> carry the
 * variant class (e.g. a wrapper built by a view around an img from sprites.js). */
.sprite-wrap.variant-golden > .variant-golden,
.sprite-wrap.variant-dark   > .variant-dark,
.sprite-wrap.variant-frozen > .variant-frozen,
.sprite-wrap.variant-shadow > .variant-shadow { filter: none; }
/* Crystal keeps its tint on the <img>: a filter on the wrapper would isolate the prism
   overlay below and blend it against the sprite alone (which is white -> invisible). */
.sprite-wrap.variant-crystal { filter: none; }
/* The ice sheet over a frozen sprite in a wrapper (a soft glassy highlight, no extra art). */
.sprite-wrap.variant-frozen::after {
  content: ''; position: absolute; inset: 8%; pointer-events: none; border-radius: 45%;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.28), rgba(125, 220, 255, 0.1) 45%, transparent 70%);
  box-shadow: inset 0 0 10px rgba(215, 245, 255, 0.38);
}
/* Size tier, 40 px list/bag rows (DECISIONS 304's small step): at that size the ice sheet is
   only a few pixels wide and smears into one pale blob, so it is dropped there - the coloured
   filter and the FROST chip still say which form it is. The shiny sparkle above is unaffected. */
.sprite-wrap.size-sm.variant-frozen::after { display: none; }
/* The prism light over a crystal sprite (task 8): one screen-blended rainbow sweep that
   slowly shimmers, so the pale tint reads as glass rather than as a washed-out sprite.
   Dropped at 40 px for the same reason as the ice sheet: it smears into a coloured blob. */
.sprite-wrap.variant-crystal::after {
  content: ""; position: absolute; inset: 6%; pointer-events: none; border-radius: 30%;
  background: linear-gradient(115deg, transparent 12%, rgba(255,80,140,.9) 26%, rgba(255,200,70,.85) 38%, rgba(70,255,150,.85) 50%, rgba(70,200,255,.9) 62%, rgba(170,90,255,.85) 74%, transparent 88%);
  mix-blend-mode: color; opacity: .7;
  animation: crystalShimmer 3.2s ease-in-out infinite;
}
@keyframes crystalShimmer { 0%,100%{opacity:.45} 50%{opacity:.85} }
@media (prefers-reduced-motion: reduce) { .sprite-wrap.variant-crystal::after { animation: none; } }
.sprite-wrap.size-sm.variant-crystal::after { display: none; }   /* 40px 列表：棱镜会糊，只留 tint + 徽章 */

/* The sparkle badge, like the stars that flash when a shiny appears in the games. */
.sprite-wrap.variant-shiny::after {
  content: "✨";
  position: absolute;
  top: -2px;
  right: -8px;
  font-size: 14px;
  line-height: 1;
  pointer-events: none;
  animation: sparkle 1.8s ease-in-out infinite;
  filter: drop-shadow(0 0 3px rgba(255, 230, 120, 0.9));
}
.sprite-wrap.size-lg.variant-shiny::after { font-size: 22px; top: 2px; right: 2px; }
.sprite-wrap.size-sm.variant-shiny::after { font-size: 10px; top: -4px; right: -6px; }

/* The variant chip next to a name: SHINY, GILDED, INK, ARGENT, FROST, PRISM - the owner's pixel design of
   27 September (DECISIONS 473, embleem.html; it replaced the plain chip of DECISIONS 302). A pill in three
   bands of the form's colour, a hard edge, inset light and shade, heavy monospace capitals. All six carry a
   small pixel icon (.variant-shiny/.golden/.dark/.shadow/.frozen/.crystal; Ink and Argent got theirs with the rename).
   Shiny's star twinkles, Gilded gets a sheen every 2 s, Frost's crystal glows now and then, Prism's gem
   glints slowly - none of it with reduced motion.
   The size: every place sets it through font-size only (10 px here, 8 to 12 px elsewhere) and the whole chip
   follows. At 12 px it is the design's badge (26 px high, 18 px icon, 2 px edge), at 9 px its in-game size
   (21 px, 15 px, 1 px); in between it scales along. The icons are the design's SVGs as data: URLs (the CSP
   allows data: images), so no page had to change its markup. The golden glow behind a sprite is another
   thing (.sprite-wrap.variant-golden) and stays as it was. */
.variant-label {
  --c0: #151b27; --c1: #232c3d; --c2: #303b50;             /* bottom, middle and top band */
  --edge: #5f6b80; --label: #e9eef6;
  --ring: rgba(0, 0, 0, 0.55); --hi: rgba(255, 255, 255, 0.2); --lo: rgba(0, 0, 0, 0.35);
  position: relative;
  isolation: isolate;                                        /* Golden's sheen runs between the fill and the letters */
  display: inline-flex;
  align-items: center;
  gap: max(3px, calc(1em - 4px));                            /* 8 px at 12 px, 5 px at 9 px */
  height: max(2.1667em, calc(1em + 12px));                   /* 26 px at 12 px, 21 px at 9 px */
  padding: 0 calc(1em - 1px);                                /* 11 px / 8 px */
  border: max(1px, calc((1em - 9px) / 3 + 1px)) solid var(--edge);   /* 2 px at 12 px, 1 px at 9 px */
  border-radius: 9999px;
  overflow: hidden;
  background: linear-gradient(to bottom, var(--c2) 0 18%, var(--c1) 18% 74%, var(--c0) 74% 100%) padding-box;
  box-shadow: 0 0 0 1px var(--ring), inset 0 2px 0 var(--hi), inset 0 -2px 0 var(--lo);
  color: var(--label);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0.035em;
  text-transform: uppercase;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.75);
  white-space: nowrap;
  vertical-align: middle;
}
/* The icon: the design's 17 px SVG in an 18 px box at 12 px (14 in 15 at 9 px). */
.variant-label.shiny::before,
.variant-label.golden::before,
.variant-label.dark::before,
.variant-label.shadow::before,
.variant-label.frozen::before,
.variant-label.crystal::before {
  content: "";
  flex: none;
  width: max(1.5em, calc(0.6667em + 9px));
  height: max(1.5em, calc(0.6667em + 9px));
  background: var(--icon) center / 94% no-repeat;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45));
}
.variant-label.shiny,
.variant-label.golden,
.variant-label.dark,
.variant-label.shadow,
.variant-label.frozen,
.variant-label.crystal { padding-left: 0.6667em; }           /* 8 px / 6 px before the icon */

.variant-label.shiny {
  --c0: #b8c0c9; --c1: #eef2f5; --c2: #ffffff; --edge: #dce3e9; --label: #202938;
  --ring: #4f5964; --hi: #ffffff; --lo: #919aa5;
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' shape-rendering='crispEdges'%3E%3Cpath fill='%23d58a00' d='M11 1h2v6h-2zM11 17h2v6h-2zM1 11h6v2H1zM17 11h6v2h-6z'/%3E%3Cpath fill='%23ffd64a' d='M12 4l2.2 5.8L20 12l-5.8 2.2L12 20l-2.2-5.8L4 12l5.8-2.2z'/%3E%3Cpath fill='%23fff3a4' d='M12 6l1.4 4.6L18 12l-4.6 1.4L12 18l-1.4-4.6L6 12l4.6-1.4z'/%3E%3Crect fill='%23ffd64a' x='3' y='4' width='2' height='2'/%3E%3Crect fill='%23ffd64a' x='19' y='5' width='2' height='2'/%3E%3C/svg%3E");
  text-shadow: none;
}
.variant-label.shiny::before { animation: vl-twinkle 1.8s ease-in-out infinite; }

.variant-label.golden {
  --c0: #4a2600; --c1: #6d3b00; --c2: #8a5100; --edge: #e29a00; --label: #ffd557;
  --ring: #2a1700; --hi: #ffcf57; --lo: #2e1600;
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' shape-rendering='crispEdges'%3E%3Cpath fill='%23b66b00' d='M3 8l4 3 5-7 5 7 4-3-2 10H5z'/%3E%3Cpath fill='%23ffd044' d='M4 7l4 3 4-6 4 6 4-3-2 9H6z'/%3E%3Crect fill='%23fff2a3' x='7' y='17' width='10' height='2'/%3E%3Crect fill='%23e03b2f' x='11' y='11' width='2' height='3'/%3E%3Crect fill='%23fff2a3' x='11' y='5' width='2' height='2'/%3E%3C/svg%3E");
}
/* Gilded's sheen: one sweep left to right every 2 seconds, behind the icon and the letters. */
.variant-label.golden::after {
  content: "";
  position: absolute;
  z-index: -1;
  top: -35%;
  left: -45%;
  width: 28%;
  height: 170%;
  pointer-events: none;
  opacity: 0;
  transform: skewX(-18deg);
  background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 245, 190, 0.25) 25%, rgba(255, 255, 255, 0.92) 50%, rgba(255, 222, 115, 0.35) 72%, rgba(255, 255, 255, 0) 100%);
  filter: blur(0.2px);
  animation: vl-sheen 2s ease-in-out infinite;
}

.variant-label.frozen {
  --c0: #06385a; --c1: #084f7e; --c2: #0d679d; --edge: #4ec8ff; --label: #c9f1ff;
  --ring: #011f35; --hi: #7ddcff; --lo: #012f50;
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke-width='2' shape-rendering='crispEdges'%3E%3Cpath stroke='%23c9f5ff' d='M12 2v20M3.4 7l17.2 10M3.4 17L20.6 7'/%3E%3Cpath stroke='%2364d7ff' d='M12 5l-2-2M12 5l2-2M12 19l-2 2M12 19l2 2M6 9L3 9M6 9L5 6M18 15l3 0M18 15l1 3M6 15H3M6 15l-1 3M18 9h3M18 9l1-3'/%3E%3C/svg%3E");
}
/* Frost's crystal glows a lighter blue now and then (the design's stroke shift, here as a brightness step). */
.variant-label.frozen::before { animation: vl-frost 2.4s ease-in-out infinite; }

.variant-label.dark {
  --c0: #1a082d; --c1: #301052; --c2: #4c187b; --edge: #a95cff; --label: #dfb4ff;
  --ring: #0d0419; --hi: #bd79ff; --lo: #160424;
  /* Ink: a purple-black crescent moon with a small sparkle, no animation. */
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' shape-rendering='crispEdges'%3E%3Cpath fill='%23a95cff' d='M13 2H8V4H6V6H5V8H4V11V16H5V19H6V20H8V22H13V20H11V18H9V16V10H10V8H11V6H12V4H13Z'/%3E%3Cpath fill='%23dfb4ff' d='M7 10h1v4H7zM6 15h1v2H6zM9 19h2v1H9zM10 3h2v1h-2zM17 4h2v2h2v2h-2v2h-2V8h-2V6h2z'/%3E%3C/svg%3E");
}
/* Argent (id shadow, DECISIONS 330): near-black with silver letters, the black and white of the form itself. */
.variant-label.shadow {
  --c0: #090b10; --c1: #12151c; --c2: #1b1f28; --edge: #626872; --label: #eef0f3;
  --ring: #020305; --hi: #7a8089; --lo: #05070a;
  /* Argent: a silver-grey pixel smoke puff (dark rim behind the light body), no animation. */
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' shape-rendering='crispEdges'%3E%3Cpath fill='%23626872' d='M11 5h6v2h2v2h2v6h-2v2h-2v2h-6v-2H9v-2H7V9h2V7h2z'/%3E%3Cpath fill='%23c9ccd6' d='M9 4h6v2h2v2h2v6h-2v2h-2v2H9v-2H7v-2H5V8h2V6h2z'/%3E%3Cpath fill='%23626872' d='M18 2h2v2h-2zM21 5h2v2h-2zM20 8h1v2h-1z'/%3E%3C/svg%3E");
}
/* Crystal / Prism (task 8): deep teal glass with ice-white letters, and a diamond gem that
   catches the light. */
.variant-label.crystal {
  --c0: #0b2733; --c1: #124054; --c2: #1a5a72; --edge: #9fe8ff; --label: #e6fbff;
  --ring: #041820; --hi: #bff2ff; --lo: #062030;
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' shape-rendering='crispEdges'%3E%3Cpath fill='%239fe8ff' d='M12 2l3 5 7 5-7 5-3 5-3-5-7-5 7-5z'/%3E%3Cpath fill='%23e6fbff' d='M12 5l2 3 4 4-4 4-2 3-2-3-4-4 4-4z'/%3E%3C/svg%3E");
}
.variant-label.crystal::before { animation: vl-crystal 3.6s ease-in-out infinite; }

@keyframes vl-crystal {
  0%, 100% { transform: scale(1); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)); }
  45% { transform: scale(1.06); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)) drop-shadow(0 0 5px rgba(190, 240, 255, 0.75)) brightness(1.18); }
}
@keyframes vl-twinkle {
  0%, 100% { transform: scale(1); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)); }
  20% { transform: scale(1.05); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)) drop-shadow(0 0 2px rgba(255, 240, 150, 0.55)); }
  35% { transform: scale(1.14); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)) drop-shadow(0 0 4px rgba(255, 245, 180, 0.95)); }
  50% { transform: scale(1.03); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)) drop-shadow(0 0 2px rgba(255, 240, 150, 0.45)); }
}
@keyframes vl-sheen {
  0% { left: -45%; opacity: 0; }
  5% { opacity: 0.92; }
  32% { left: 118%; opacity: 0.72; }
  38%, 100% { left: 118%; opacity: 0; }
}
@keyframes vl-frost {
  0%, 100% { transform: scale(1); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)); }
  22% { transform: scale(1.02); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)) drop-shadow(0 0 2px rgba(140, 225, 255, 0.35)); }
  38% { transform: scale(1.06); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)) drop-shadow(0 0 4px rgba(205, 245, 255, 0.7)) brightness(1.15); }
  52% { transform: scale(1.02); filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45)) drop-shadow(0 0 2px rgba(140, 225, 255, 0.3)); }
}
@media (prefers-reduced-motion: reduce) {
  .variant-label.shiny::before, .variant-label.frozen::before, .variant-label.crystal::before { animation: none; }
  .variant-label.golden::after { animation: none; display: none; }
}

/* Gender symbols use the classic colours: blue ♂, pink ♀. */
.gender { font-weight: 700; margin-left: 4px; }
.gender.male   { color: #6cb4ff; }
.gender.female { color: #ff8fc1; }

/* Types row: base.css draws .type-badge.type-<type>; we only space them. */
.types-row { display: inline-flex; gap: 4px; flex-wrap: wrap; vertical-align: middle; }

/* HP bars: base.css already animates `.hpbar-fill` width, so damage "drains"
 * like in the games; battle.js only changes the width/colour class. */

/* Experience bar: the thin blue bar under the HP bar (Gen 3+ style). */
.expbar {
  height: 5px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
}
.expbar-fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, #4fa8ff, #7cc4ff);
  transition: width 0.6s ease;
}

/* Status condition tags (PSN/BRN/PAR/SLP/FRZ) — stored but unused by v1 rules. */
.status-tag {
  display: inline-block;
  padding: 0 6px;
  border-radius: 4px;
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  background: #7b4bd1;
  color: #fff;
  vertical-align: middle;
}
/* FNT is not an ailment but an absence (owner, 22 September): still a grey chip, but a soft
   translucent grey with a hairline and lighter, spaced letters instead of the heavy solid
   block the real statuses get (the outlined try read as white on the dark boxes). */
.status-tag.fnt { background: rgba(122, 130, 144, 0.6); border: 1px solid rgba(255, 255, 255, 0.14); color: #e6e9ef; font-weight: 600; letter-spacing: 0.06em; padding: 0 5px; line-height: 14px; }
.status-tag.psn { background: #a040a0; }
.status-tag.brn { background: #f08030; }
.status-tag.par { background: #d8b000; color: #222; }
.status-tag.slp { background: #8c8c8c; }
.status-tag.frz { background: #6ac7e8; color: #123; }

/* Section header: `.view-head` / `.view-head h2` / `.sub` come from layout.css;
 * we only add the right-aligned tools group. */
.view-tools { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* Generic error state so views never render a blank centre. `.view-loading` is the loading screen
   of css/base.css since 25 September (Poké Ball, bold line, sliding bar). */
.view-error {
  padding: 32px 16px;
  text-align: center;
  color: var(--danger, #ff6b6b);
}

/* Modal content built by the game views (party pickers, dex entry). */
.picker-list { display: flex; flex-direction: column; gap: 6px; max-height: 60vh; overflow: auto; }
.picker-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm, 8px);
  background: var(--panel-2, rgba(255, 255, 255, 0.05));
  border: 1px solid transparent;
  cursor: pointer;
  text-align: left;
  color: inherit;
  font: inherit;
  width: 100%;
}
.picker-row:hover:not(:disabled), .picker-row:focus-visible {
  border-color: var(--accent-light, #f08c8c);
  outline: none;
}
.picker-row:disabled { opacity: 0.45; cursor: not-allowed; }
.picker-row[hidden] { display: none; }   /* filtered out by a search field */
/* "Give the Exp. Share" (DECISIONS 317): the name filter above party + PC. */
.give-search { width: 100%; margin: 2px 0 10px; }
.picker-row .picker-main { flex: 1; min-width: 0; }
.picker-row .picker-name { font-weight: 700; }
.picker-row .hpbar { margin-top: 4px; }
/* Button row inside picker modals (base.css owns `.modal-actions` for openModal's own buttons). */
.picker-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }

/* ------------------------------------------------------------------------- */
/* 2. Battle                                                                  */
/* ------------------------------------------------------------------------- */

/*
 * Arena mirrors the classic battle screen: the opponent's info box top-left,
 * the opponent sprite top-right, your Pokémon's back sprite bottom-left and your
 * info box bottom-right, the message strip (the games' text box) over the bottom edge.
 * Below it (docs/BATTLE-DECISION.md §6): the attack bar — the known moves as cards, no
 * FIGHT gate — with the small Bag / Pokémon / Run buttons, then the party strip, then
 * the history drawer. Sprites are scaled 1.5x / 1.35x, info boxes overlaid in the corners.
 */
/* One column (docs/BATTLE-VISUAL.md): arena on top with the message strip laid over its
 * bottom edge, the attack bar + secondary buttons, the party strip, and the history drawer
 * (hidden until the Log button opens it). No panel boxes around the action zone. */
/* Two columns from the action row down: the moves on the left, the message panel on the right
 * (owner's sketch). The panel is its own column, so extra log lines never move the attack bar;
 * a negative top margin lets it reach over the arena's bottom-right corner. */
.battle {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--msg-w);
  grid-template-areas:
    "arena   arena"
    "actions msg"
    "party   party";
  row-gap: 8px;
  column-gap: 0;   /* the action zone and the message panel touch: one surface (owner feedback) */
  --msg-w: min(420px, 34%);
  --arena-h: 300px;   /* 270 until 24 September: the backdrop runs 30 px further down on a PC (DECISIONS 322) */
  --msg-overlap: 84px;   /* how far the panel reaches into the arena once expanded */
  --msg-overlap-collapsed: 22px;   /* collapsed it only just peeks over the arena (owner, 22 Sept) */
}
/* No gap under the header: the arena starts at the first pixel of the column. Views mount in
   .view-scroll (DECISIONS 127), whose first-child rule gave the battle 8 px again (owner, 22 Sept). */
.main > .battle, .view-scroll > .battle { margin-top: 0; }
/* After the battle the outcome panel takes the action zone's place; the panel stays beside it. */
.battle.finished {
  grid-template-areas:
    "arena   arena"
    "outcome msg";
  align-items: stretch;
}
.battle [hidden] { display: none !important; }
/* The wrapper around everything under the arena exists for phones (see the 600 px block);
   here it dissolves, so msg / outcome / actions / party are grid items as before. */
.battle-below { display: contents; }
.battle-arena {
  grid-area: arena;
  position: relative;
  min-height: 250px;
  height: var(--arena-h, 300px);
  /* Full bleed (owner feedback): the scene runs into the column's side padding and up to the
     header, so it looks like it carries on behind the shell instead of floating in it. */
  margin: 0 calc(-1 * var(--main-pad, 8px));
  border-radius: 0 0 var(--radius, 12px) var(--radius, 12px);
  box-shadow: var(--shadow, 0 8px 24px rgba(0, 0, 0, 0.35));
  overflow: hidden;
  /* Soft sky-to-grass field: the fallback under the Platinum backdrop (and all there
   * is while it loads, or if the sliced PNG is missing). Per terrain below. */
  --arena-fallback:
    radial-gradient(ellipse at 72% 44%, rgba(255, 255, 255, 0.28), transparent 32%),
    radial-gradient(ellipse at 28% 88%, rgba(255, 255, 255, 0.22), transparent 30%),
    linear-gradient(180deg, #8fd0ff 0%, #bfe6ff 42%, #8cc96a 43%, #5fa64b 100%);
  background-color: #5fa64b;
  background-image: var(--arena-fallback);
  /* The backdrop is a 256×152 cell; `cover` crops it to the compact arena (its bands are
   * blurred, so nothing is lost) and pixelated scaling keeps the Gen 4 look. */
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  image-rendering: pixelated;
}
.battle-arena.terrain-cave   { --arena-fallback: linear-gradient(180deg, #3b3550 0%, #5a5270 42%, #6e6350 43%, #4d4536 100%); }
.battle-arena.terrain-forest { --arena-fallback: linear-gradient(180deg, #6fb98f 0%, #a6dcb7 42%, #4f9a3f 43%, #2f6d2a 100%); }
.battle-arena.terrain-water  { --arena-fallback: linear-gradient(180deg, #8fd0ff 0%, #bfe6ff 42%, #4aa8e8 43%, #2c78c4 100%); }
.battle-arena.terrain-gym    { --arena-fallback: linear-gradient(180deg, #4b4f7a 0%, #7d82b5 42%, #c9b46a 43%, #a08a3c 100%); }
.battle-arena.terrain-town   { --arena-fallback: linear-gradient(180deg, #8fd0ff 0%, #d5efff 42%, #c9c1a8 43%, #a89e82 100%); }

/* Pokémon Platinum backdrops (assets/ui/battle/backgrounds/, cut from the source sheet by
 * scripts/slice_battle_ui.py; terrain mapping in docs/PLAN-ASSETS.md). battle.js puts
 * `terrain-*` (location) and `time-day|afternoon|night` (client clock) on the arena. Like the
 * Gen 4 games, Field / Forest / Surf / City have three palettes for the time of day — the
 * day/night cycle exists since Gold/Silver — while caves and gyms (Indoor) have one look.
 * url() is written out per rule rather than through the custom property: relative URLs inside
 * custom properties resolved against the page, not the stylesheet, in older Chrome. */
.battle-arena.time-day       { background-image: url(../assets/ui/battle/backgrounds/field-day.png), var(--arena-fallback); }
.battle-arena.time-afternoon { background-image: url(../assets/ui/battle/backgrounds/field-afternoon.png), var(--arena-fallback); }
.battle-arena.time-night     { background-image: url(../assets/ui/battle/backgrounds/field-night.png), var(--arena-fallback); }
.battle-arena.terrain-forest.time-day       { background-image: url(../assets/ui/battle/backgrounds/forest-day.png), var(--arena-fallback); }
.battle-arena.terrain-forest.time-afternoon { background-image: url(../assets/ui/battle/backgrounds/forest-afternoon.png), var(--arena-fallback); }
.battle-arena.terrain-forest.time-night     { background-image: url(../assets/ui/battle/backgrounds/forest-night.png), var(--arena-fallback); }
.battle-arena.terrain-water.time-day        { background-image: url(../assets/ui/battle/backgrounds/surf-day.png), var(--arena-fallback); }
.battle-arena.terrain-water.time-afternoon  { background-image: url(../assets/ui/battle/backgrounds/surf-afternoon.png), var(--arena-fallback); }
.battle-arena.terrain-water.time-night      { background-image: url(../assets/ui/battle/backgrounds/surf-night.png), var(--arena-fallback); }
.battle-arena.terrain-town.time-day         { background-image: url(../assets/ui/battle/backgrounds/city-day.png), var(--arena-fallback); }
.battle-arena.terrain-town.time-afternoon   { background-image: url(../assets/ui/battle/backgrounds/city-afternoon.png), var(--arena-fallback); }
.battle-arena.terrain-town.time-night       { background-image: url(../assets/ui/battle/backgrounds/city-night.png), var(--arena-fallback); }
/* After the time rules on purpose: same specificity as `.battle-arena.time-*`, and these win. */
.battle-arena.terrain-cave { background-image: url(../assets/ui/battle/backgrounds/cave-1.png), var(--arena-fallback); }
.battle-arena.terrain-gym  { background-image: url(../assets/ui/battle/backgrounds/indoor-1.png), var(--arena-fallback); }

/* Platforms under the sprites: the Platinum "bases" (assets/ui/battle/platforms/) — the
 * small oval the foe stands on and the wide arc under the back sprite, as in Gen 4/5.
 * Each wrapper mirrors its sprite stage's box (same coordinates as .battle-sprite.opp/.you
 * below) so the image sits centred under the feet, one layer beneath the sprite; battle.js
 * swaps the img src per terrain and time of day (`<set>-[<time>-]<opp|you>.png`). */
.battle-platform {
  position: absolute;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  width: 40%;
  z-index: 0;
  pointer-events: none;
}
/* Both stages sit well inside the arena, so the status boxes can take the outer corners
 * (opponent top-right, yours bottom-left) without ever covering a Pokémon. */
.battle-platform.opp { right: 24%; top: 0; height: 58%; }
.battle-platform.you { left: 12%; bottom: 0; height: 68%; }   /* flush with the arena's bottom edge */
.battle-platform img {
  display: block;
  height: auto;
  image-rendering: pixelated;
  /* A hint of contact shadow, in place of the old blurred ellipse. */
  filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.22));
}
.battle-platform img:not([src]) { visibility: hidden; }
/* Foe oval at ~1.7× (126 → 210 px, ~53 px tall): its middle sits at the sprite's feet,
 * which stand 22 px above the stage bottom (12 px padding + 10 px wrap margin). */
.battle-platform.opp img { width: 210px; margin-bottom: -5px; }
/* Player arc at ~1.4× (220 → 300 px); it runs under the arena edge as the game draws it
 * (the source is cut off at the bottom for that reason), the feet just inside its rim. */
.battle-platform.you img { width: 300px; margin-bottom: -8px; }
/* Sprite stages are absolutely positioned so the info boxes can overlay the corners. */
.battle-sprite {
  position: absolute;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  width: 40%;
  padding-bottom: 12px;
  z-index: 1;
}
.battle-sprite.opp { right: 24%; top: 0; height: 58%; }
.battle-sprite.you { left: 12%; bottom: 0; height: 68%; }
.battle-sprite .sprite-wrap {
  /* Gen 5 gifs are ~96px; scale 1.5x from the feet so different heights line up. --grow is the
     Pokemon's size step (DECISIONS 304), set on .battle-sprite by js/ui/battle.js updateSprite:
     +2.5 % per step on your side, +1.25 % on the foe's; unset = 1. */
  transform: scale(calc(1.5 * var(--grow, 1)));
  transform-origin: bottom center;
  margin-bottom: 10px;
}
.battle-sprite.opp .sprite-wrap { transform: scale(calc(1.35 * var(--grow, 1))); }
.battle-sprite.empty .sprite-wrap { opacity: 0; }

/* Info boxes: the name / level / HP panels, overlaid in the arena corners. Asymmetric
 * corners and a thin double edge (border + dark ring) give them the games' status-box feel
 * instead of a generic card. */
.battle-info {
  position: absolute;
  z-index: 2;
  margin: 0;
  padding: 6px 9px;
  border-radius: 12px 4px 12px 4px;
  background: rgba(14, 20, 36, 0.9);
  border: 1px solid rgba(255, 255, 255, 0.22);
  box-shadow: 0 0 0 3px rgba(14, 20, 36, 0.55), var(--shadow-sm, 0 4px 12px rgba(0, 0, 0, 0.3));
  color: var(--text, #fff);
  /* Proportional width: both boxes are equally wide and always leave room for the sprite
   * stages next to them, from a 1800 px window down to a phone. Kept compact (owner feedback)
   * so the player's Pokémon can stand further to the left. */
  width: min(250px, 30%);
}
/* Each box sits on the same side as its own Pokémon (owner's choice): the foe's top-right,
 * yours bottom-left, in the corners the sprite stages leave free. The foe's box shares a
 * column with the "A fight against:" card above it. */
.battle-foe-col {
  position: absolute;
  top: 10px; right: 10px;
  z-index: 2;
  /* + what the HP numbers need beyond the 46 px value column (--val-w, battle.js fitInfoVal). */
  width: calc(min(250px, 30%) + max(0px, var(--val-w, 0px) - 46px));
  display: flex;
  flex-direction: column;
  align-items: flex-end;   /* the little "vs" pill hugs the right edge */
  gap: 5px;
}
.battle-foe-col .battle-info { position: static; width: 100%; }
.battle-info.opp { border-radius: 4px 12px 4px 12px; }
/* Your own box is the smallest of the two (owner feedback): it only needs the bars, and the
 * narrower it is, the further left your Pokémon can stand. */
.battle-info.you { bottom: 10px; left: 10px; width: calc(min(236px, 27%) + max(0px, var(--val-w, 0px) - 46px)); padding: 5px 8px; }
/* Who you are fighting: one small pill above the foe's status box, with a ball per Pokémon. */
.battle-foe {
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 2px 8px 2px 7px;
  border-radius: 999px;
  background: rgba(14, 20, 36, 0.9);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: var(--text, #fff);
  line-height: 16px;
}
.battle-foe-head { flex: none; font-size: 9px; font-weight: 700; letter-spacing: 0.01em; color: var(--muted, #b8c0d4); }
.battle-foe-name { display: flex; align-items: center; gap: 4px; min-width: 0; font-size: 11.5px; font-weight: 600; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.battle-foe-img { width: 16px; height: 16px; flex: none; object-fit: contain; image-rendering: pixelated; }
.battle-foe .party-balls { margin: 0 0 0 2px; gap: 3px; }
.battle-foe .party-ball { width: 8px; height: 8px; }
/* Rows inside the box: a small label left of the bar ("HP", "EXP"), the bar in the middle and
 * the value right of it. Label and value have fixed widths, so the HP and EXP bars are exactly
 * the same width and the text can never shorten them (owner feedback). */
/* The bars take every pixel the label and the value column leave (owner, 22 September:
   both could be wider) - label 22 px, value 46 px ("123 / 456" and "98.22%" both fit). */
.battle-info-row { display: flex; align-items: center; gap: 5px; margin-top: 5px; }
.battle-info-row .hpbar, .battle-info-row .expbar { flex: 1 1 auto; min-width: 0; }
.battle-info-row .expbar { height: 6px; }
.battle-info-label { width: 22px; flex: none; font-size: 9.5px; font-weight: 700; letter-spacing: 0.04em; color: var(--muted, #b8c0d4); }
/* One line always (owner, 24 September: "3860 / 3860" at Lv. 1750 wrapped). The column takes the
   widest of its two texts (--val-w, measured by battle.js fitInfoVal) and both rows share it, so the
   HP and EXP bars stay equally long; the box above grows by the same amount. */
.battle-info-val { width: max(46px, var(--val-w, 0px)); flex: none; white-space: nowrap; text-align: right; font-size: 10.5px; color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; }
.battle-info-top {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
/* Owner feedback: semibold (600) instead of black (800) — Segoe UI Black looked clumsy. */
.battle-info-name { font-weight: 600; font-size: 13px; letter-spacing: 0.01em; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A special form: the game's own chip next to the name, a notch smaller in this tight box, like
   the market cards (DECISIONS 337; it was a coloured word in the name, DECISIONS 157). */
.battle-info-extras .variant-label { font-size: 9px; }
.battle-info-level { font-weight: 600; color: var(--text, #fff); white-space: nowrap; }
/* Gender symbol, variant label and status badge next to the name: spaced, not glued together. */
.battle-info-extras { display: inline-flex; align-items: center; gap: 7px; }
.battle-info-extras .gender { margin-left: 0; }

/* The Poké Ball mark by a wild foe's name (DECISIONS 297): red and white = your Pokédex has that
   species as caught, grey = not yet (the games show the caught ball since Gen 3). Hover, focus or
   tap opens the IV card; it hangs under the foe's box, whose column is its containing block. */
.dex-mark-wrap { display: inline-flex; }
.dex-mark {
  appearance: none; -webkit-appearance: none;
  position: relative; flex: none; width: 13px; height: 13px; padding: 0; margin: 0; cursor: help;
  border-radius: 50%; border: 1.5px solid #1b1f29;
  background: linear-gradient(to bottom, #e3350d 0 43%, #1b1f29 43% 57%, #f4f4f4 57% 100%);
}
.dex-mark::after {   /* the button in the middle of the ball */
  content: ''; position: absolute; left: 50%; top: 50%; width: 5px; height: 5px; transform: translate(-50%, -50%);
  box-sizing: border-box; border-radius: 50%; background: #f4f4f4; border: 1.5px solid #1b1f29;
}
.dex-mark::before { content: ''; position: absolute; inset: -7px; }   /* a finger-sized target */
.dex-mark:not(.caught) { filter: grayscale(1); opacity: 0.5; }
.dex-mark:focus-visible { outline: 2px solid var(--accent, #d64545); outline-offset: 2px; }
.dex-card {
  display: none; position: absolute; top: calc(100% + 6px); right: 0; z-index: 6;
  width: 220px; max-width: calc(100vw - 32px);   /* the sentence wraps (DECISIONS 301) */
  padding: 6px 8px; border-radius: 8px; white-space: normal; line-height: 1.35; text-align: left;
  background: rgba(14, 20, 36, 0.96); border: 1px solid rgba(255, 255, 255, 0.22);
  box-shadow: var(--shadow-sm, 0 4px 12px rgba(0, 0, 0, 0.3));
  font-size: 10.5px; font-weight: 500; color: var(--text, #fff);
}
/* Mouse: hover. Keyboard: focus. Touch: a tap toggles .open (js/ui/battle.js dexMark); hover is
   left to real mice, else a phone's sticky hover would keep the card up after the second tap. */
@media (hover: hover) { .dex-mark-wrap:hover .dex-card { display: block; } }
.dex-mark:focus-visible + .dex-card, .dex-mark-wrap.open .dex-card { display: block; }
.dex-card-head { display: block; margin-bottom: 4px; font-weight: 700; color: var(--muted, #b8c0d4); }
.dex-card-head.caught { color: #ffb0a4; }
/* "This Pidgey beats your best one (Lv. 42) in 2 of 6 IVs, but not in Attack." (DECISIONS 298, 301):
   the count and one stat it does not beat, never which ones are higher. */
.dex-card-line { display: block; }
.dex-card-line strong { font-weight: 800; }
.dex-card-line .up { color: #7ee08a; }
.dex-card-foot { display: block; margin-top: 4px; color: var(--muted, #b8c0d4); }
/* On a phone the foe's column sits top-left, so the card opens to the right. */
@media (max-width: 600px) { .dex-card { right: auto; left: 0; } }
.battle-info-trainer { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted, #b8c0d4); margin-bottom: 4px; }
.battle-info-trainer img { width: 24px; height: 24px; object-fit: contain; image-rendering: pixelated; }

/* Party balls: one dot per party member (filled = able to fight), like the row of
 * Poké Balls shown at the start of a trainer battle. */
.party-balls { display: flex; gap: 4px; margin-top: 5px; }
.party-ball {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: linear-gradient(180deg, #ff4b4b 50%, #fff 50%);
  border: 1px solid #222;
  box-sizing: border-box;
}
.party-ball.fainted { background: #555; opacity: 0.6; }
.party-ball.active { box-shadow: 0 0 0 2px var(--accent, #d64545); }

/* Message panel = the games' text box. It is the only log surface: the lines of the current
 * turn by default, the whole history after "Show all"; click = skip the text reveal. */
.battle-msg {
  /* Own column next to the action zone, reaching up over the arena and down to the party
   * strip, so the lines can grow without moving anything. */
  grid-area: msg;
  position: relative;
  z-index: 4;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* Collapsed the panel barely crosses the arena's edge; Expand lets it climb the full way
     (js toggles .expanded). The height follows the action zone, so the history gets more room. */
  margin-top: calc(-1 * var(--msg-overlap-collapsed, 22px));
  transition: margin-top 0.2s ease;
  padding: 0;
  /* Owner feedback: light panel, so the text reads as a page rather than a dark overlay. */
  border-radius: var(--radius, 12px) var(--radius, 12px) var(--radius, 12px) 0;
  background: #eef0f5;
  border: 1px solid rgba(0, 0, 0, 0.14);
  box-shadow: var(--shadow-sm, 0 4px 12px rgba(0, 0, 0, 0.3));
  color: #1d2432;
  font-size: 13px;
  line-height: 18px;
  overflow: hidden;
  /* Owner (22 September): the panel never sets the height of its row. Expand and the HP-loss
     setting used to make it taller, which pushed Bag / Pokemon / Run and the party strip down;
     with size containment the row follows the action zone alone and the panel stretches to it,
     so extra lines scroll inside (thin scrollbar below) instead of growing the screen. */
  contain: size;
  cursor: pointer; /* click = skip the text reveal, like mashing A */
}
.battle-msg.expanded { margin-top: calc(-1 * var(--msg-overlap, 84px)); }
/* Owner (21 September, evening): the head no longer takes a row of its own - it floats in the
 * top-right corner over the first line, which keeps clear of it (--msg-head-w is measured by
 * js/ui/battle.js: the link reads Expand / Collapse and the HP-loss box comes and goes). */
.battle-msg-head { position: absolute; top: 0; right: 0; z-index: 2; display: flex; align-items: center; justify-content: flex-end; gap: 8px; height: 22px; padding: 3px 8px 0; }
.battle-msg-line:first-child, .battle-log-group:first-child .battle-log-turn { padding-right: var(--msg-head-w, 76px); }
/* Quiet text link, not a button: the history is a side road (owner feedback). */
.battle-msg-log {
  padding: 0 2px;
  border: 0;
  background: none;
  color: #79839a;
  font: inherit;
  font-size: 11px;
  white-space: nowrap;
  cursor: pointer;
}
.battle-msg-log:hover { color: #1d2432; text-decoration: underline; }
/* Both views live in the same panel; only one is visible at a time (Show all / Show less).
 * The rows run edge to edge: no gap between the lines and the panel (owner feedback). */
.battle-msg-lines, .battle-msg .battle-log-lines { flex: 1; min-height: 0; overflow-y: auto; padding: 3px 0 4px; scrollbar-width: thin; scrollbar-color: #b9c0cf transparent; }   /* the first line on the head's row */
.battle-msg-lines::-webkit-scrollbar, .battle-msg .battle-log-lines::-webkit-scrollbar { width: 6px; }
.battle-msg-lines::-webkit-scrollbar-thumb, .battle-msg .battle-log-lines::-webkit-scrollbar-thumb { background: #b9c0cf; border-radius: 3px; }
.battle-msg-lines::-webkit-scrollbar-track, .battle-msg .battle-log-lines::-webkit-scrollbar-track { background: transparent; }
.battle-msg-line { padding: 1px 10px; color: #6a7386; }          /* earlier lines of the turn */
.battle-msg-line.current { color: #111823; font-weight: 600; }   /* the newest line */
/* Lines about exp and money get the colour of their bar / of coins, so they read at a glance. */
.battle-log-line.kind-exp, .battle-msg-line.kind-exp { color: #1665c8; font-weight: 600; }
/* "Your party gained 6 Exp. Points!": a shade lighter than the line of the one that earned most (owner, 23 September). */
.battle-log-line.kind-exp.group, .battle-msg-line.kind-exp.group { color: #4c8fe0; }
.battle-log-line.kind-money, .battle-msg-line.kind-money { color: #9a6a00; font-weight: 600; }
/* "X lost N HP!" lines are hidden unless the "HP loss" setting (next to Show all) is on. */
.battle:not(.show-hp) .hp-loss { display: none; }
.battle-log-setting { display: inline-flex; align-items: center; gap: 4px; margin-right: auto; padding-left: 2px; font-size: 11px; color: #6a7386; cursor: pointer; }

/* The history is the panel's second view (there is no separate panel below any more):
 * console-like, monospace, one compact row per line, grouped under "Turn N · time". */
.battle-log-lines { font-family: var(--font-mono, monospace); font-size: 12px; line-height: 1.45; }
/* Newest turn on top (js prepends the group); its heading spans the whole width. */
.battle-log-turn { display: flex; justify-content: space-between; padding: 3px 10px; background: rgba(0, 0, 0, 0.06); color: #b83232; font-weight: 700; }
.battle-log-group + .battle-log-group { border-top: 1px solid rgba(0, 0, 0, 0.08); }
.battle-log-line { padding: 1px 10px 1px 18px; color: #2b3345; }
.log-time { color: #8b94a6; font-size: 11px; margin-right: 8px; font-weight: 400; }
.battle-log-line.kind-damage, .battle-log-line.kind-miss { color: #79839a; }
.battle-log-line.kind-faint, .battle-msg-line.kind-faint { color: #c0392b; font-weight: 700; }
.battle-log-line.kind-catch, .battle-msg-line.kind-catch,
.battle-log-line.kind-evolve, .battle-msg-line.kind-evolve { color: #b83232; font-weight: 700; }
/* Items: a thrown ball, a potion, a revive - something you chose to spend, so it stands out
   from the play-by-play (owner, 22 September). Teal, so it is neither a move nor a verdict. */
.battle-log-line.kind-item, .battle-msg-line.kind-item { color: #0f6f9c; font-weight: 600; }
/* Pokémon of the Day story lines ("back on its feet", "is yours!") - gold, like the prize it is. */
.battle-log-line.kind-daily, .battle-msg-line.kind-daily { color: #9a6a00; font-weight: 700; }
/* A level-up matters, so it is the strongest text colour and bold, in both views. */
.battle-log-line.kind-levelup, .battle-msg-line.kind-levelup { color: #111823; font-weight: 700; }
.battle-log-line.kind-win, .battle-msg-line.kind-win { color: #1a7f47; font-weight: 700; }
/* A badge is a one-off reward: green, like winning, but it stands out on its own. */
.battle-log-line.kind-badge, .battle-msg-line.kind-badge { color: #16794a; font-weight: 700; }
.battle-log-line.kind-lose, .battle-msg-line.kind-lose { color: #c0392b; font-weight: 700; }
.battle-log-line.kind-info, .battle-log-line.kind-switch { font-style: italic; }
.battle-log-line.kind-status, .battle-msg-line.kind-status { color: #6d3fbf; }

/* Action zone: the attack bar with the secondary buttons, or a drawer (Bag / Pokémon).
 * No panel box: the cards themselves are the visual unit (docs/BATTLE-VISUAL.md). */
/* Owner feedback: the page background is light, so the action zone and the party strip sit on
 * a solid dark HUD panel (otherwise the cards and buttons are hard to read). */
.battle-actions, .party-strip {
  background: var(--panel, rgba(18, 26, 44, 0.88));
  border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.1));
  border-radius: var(--radius, 12px);
  box-shadow: var(--shadow-sm, 0 4px 12px rgba(0, 0, 0, 0.3));
}
.battle-actions { grid-area: actions; min-height: 64px; padding: 8px 10px; border-radius: var(--radius, 12px) 0 0 var(--radius, 12px); }
.battle-actions > div { display: flex; flex-direction: column; gap: 8px; }
.battle-actions-title { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.battle-actions-title .btn { padding: 4px 10px; }
.battle-menu { display: flex; flex-direction: column; gap: 6px; }
.battle-secondary { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }
.battle-secondary .btn { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.18); color: var(--text, #fff); font-weight: 700; }
.battle-secondary .btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.2); }
.battle-secondary .btn-run { color: var(--danger, #ff6b6b); }
/* "Use next Pokémon? - No": the run-away offer under the forced party choice (DECISIONS 148,
   restyled 22 September): its own strip with a line above it, text left, button right. */
.battle-run-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  margin-top: 10px; padding: 9px 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0 0 var(--radius-sm, 8px) var(--radius-sm, 8px);
  background: rgba(255, 255, 255, 0.03);
}
.battle-run-text { font-size: 12.5px; line-height: 1.35; min-width: 0; flex: 1 1 240px; }
.battle-run-row .btn-run { flex: none; color: var(--danger, #ff6b6b); border-color: rgba(255, 107, 107, 0.45); }
.battle-run-row .btn-run:hover { background: rgba(255, 107, 107, 0.12); }
.battle-actions .btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Turn in progress: the bar stays but goes grey and inert (js: lockActions). */
.battle-actions.locked .battle-menu { filter: grayscale(1); opacity: 0.55; pointer-events: none; transition: opacity 0.2s ease; }
.battle-actions.locked .move-btn, .battle-actions.locked .battle-secondary .btn { opacity: 1; }
/* Only the turn's text runs (DECISIONS 320): the move cards take a tap ahead, so the bar is
   lighter than the fully locked one; Bag / Pokemon / Run stay disabled. The tapped card gets a
   white ring and "Next" in place of its key number (over .move-btn.last), and goes as soon as the
   text is through. */
.battle-actions.locked.queueable .battle-menu { pointer-events: auto; filter: grayscale(0.6); opacity: 0.8; }
.battle-actions .move-btn.queued { box-shadow: 0 0 0 2px #fff, 0 0 12px rgba(255, 255, 255, 0.55); }
.battle-actions .move-btn.queued .move-key { display: none; }
.battle-actions .move-btn.queued::after {
  content: "Next";
  position: absolute; top: 6px; right: 8px;
  padding: 0 5px; border-radius: 4px;
  background: #fff; color: #1c2740;
  font-size: 10px; font-weight: 800; line-height: 16px;
}

/* The attack bar: one card per known move (never an empty tile), horizontal on desktop.
 * The type colour is a soft tint + border, i.e. information rather than decoration. */
.move-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; }
.move-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }   /* js/ui/movelearn.js (learn / forget prompt) */
.move-btn {
  --move-color: #8a8a8a;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  padding: 8px 32px 8px 10px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid color-mix(in srgb, var(--move-color) 55%, transparent);
  background: linear-gradient(180deg, color-mix(in srgb, var(--move-color) 22%, rgba(18, 26, 44, 0.85)), rgba(18, 26, 44, 0.85));
  color: var(--text, #fff);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
/* Hover = only the border takes the full type colour (owner feedback: no lift, no shadow). */
.move-btn:hover:not(:disabled) { border-color: var(--move-color); }
.move-btn:focus-visible { outline: none; border-color: var(--move-color); box-shadow: var(--focus, 0 0 0 3px rgba(214, 69, 69, 0.45)); }
.move-btn.last { box-shadow: 0 0 0 2px var(--accent, #d64545); }   /* the move Enter repeats */
.move-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.move-btn.pp-out { filter: grayscale(0.6); }
.move-btn-name { font-weight: 600; font-size: 14px; letter-spacing: 0.01em; }
.move-key {
  position: absolute; top: 6px; right: 8px;
  min-width: 16px; padding: 0 4px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.12);
  color: var(--muted, #b8c0d4);
  font-size: 10px; font-weight: 800; text-align: center; line-height: 16px;
}
.move-btn-meta { display: flex; gap: 8px; align-items: center; font-size: 11.5px; color: var(--muted, #b8c0d4); flex-wrap: wrap; }
.move-btn-meta .move-pp { margin-left: auto; font-variant-numeric: tabular-nums; }
.move-btn-meta .pp-low { color: var(--danger, #ff6b6b); font-weight: 700; }

/* Party strip: six compact slots (icon, name, status, mini HP bar). Click = switch. */
.party-strip { grid-area: party; display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 6px; padding: 8px; }
.party-slot {
  display: flex; align-items: center; gap: 6px;
  min-width: 0; padding: 4px 6px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.09);
  color: var(--text, #fff); font: inherit; text-align: left; cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.party-slot:hover:not(:disabled), .party-slot:focus-visible { outline: none; border-color: var(--accent-light, #f08c8c); background: rgba(255, 255, 255, 0.16); }
.party-slot:disabled { cursor: default; }
.party-slot.active { border-color: var(--accent, #d64545); background: var(--accent-soft, rgba(214, 69, 69, 0.16)); }
.party-slot.fainted { opacity: 0.4; filter: grayscale(1); }
.party-slot.empty { justify-content: flex-start; gap: 8px; opacity: 0.35; }
.party-slot-num { flex: none; font-size: 10px; font-weight: 700; color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; }
.party-slot .sprite-wrap.size-sm img { width: 28px; height: 28px; }
.party-slot-body { flex: 1; min-width: 0; }
.party-slot-name { display: flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 600; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.party-slot-name .status-tag { font-size: 9px; padding: 0 4px; }
.party-slot .hpbar { height: 5px; margin-top: 3px; }
/* Placeholders while the battle loads (DECISIONS 319, js/ui/battle.js showLoading): the party slots
   and move cards are real markup under .is-skeleton (css/base.css); these give the empty parts a
   size - the 28 px icon, the status box texts, and the one bar a spectator gets. */
.battle-skel-icon { display: inline-block; width: 28px; height: 28px; }
.battle-skel-bar { height: 44px; border-radius: var(--radius-sm, 8px); }
.battle-info-name.is-skeleton { flex: 0 1 96px; height: 13px; border-radius: 4px; }
.battle-info-level.is-skeleton { width: 34px; height: 13px; border-radius: 4px; }
.battle-info-val.is-skeleton { height: 10px; border-radius: 3px; }
/* Owner (21 September): level at the right of the name, a 2 px EXP bar under the HP bar. */
.party-slot-name-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.party-slot-lv { flex: none; margin-left: auto; font-size: 10px; font-weight: 600; color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; }
.party-slot .expbar { height: 2px; margin-top: 2px; }
.move-btn.type-normal   { --move-color: var(--type-normal, #a8a878); }
.move-btn.type-fire     { --move-color: var(--type-fire, #f08030); }
.move-btn.type-water    { --move-color: var(--type-water, #6890f0); }
.move-btn.type-electric { --move-color: var(--type-electric, #f8d030); }
.move-btn.type-grass    { --move-color: var(--type-grass, #78c850); }
.move-btn.type-ice      { --move-color: var(--type-ice, #98d8d8); }
.move-btn.type-fighting { --move-color: var(--type-fighting, #c03028); }
.move-btn.type-poison   { --move-color: var(--type-poison, #a040a0); }
.move-btn.type-ground   { --move-color: var(--type-ground, #e0c068); }
.move-btn.type-flying   { --move-color: var(--type-flying, #a890f0); }
.move-btn.type-psychic  { --move-color: var(--type-psychic, #f85888); }
.move-btn.type-bug      { --move-color: var(--type-bug, #a8b820); }
.move-btn.type-rock     { --move-color: var(--type-rock, #b8a038); }
.move-btn.type-ghost    { --move-color: var(--type-ghost, #705898); }
.move-btn.type-dragon   { --move-color: var(--type-dragon, #7038f8); }
.move-btn.type-dark     { --move-color: var(--type-dark, #705848); }
.move-btn.type-steel    { --move-color: var(--type-steel, #b8b8d0); }
.move-btn.type-fairy    { --move-color: var(--type-fairy, #ee99ac); }

/* Bag / Pokémon sub-menus inside the action panel. */
.battle-sub { display: flex; flex-direction: column; gap: 6px; max-height: 240px; overflow-y: auto; }

/* Pokémon drawer: a 2-wide grid of compact cards (3 rows for a full party) that fits on screen. */
.party-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.party-card {
  display: flex; align-items: center; gap: 8px;
  min-width: 0; padding: 6px 8px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.09);
  color: var(--text, #fff); font: inherit; text-align: left; cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.party-card:hover:not(:disabled) { border-color: var(--accent-light, #f08c8c); background: rgba(255, 255, 255, 0.16); }
.party-card:focus-visible { outline: none; box-shadow: var(--focus, 0 0 0 3px rgba(214, 69, 69, 0.45)); }
.party-card:disabled { cursor: default; }
.party-card.active { border-color: var(--accent, #d64545); background: var(--accent-soft, rgba(214, 69, 69, 0.16)); }
.party-card.fainted { opacity: 0.45; filter: grayscale(1); }
/* "Use Revive on which Pokémon?": the fainted one is the only valid target and must look like it, not
   greyed out while the three it cannot help stand bright (MASTER GV-10, idea #24). */
.item-targets .party-card.fainted:not(:disabled) { opacity: 1; filter: none; }
.party-card .sprite-wrap.size-sm img { width: 32px; height: 32px; }
.party-card-body { flex: 1; min-width: 0; }
.party-card-name { display: flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.party-card-lv { color: var(--muted, #b8c0d4); font-weight: 500; }
.party-card-name .status-tag { font-size: 9px; padding: 0 4px; }
.party-card .hpbar { height: 6px; margin-top: 3px; }
.party-card-hp { display: flex; justify-content: space-between; margin-top: 2px; font-size: 11px; color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; }
.party-card-why { color: var(--accent-light, #f08c8c); font-weight: 600; }
.battle-sub-msg { padding: 8px 10px; border-radius: var(--radius-sm, 8px); background: var(--accent-soft, rgba(214, 69, 69, 0.16)); color: var(--accent-light, #f08c8c); font-weight: 700; }
.battle-waiting { display: flex; align-items: center; gap: 10px; padding: 16px; justify-content: center; color: var(--muted, #b8c0d4); font-weight: 700; }
.battle-waiting .dots::after { content: "…"; animation: dots 1.2s steps(4, end) infinite; }

/* Outcome panel shown when status === finished. */
.battle-outcome { grid-area: outcome; padding: 8px 10px 10px; border-radius: var(--radius, 12px) 0 0 var(--radius, 12px); }   /* 8 px on top, like the action zone */
/* The banner is also an .outcome-buttons row (Rematch next to Continue), whose 10 px top margin
   pushed Continue down from the panel's edge (owner, 22 September). */
.battle-outcome .outcome-banner { margin-top: 0; }
/* Holds the Continue button; its colour is the result (win / lose / draw / caught / escape). */
.outcome-banner {
  display: flex;
  justify-content: center;
  padding: 8px;
  border-radius: var(--radius-sm, 8px);
  margin: 0 0 8px;
  animation: banner-in 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
/* Owner (22 September): after a battle Continue is the one thing you press, so on a desktop
   it fills the banner instead of sitting in the middle as a 200 px island (the phone rule
   below already made it full width). With Rematch beside it in PvP the two share the row. */
.outcome-banner .btn { flex: 1 1 auto; min-width: 200px; max-width: 460px; }
.outcome-banner .btn-label { display: inline-flex; align-items: center; gap: 10px; }
/* Owner (21 September, evening): on a phone Continue was a 200 px island in a full-width
   banner. It now spans the banner (Rematch shares the row in PvP), 48 px tall, and the
   space-bar hint disappears wherever there is no keyboard. */
@media (max-width: 600px) {
  .outcome-banner { flex-wrap: wrap; gap: 8px; padding: 8px; }
  .outcome-banner .btn { flex: 1 1 auto; min-width: 0; width: 100%; min-height: 48px; }
  .outcome-banner .safari-outcome-note { width: 100%; }
  /* In a battle the toasts (6 s errors, 15 s challenges) sat on the move buttons and the party strip:
     over the arena instead, under the top bar (MASTER GV-16; js/ui/battle.js sets body.in-battle). */
  body.in-battle .toast-container { top: 60px; bottom: auto; }
  /* 44 px tap targets for Bag / Pokémon / Run, "Run away" at the KO prompt and the second chance's
     "Run after it" / "Let it go" (MASTER GV-20). */
  .battle-secondary .btn, .battle-run-row .btn, .modal:has(.chase) .modal-actions .btn { min-height: 44px; }
}
/* "tap to skip" in the message head, phones only (KZ-12, the coarse-pointer rule below); gone once the result is up. */
.battle-msg-tap { display: none; font-size: 11px; color: #79839a; white-space: nowrap; }
@media (pointer: coarse) {
  .kbd-hint { display: none; }
  /* Expand / Collapse is 39x18 px of text: a finger gets a bigger hit area without moving the head (GV-20). */
  .battle-msg-log { position: relative; }
  .battle-msg-log::before { content: ''; position: absolute; inset: -12px -8px; }
  .battle-msg-tap { display: inline; }
  .battle.finished .battle-msg-tap, .battle-msg.expanded .battle-msg-tap { display: none; }
}
/* Stat gains under "Grew to Lv. N! · HP +3": the other five stats, quiet (owner, 21 September). */
.outcome-stat-gains { margin-top: 2px; font-size: 0.72rem; color: var(--muted, #b8c0d4); }
/* Quiet hint that the space bar does the same thing. */
.kbd-hint {
  padding: 0 5px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.22);
  font-family: inherit;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  opacity: 0.9;
}
.outcome-banner.win    { background: linear-gradient(90deg, rgba(98, 210, 111, 0.28), rgba(214, 69, 69, 0.22)); color: #fff; }
.outcome-banner.lose   { background: rgba(255, 107, 107, 0.18); color: var(--danger, #ff6b6b); }
.outcome-banner.draw   { background: rgba(255, 255, 255, 0.08); color: var(--text, #fff); }
.outcome-banner.caught { background: linear-gradient(90deg, rgba(214, 69, 69, 0.3), rgba(255, 143, 177, 0.25)); color: #fff; }
.outcome-banner.escape { background: rgba(59, 130, 246, 0.2); color: #cfe2ff; }
.outcome-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px; }
.outcome-card { padding: 10px; border-radius: var(--radius-sm, 8px); background: var(--panel-2, rgba(255, 255, 255, 0.05)); }
/* Owner feedback: normal capitalisation, not SHOUTING. */
.outcome-card h4 { margin: 0 0 6px; font-size: 12.5px; font-weight: 600; letter-spacing: 0.01em; color: var(--muted, #b8c0d4); }
.outcome-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; border-top: 1px solid rgba(255, 255, 255, 0.06); }
.outcome-row:first-of-type { border-top: 0; }
.outcome-row .grow { flex: 1; min-width: 0; }
/* A row with a story (level-up, stats, moves): head row, then the text at full width. */
.outcome-row.wide { flex-direction: column; align-items: stretch; gap: 3px; }
.outcome-row.wide .outcome-head { display: flex; align-items: center; gap: 8px; }
.outcome-row.wide .outcome-body { padding: 0 2px 2px; }
.outcome-row.wide .outcome-body .outcome-stat-gains { margin-top: 1px; }
/* "+105 EXP" and its bar on one line under the name, the bar taking the rest of the width
   (DECISIONS 219); it no longer shares the head row with the sprite and the name. */
.outcome-row.wide .outcome-body .outcome-gain { flex-direction: row; align-items: center; gap: 8px; }
.outcome-row.wide .outcome-body .outcome-exp { flex: 1; width: auto; margin-top: 0; }
/* EXP is blue everywhere (the bar, the log lines, this gain). */
.outcome-gain { color: #7cc4ff; font-weight: 700; white-space: nowrap; }
.outcome-detail { font-size: 12px; color: var(--muted, #b8c0d4); }
.outcome-detail b { color: var(--accent-light, #f08c8c); }
.outcome-money { font-size: 20px; font-weight: 900; color: var(--accent-light, #f08c8c); }
.outcome-badge { display: flex; align-items: center; gap: 10px; font-weight: 700; color: var(--success, #4cd964); }
.outcome-badge .badge-icon { width: 30px; height: 30px; border-radius: 7px; transform: rotate(45deg); background: linear-gradient(135deg, var(--accent-hover, #e25a5a), var(--accent-strong, #b83232)); border: 2px solid #fff; box-shadow: 0 0 12px rgba(214, 69, 69, 0.7); }
.outcome-buttons { display: flex; gap: 8px; justify-content: center; margin-top: 10px; flex-wrap: wrap; }
.outcome-pending { display: inline-flex; align-items: center; gap: 4px; color: var(--accent-light, #f08c8c); font-weight: 700; }
.caught-card { display: flex; align-items: center; gap: 14px; }

/* Sprite animations. Classes are toggled by battle.js on the sprite wrapper; the idle bob
 * runs on the <img> inside it so it never competes with the wrapper's animations. */
.battle-sprite .sprite-wrap > img { animation: idle-bob 3.2s ease-in-out infinite; }
.anim-enter { animation: sprite-enter 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both; }
.battle-sprite.you .anim-enter { animation-name: sprite-enter-left; }
.anim-attack { animation: attack-nudge 0.4s ease-out both; }   /* the attacker lunges forward */
.battle-sprite.opp .anim-attack { animation-name: attack-nudge-opp; }
.anim-hit { animation: hit-shake 0.4s ease both; }
.anim-faint { animation: faint-drop 0.6s ease-in both; }
/* Catch: the foe is drawn into the ball (white flash, shrink) and stays inside while the
 * ball rocks; it pops back at the end, which is the "broke free" case. battle.js toggles
 * this ~0.6 s into the throw, when the ball reaches the foe. */
.anim-ball { animation: catch-suck 1.7s ease both; }
/* The throw in three states on the foe's STAGE (battle.js throwBall, owner 22 September):
 *   .catch-in     first contact - white flash, shrink into the ball, and stay gone (fill both)
 *                 until the result is known;
 *   .catch-out    broke free - pops back out where it stood;
 *   .caught-away  clicked shut - never comes back, the closed ball stays lying there. */
.battle-sprite.catch-in .sprite-wrap { animation: catch-suck-in 0.35s ease both; }
.battle-sprite.catch-out .sprite-wrap { animation: catch-pop-out 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both; }
.battle-sprite.caught-away .sprite-wrap,
.caught-away { opacity: 0 !important; visibility: hidden; }
@keyframes catch-suck-in {
  0% { filter: none; opacity: 1; scale: 1; }
  40% { filter: brightness(3) saturate(0); opacity: 1; scale: 1; }
  100% { filter: brightness(3) saturate(0); opacity: 0; scale: 0.2; }
}
@keyframes catch-pop-out {
  0% { filter: brightness(3) saturate(0); opacity: 0; scale: 0.2; }
  60% { filter: brightness(1.6) saturate(0.5); opacity: 1; scale: 1.08; }
  100% { filter: none; opacity: 1; scale: 1; }
}
@keyframes catch-burst { 0% { opacity: 1; scale: 1; } 100% { opacity: 0; scale: 1.7; } }
/* Once it has clicked shut the ball lies still: no new wobble, only a soft glow. */
.catch-ball.caught img { rotate: 0deg; filter: drop-shadow(0 0 6px rgba(246, 201, 69, 0.8)) drop-shadow(0 3px 3px rgba(0, 0, 0, 0.35)); }

/* The Poké Ball throw (battle.js throwBall() on a `kind: catch` log line): the big ball from
 * the Black/White HUD sheet, assets/ui/battle/hud/pokeball.png. Staged like the games: it
 * arcs from the player's side to the foe in 0.7 s, spinning, then rocks three times on its
 * bottom point. The wrapper flies in a straight line and the img adds the vertical arc, so
 * the path is a parabola without a motion path; the wobble is a separate, delayed rotate.
 * v1 ends the same way for a catch and a break-out (the log line tells); the games add a
 * click with stars, or the ball bursting open. */
.catch-ball {
  position: absolute;
  z-index: 3;
  /* Start and landing point in arena pixels, measured from the two sprites by battle.js
     (--from-x/y, --to-x/y); the percentages are only the fallback. */
  left: var(--from-x, 26%);
  top: var(--from-y, 58%);
  /* 28 px: a touch smaller than the 36 it was - it read too big next to the foe (owner, 22 September). */
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  pointer-events: none;
  animation: catch-fly 0.7s linear both;
}
.catch-ball img {
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  transform-origin: 50% 95%;
  filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.35));
  animation:
    catch-arc 0.7s both,
    catch-spin 0.7s linear both;
}
/* After contact the ball falls from the hit point to the foe's feet with one small bounce
   (battle.js adds .drop, then .landed once it lies still). `left` is repeated because the
   fly's fill state goes when its animation is replaced. */
.catch-ball.drop { left: var(--to-x, 72%); animation: catch-drop 0.4s ease-in both; }
@keyframes catch-drop {
  0% { top: var(--to-y, 36%); }
  70% { top: var(--land-y, 52%); animation-timing-function: ease-out; }
  85% { top: calc(var(--land-y, 52%) - 7px); animation-timing-function: ease-in; }
  100% { top: var(--land-y, 52%); }
}
/* Lying still: no arc, no spin. Each wobble is its own class toggled by battle.js, with a
   gap of stillness in between - that is where you watch whether it holds. */
.catch-ball.landed img { animation: none; }
.catch-ball.landed img.rock { animation: catch-wobble 0.45s ease-in-out both; }
/* Verdict: caught = a click with a golden glow; broke free = the ball bursts open. */
.catch-ball.caught img { animation: catch-click 0.35s ease-out both; }
.catch-ball.burst img { animation: catch-burst 0.3s ease-out both; }
@keyframes catch-click { 0% { scale: 1; } 40% { scale: 1.18; filter: brightness(2) drop-shadow(0 0 10px rgba(246, 201, 69, 1)); } 100% { scale: 1; } }

/* Entrance uses `translate` (independent of `transform`) so the base scale of the
 * wrapper is untouched at every breakpoint. */
@keyframes sprite-enter { from { opacity: 0; translate: 40px 0; } to { opacity: 1; translate: 0 0; } }
@keyframes sprite-enter-left { from { opacity: 0; translate: -40px 0; } to { opacity: 1; translate: 0 0; } }
@keyframes hit-shake {
  0%, 100% { translate: 0 0; filter: none; }
  20% { translate: -6px 0; filter: brightness(1.8); }
  40% { translate: 6px 0; }
  60% { translate: -4px 0; filter: brightness(1.8); }
  80% { translate: 4px 0; }
}
@keyframes faint-drop { to { translate: 0 60px; opacity: 0; filter: brightness(0.4); } }
@keyframes idle-bob { 0%, 100% { translate: 0 0; } 50% { translate: 0 -3px; } }
@keyframes attack-nudge { 0% { translate: 0 0; } 35% { translate: 26px -10px; } 100% { translate: 0 0; } }
@keyframes attack-nudge-opp { 0% { translate: 0 0; } 35% { translate: -26px 8px; } 100% { translate: 0 0; } }
/* Catch sequence (see .catch-ball / .anim-ball above). The ball's start and end points are
 * arena percentages: from just above the back sprite (stage centre x 26%) to the middle
 * of the foe's sprite (stage centre x 72%). */
@keyframes catch-fly { from { left: var(--from-x, 26%); top: var(--from-y, 58%); } to { left: var(--to-x, 72%); top: var(--to-y, 36%); } }
@keyframes catch-arc {
  0% { translate: 0 0; animation-timing-function: ease-out; }
  45% { translate: 0 -90px; animation-timing-function: ease-in; }
  100% { translate: 0 0; }
}
@keyframes catch-spin { from { rotate: 0deg; } to { rotate: 720deg; } }
@keyframes catch-wobble { 0%, 100% { rotate: 0deg; } 25% { rotate: -24deg; } 75% { rotate: 24deg; } }
@keyframes catch-suck {
  0% { filter: none; opacity: 1; scale: 1; }
  15% { filter: brightness(3) saturate(0); opacity: 1; scale: 1; }
  30%, 88% { filter: brightness(3) saturate(0); opacity: 0; scale: 0.2; }
  100% { filter: none; opacity: 1; scale: 1; }
}
@keyframes sparkle { 0%, 100% { opacity: 0.55; transform: scale(0.9) rotate(-8deg); } 50% { opacity: 1; transform: scale(1.15) rotate(8deg); } }
@keyframes line-in { from { opacity: 0; translate: 0 4px; } to { opacity: 1; translate: 0 0; } }
@keyframes dots { 0% { content: ""; } 25% { content: "."; } 50% { content: ".."; } 75% { content: "..."; } }
@keyframes banner-in { from { opacity: 0; transform: translateY(-10px) scale(0.96); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .anim-enter, .anim-attack, .anim-hit, .anim-faint, .anim-ball, .battle-sprite .sprite-wrap > img, .sprite-wrap.variant-shiny::after, .battle-log-line, .outcome-banner { animation: none !important; }
  /* No flying ball either: without its animation it would just hang mid-air. */
  .catch-ball { display: none; }
  /* A real catch still ends with the closed ball at the foe's spot (battle.js throwBall hides
     the sprite for good), so the stage is not simply empty. */
  .catch-ball.caught { display: block; left: var(--to-x, 72%); top: var(--land-y, 52%); animation: none; }
  .catch-ball.caught img, .catch-ball.landed img.rock { animation: none !important; }
  /* No draw-in animation, but the foe still disappears into the ball and pops back on a break-out. */
  .battle-sprite.catch-in .sprite-wrap { animation: none !important; opacity: 0; }
  .hpbar-fill, .expbar-fill { transition: none; }
}

/* ------------------------------------------------------------------------- */
/* 3. Team (party of six) — compact rows (ARCHITECTURE.md 11.5)               */
/* ------------------------------------------------------------------------- */

/*
 * One row per party slot: 40 px icon · name + Lv + types · HP bar with numbers ·
 * EXP bar · small icon buttons (reorder, details, use item, send to PC). Six rows
 * fit in ~420 px. A "!" badge marks a Pokémon that wants to learn a move
 * (pending_moves, phase 2 move choice).
 */
/* Owner feedback: three cards across (two rows for a full party) instead of six long rows. */
.team-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; align-items: start; }
.team-row {
  display: grid;
  grid-template-columns: 18px 72px minmax(0, 1fr);   /* room for the front sprite (DECISIONS 179) */
  grid-template-areas:
    "slot sprite body"
    "actions actions actions";
  align-items: center;
  gap: 0 8px;
  padding: 8px 10px;
  min-height: 92px;
  border-radius: var(--radius-sm, 8px);
  background: var(--panel, rgba(18, 26, 44, 0.88));
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: var(--shadow-sm, 0 4px 12px rgba(0, 0, 0, 0.3));
  position: relative;
}
.team-row.fainted { opacity: 0.75; }
.team-row.fainted .team-sprite img { filter: grayscale(1); }
.team-row .slot-no { grid-area: slot; font-size: 11px; font-weight: 800; color: var(--muted, #b8c0d4); text-align: center; }
.team-row .team-sprite { grid-area: sprite; position: relative; display: flex; align-items: flex-end; justify-content: center; width: 72px; height: 72px; }
/* The animated front gif (DECISIONS 179): scaled into 72 px, feet on the bottom edge like a summary stage. */
.team-row .team-sprite img { max-width: 72px; max-height: 72px; width: auto; height: auto; }
.team-row .pending-badge {
  position: absolute; top: -4px; right: -6px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--accent, #d64545); color: #fff; border: 2px solid #fff;
  font-size: 10px; font-weight: 900; line-height: 12px; text-align: center;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
}
.team-row .team-body { grid-area: body; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.team-row .team-name { font-weight: 600; font-size: 13.5px; display: flex; align-items: center; gap: 5px; flex-wrap: wrap; line-height: 1.25; }
.team-row .team-name .species { font-weight: 400; color: var(--muted, #b8c0d4); font-size: 12px; }
.team-row .team-name .lv { font-weight: 700; color: var(--muted, #b8c0d4); font-size: 12px; }
.team-row .team-name .types-row .type-badge { font-size: 9.5px; padding: 1px 6px; }
.team-row .team-name .variant-label { font-size: 10px; }
.team-row .team-name .pending-text { font-size: 11px; font-weight: 700; color: var(--accent-light, #f08c8c); cursor: pointer; }
.team-row .team-name .pending-text:hover { text-decoration: underline; }
/* On Hold and ready to evolve (DECISIONS 216); the card click opens the summary's Evolve button. */
.team-row .team-name .evolve-text { font-size: 11px; font-weight: 700; color: var(--warning, #f5b400); }
.team-row .team-bars { display: grid; grid-template-columns: 22px minmax(0, 1fr) 52px; gap: 2px 6px; align-items: center; font-size: 10.5px; color: var(--muted, #b8c0d4); }
.team-row .team-bars .bar-label { font-weight: 700; font-size: 10px; letter-spacing: 0.04em; }
.team-row .team-bars .hpbar { height: 6px; }
.team-row .team-bars .expbar { height: 4px; }
.team-row .team-bars .bar-text { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The buttons sit on their own line under the card, quiet until you hover the card. */
.team-row .team-actions {
  grid-area: actions;
  display: flex; gap: 2px; align-items: center; justify-content: flex-end;
  margin-top: 7px; padding-top: 5px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  opacity: 0.65;
  transition: opacity 0.15s ease;
}
.team-row:hover .team-actions, .team-row:focus-within .team-actions { opacity: 1; }
.team-row .team-actions .btn-xs.btn-ghost { color: var(--muted, #b8c0d4); }
.team-row .team-actions .btn-xs.btn-ghost:hover:not(:disabled) { color: var(--text, #fff); }
/* A card is a button (details) and a drag handle (reorder) at the same time. */
.team-row[draggable="true"] { cursor: pointer; transition: border-color 0.15s ease, transform 0.15s ease, background 0.15s ease; }
.team-row[draggable="true"]:hover { background: rgba(28, 38, 62, 0.94); border-color: rgba(255, 255, 255, 0.22); transform: translateY(-1px); }
.team-row[draggable="true"]:focus-visible { outline: 2px solid var(--accent-light, #f08c8c); outline-offset: 2px; }
.team-row.dragging { opacity: 0.4; }
/* Another trainer's party on their profile is the same card grid (DECISIONS 218); the loading
   line and "No Pokémon" span the whole row instead of one card cell. */
.profile-party > :not(.team-row) { grid-column: 1 / -1; }
.team-row.drop-target { border-color: var(--accent-light, #f08c8c); box-shadow: 0 0 0 2px rgba(240, 140, 140, 0.45); }
/* Empty slot: darker than a card instead of lighter, so it reads as a hole on the wallpaper. */
.team-row.empty-slot {
  display: flex; align-items: center; justify-content: center; min-height: 92px;
  color: rgba(255, 255, 255, 0.55); font-size: 12px; box-shadow: none;
  background: rgba(8, 12, 24, 0.6);
  border: 1px dashed rgba(255, 255, 255, 0.3);
}
.team-row.empty-slot.drop-target { border-color: var(--accent-light, #f08c8c); color: var(--accent-light, #f08c8c); }
.team-row.empty-slot .slot-no { position: absolute; left: 8px; }
/* ------------------------------------------------------------------------- */
/* 4. Pokémon detail                                                          */
/* ------------------------------------------------------------------------- */

.pkm { display: grid; grid-template-columns: 300px 1fr; gap: 14px; }
/* While ‹ › loads the next Pokémon (DECISIONS 448) the summary stays and dims a little instead of
   the page emptying; js/ui/pokemon.js update() sets the class on the scroller and redraws in place. */
#view-scroll.is-stepping > .pkm { opacity: 0.55; pointer-events: none; transition: opacity 0.12s ease; }
@media (prefers-reduced-motion: reduce) { #view-scroll.is-stepping > .pkm { transition: none; } }
.pkm-hero { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }
.pkm-stage {
  width: 100%;
  min-height: 220px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: 24px;
  border-radius: var(--radius, 12px);
  background: radial-gradient(ellipse at 50% 90%, rgba(255, 255, 255, 0.18), transparent 60%), var(--panel-2, rgba(255, 255, 255, 0.05));
  cursor: pointer;
  border: 0;
  color: inherit;
  font: inherit;
}
.pkm-stage:focus-visible { outline: 2px solid var(--accent, #d64545); }
.pkm-stage .sprite-wrap { transform: scale(2); transform-origin: bottom center; margin-bottom: 8px; }
.pkm-stage .sprite-wrap.variant-shiny::after { font-size: 12px; }
.pkm-title { font-size: 22px; font-weight: 900; display: flex; gap: 8px; align-items: center; justify-content: center; flex-wrap: wrap; }
.pkm-sub { color: var(--muted, #b8c0d4); font-size: 13px; }
.pkm-nick { display: flex; gap: 6px; width: 100%; }
/* Owner (22 September): the field inherited the page's 15 px input and its placeholder ran
   off the 300 px column. Smaller type, a tighter box, and the button beside it. */
.pkm-nick input { flex: 1; min-width: 0; font-size: 0.85rem; padding: 6px 9px; min-height: 32px; }
.pkm-nick .btn { flex: none; }
/* The six party places under the name (DECISIONS 316): a click moves this Pokémon there. */
.pkm-party { width: 100%; }
.pkm-party-head { margin-bottom: 5px; font-size: 11px; font-weight: 500; color: var(--muted, #b8c0d4); text-align: left; }
.pkm-party-head strong { font-weight: 700; color: var(--text, #fff); }   /* "Now in party place 3" */
.pkm-party-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; }
.pkm-party-slot {
  position: relative; display: grid; place-items: center; aspect-ratio: 1; min-width: 0; padding: 0; cursor: pointer;
  border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.16); background: rgba(255, 255, 255, 0.06);
  color: var(--muted, #b8c0d4);
}
.pkm-party-slot:hover:not(:disabled), .pkm-party-slot:focus-visible { border-color: var(--accent, #d64545); background: rgba(255, 255, 255, 0.12); outline: none; }
/* Where it is now: filled red with a thick border and a white number - never missed (owner, 24 Sept). */
.pkm-party-slot.is-here {
  border: 2px solid var(--accent, #d64545); background: rgba(214, 69, 69, 0.45);
  box-shadow: 0 0 0 2px rgba(214, 69, 69, 0.3); cursor: default;
}
.pkm-party-slot.is-empty { border-style: dashed; }
.pkm-party-slot:disabled:not(.is-here) { opacity: 0.45; cursor: default; }
.pkm-party-no { position: absolute; top: 2px; left: 4px; font-size: 9px; font-weight: 700; line-height: 1; color: var(--muted, #b8c0d4); }
.pkm-party-slot.is-here .pkm-party-no { color: #fff; }
.pkm-party-icon { width: 34px; height: 34px; object-fit: contain; }
/* Evolution panel (DECISIONS 216): Hold | Auto side by side, then one button per form. */
.pkm-evo { display: flex; flex-direction: column; gap: 8px; }
.pkm-evo p { margin: 0; }
.pkm-evo-switch { display: flex; gap: 6px; }
.pkm-evo-switch .btn { flex: 1; }
/* "Evolve into X" has to catch the eye (owner, 24 September, DECISIONS 312): a red glow that
   breathes, and every 2.6 s a white shine sweeps across - the white light that runs over a
   Pokémon as it evolves in the games. ::before, because .btn.loading owns ::after (the spinner);
   isolation keeps the shine above the red but under the label. */
.btn-evolve { position: relative; overflow: hidden; isolation: isolate; animation: evolve-glow 2s ease-in-out infinite; }
.btn-evolve::before {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 40%;
  z-index: -1;
  background: linear-gradient(100deg, transparent 0%, rgba(255, 255, 255, 0.6) 50%, transparent 100%);
  transform: translateX(-150%) skewX(-20deg);
  animation: evolve-shine 2.6s ease-in-out infinite;
  pointer-events: none;
}
.btn-evolve:disabled, .btn-evolve:disabled::before { animation: none; }
.btn-evolve:disabled::before { display: none; }
@keyframes evolve-glow {
  0%, 100% { box-shadow: 0 3px 0 rgba(0, 0, 0, 0.18), 0 0 0 0 rgba(214, 69, 69, 0.55); }
  50% { box-shadow: 0 3px 0 rgba(0, 0, 0, 0.18), 0 0 14px 3px rgba(214, 69, 69, 0.6); }
}
/* The sweep takes the first 55 %, then the button rests until the next one. */
@keyframes evolve-shine {
  0% { transform: translateX(-150%) skewX(-20deg); }
  55%, 100% { transform: translateX(280%) skewX(-20deg); }
}
@media (prefers-reduced-motion: reduce) {
  .btn-evolve { animation: none; box-shadow: 0 3px 0 rgba(0, 0, 0, 0.18), 0 0 10px 2px rgba(214, 69, 69, 0.5); }
  .btn-evolve::before { display: none; }
}
/* The battle's "Evolve / Not now" question: the Pokémon above the text. */
.evo-ask { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }
.evo-ask p { margin: 0; }
/* Label left, numbers right - the two spans used to butt together ("HP0 / 0"). */
.pkm-hp-line { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-top: 4px; font-size: 12.5px; }
.pkm-hp-line .bar-label { font-weight: 700; letter-spacing: 0.04em; color: var(--muted, #b8c0d4); }
/* Sprite, name and memo share the left column (DECISIONS 187). */
.pkm-left { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.pkm-left .pkm-section { margin-bottom: 0; }
/* "‹ 12 / 87 ›" next to Back: through the PC or the party without going back (DECISIONS 395). */
.pkm-step { display: inline-flex; align-items: center; gap: 2px; }
.pkm-step .btn { min-width: 30px; padding: 3px 5px; }
.pkm-memo .info-table { font-size: 12px; }
.pkm-memo .info-table td:first-child { width: 96px; }
/* The held item's name in the red of the buttons (the lighter one, readable on the dark panel). */
.pkm-memo .held-name { color: var(--accent-hover, #e25a5a); font-weight: 800; }
.pkm-section { margin-bottom: 12px; }
.pkm-section h3 { margin: 0 0 8px; font-size: 14px; letter-spacing: 0.01em; color: var(--muted, #b8c0d4); }
.pkm-level-row { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
.pkm-level-row .lv { font-size: 20px; font-weight: 900; color: var(--accent-light, #f08c8c); }
.stat-table { width: 100%; border-collapse: collapse; }
.stat-table td { padding: 4px 6px; vertical-align: middle; font-size: 13px; }
.stat-table td.stat-name { width: 64px; font-weight: 700; color: var(--muted, #b8c0d4); }
.stat-table td.stat-val { width: 48px; text-align: right; font-weight: 800; font-variant-numeric: tabular-nums; }
.stat-bar { height: 8px; border-radius: 999px; background: rgba(255, 255, 255, 0.1); overflow: hidden; }
.stat-bar-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, #62d26f, #4fa8ff); transition: width 0.5s ease; }
.stat-bar-fill.iv { background: linear-gradient(90deg, #6cb4ff, #b58cff); }
details.pkm-advanced summary { cursor: pointer; font-weight: 700; color: var(--accent-light, #f08c8c); }
/* The IV hexagon (js/ui/pokemon.js ivRadar, DECISIONS 166): the games' summary chart - white
   grid, light-blue shape, yellow stat names, white values, red ▲ / blue ▼ on the extremes. */
.iv-radar-wrap { display: flex; justify-content: center; padding: 6px 0 2px; }
.iv-radar { width: 100%; max-width: 340px; height: auto; display: block; }
.iv-radar .iv-grid { fill: rgba(255, 255, 255, 0.05); stroke: rgba(255, 255, 255, 0.85); stroke-width: 1.5; }
.iv-radar .iv-grid.faint { fill: none; stroke: rgba(255, 255, 255, 0.28); stroke-width: 1; }
.iv-radar .iv-spoke { stroke: rgba(255, 255, 255, 0.28); stroke-width: 1; }
.iv-radar .iv-shape { fill: rgba(150, 200, 255, 0.82); stroke: #dbeeff; stroke-width: 1.5; stroke-linejoin: round; }
.iv-radar .iv-dot { fill: #fff; }
.iv-radar .iv-name { font-size: 11px; font-weight: 700; fill: #f0e28c; }
.iv-radar .iv-value { font-size: 14px; font-weight: 800; fill: #fff; font-variant-numeric: tabular-nums; }
.iv-radar .iv-judge { font-size: 8.5px; fill: rgba(255, 255, 255, 0.6); }
.iv-radar .iv-mark { font-size: 10px; font-weight: 800; }
.iv-radar .iv-mark.up { fill: #ff6b6b; }
.iv-radar .iv-mark.down { fill: #6cb4ff; }
/* Hyper Training (DECISIONS 332): a trained stat's 31 and its "Hyper trained" in gold, the colour of
   the Gold Bottle Cap; the tooltip on that corner keeps the real IV. */
.iv-radar .iv-value.ht { fill: #f7d154; }
.iv-radar .iv-judge.ht { fill: #f7d154; font-weight: 700; }
/* Your best other one of this line (DECISIONS 352): a dashed orange outline over the blue shape,
   the same dash as the key in front of its name under the graph. Thinner since DECISIONS 357 (was 2,
   "te dik"): it marks the other one, the blue shape stays the main thing. */
.iv-radar .iv-ghost { fill: none; stroke: #ffa94d; stroke-width: 1.2; stroke-dasharray: 4 3; stroke-linejoin: round; }
.iv-compare {
  display: grid; gap: 6px; margin-top: 8px; padding: 8px 10px;
  border-radius: var(--radius-sm); background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 0.82rem;
}
.iv-compare-head { font-weight: 700; }
.iv-compare-verdict { font-weight: 700; font-variant-numeric: tabular-nums; }
.iv-compare-verdict.top { color: #7ee08a; }
.iv-compare-verdict.behind { color: #ffc46b; }
/* One row per Pokémon (DECISIONS 357): "This one" (blue swatch = the shape) and the other one
   (dashed swatch = the outline), each with its own chips under it, so no line is about "which one?". */
.iv-compare-row { display: grid; gap: 4px; padding: 6px 8px; border-radius: var(--radius-sm); background: rgba(255, 255, 255, 0.03); }
.iv-compare-row.this { box-shadow: inset 3px 0 0 rgba(150, 200, 255, 0.82); }
.iv-compare-row.other { box-shadow: inset 3px 0 0 #ffa94d; }
.iv-compare-line { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; }
.iv-compare-line .btn { margin-left: auto; }
.iv-compare-total { font-weight: 700; font-variant-numeric: tabular-nums; }
.iv-legend-shape { width: 14px; height: 10px; flex: none; border-radius: 2px; background: rgba(150, 200, 255, 0.82); border: 1px solid #dbeeff; }
/* The same 4-on / 3-off dash as the outline in the graph, 1.5 px high. */
.iv-legend-dash { width: 18px; height: 1.5px; flex: none; background: repeating-linear-gradient(90deg, #ffa94d 0 4px, transparent 4px 7px); }
.iv-just-caught {
  padding: 1px 7px; border-radius: 999px; font-size: 0.68rem; font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase;
  color: #fff; background: var(--accent, #d64545);
}
.iv-compare-stats { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.iv-chip {
  padding: 1px 7px; border-radius: 999px; font-size: 0.72rem; font-weight: 700;
  background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.14);
}
.iv-chip.up { color: #ffc46b; border-color: rgba(255, 169, 77, 0.45); }   /* where the other one is higher */
.iv-chip.top { color: #7ee08a; border-color: rgba(126, 224, 138, 0.45); }   /* where this one is the best */
.pkm-hyper .ht-caps { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 6px 0 10px; }
.pkm-hyper .ht-count { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; font-variant-numeric: tabular-nums; }
.pkm-hyper .ht-cap { width: 20px; height: 20px; flex: none; }
.pkm-hyper .ht-stats { display: flex; flex-wrap: wrap; gap: 6px; }
.move-list { display: flex; flex-direction: column; gap: 6px; }
.move-row { display: grid; grid-template-columns: auto 1fr auto auto; align-items: center; gap: 10px; padding: 8px 10px; border-radius: var(--radius-sm, 8px); background: var(--panel-2, rgba(255, 255, 255, 0.05)); border-left: 4px solid var(--move-color, #8a8a8a); }
.move-row .move-name { font-weight: 800; }
.move-row .move-stats { display: flex; gap: 10px; font-size: 12px; color: var(--muted, #b8c0d4); white-space: nowrap; }
.move-row.type-normal   { --move-color: var(--type-normal, #a8a878); }
.move-row.type-fire     { --move-color: var(--type-fire, #f08030); }
.move-row.type-water    { --move-color: var(--type-water, #6890f0); }
.move-row.type-electric { --move-color: var(--type-electric, #f8d030); }
.move-row.type-grass    { --move-color: var(--type-grass, #78c850); }
.move-row.type-ice      { --move-color: var(--type-ice, #98d8d8); }
.move-row.type-fighting { --move-color: var(--type-fighting, #c03028); }
.move-row.type-poison   { --move-color: var(--type-poison, #a040a0); }
.move-row.type-ground   { --move-color: var(--type-ground, #e0c068); }
.move-row.type-flying   { --move-color: var(--type-flying, #a890f0); }
.move-row.type-psychic  { --move-color: var(--type-psychic, #f85888); }
.move-row.type-bug      { --move-color: var(--type-bug, #a8b820); }
.move-row.type-rock     { --move-color: var(--type-rock, #b8a038); }
.move-row.type-ghost    { --move-color: var(--type-ghost, #705898); }
.move-row.type-dragon   { --move-color: var(--type-dragon, #7038f8); }
.move-row.type-dark     { --move-color: var(--type-dark, #705848); }
.move-row.type-steel    { --move-color: var(--type-steel, #b8b8d0); }
.move-row.type-fairy    { --move-color: var(--type-fairy, #ee99ac); }
.info-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.info-table td { padding: 4px 6px; border-top: 1px solid rgba(255, 255, 255, 0.06); }
.info-table td:first-child { color: var(--muted, #b8c0d4); width: 120px; }

/* ------------------------------------------------------------------------- */
/* 5. PC storage (Bill's PC)                                                  */
/* ------------------------------------------------------------------------- */

/* The PC page has no party since DECISIONS 553 (Team and the right column show it): the .pc-party rules went. */
.pc-box-nav { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pc-box-nav .box-name { font-weight: 800; min-width: 70px; text-align: center; }
/* Real boxes (owner, 21 September): the dropdown lists Box 1-8 with their counts. */
.pc-box-nav .box-select { width: auto; min-width: 118px; min-height: 30px; padding: 3px 8px; font-size: 0.84rem; font-weight: 700; }
.pc-tile .tile-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px; }
.box-picker .picker-row { padding: 8px 12px; }
.pc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }
.pc-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 6px;
  border-radius: var(--radius-sm, 8px);
  background: var(--panel-2, rgba(255, 255, 255, 0.05));
  border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.1));
  text-align: center;
  font-size: 12px;
  transition: border-color 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease, background 0.15s ease;
}
/* Hover / keyboard focus on a box slot (DECISIONS 141): the owner found the old 1 px tint
 * "too little". Now an unmistakable 2 px accent ring (the 1 px border plus a 1 px shadow ring,
 * so nothing shifts by a pixel), a soft red glow and a slight lift - the cursor frame the
 * games draw around the slot you point at in Bill's PC. `:focus-within` also catches the Tab
 * key landing on the sprite button or the To party button inside. The party strip on top gets
 * the same frame so both rows of slots react alike. */
.pc-tile:hover, .pc-tile:focus-within {
  border-color: var(--accent, #d64545);
  box-shadow: 0 0 0 1px var(--accent, #d64545), 0 6px 18px rgba(214, 69, 69, 0.32);
  background: rgba(255, 255, 255, 0.09);
  transform: translateY(-1px);
}
.pc-tile .tile-sprite { background: none; border: 0; padding: 0; cursor: pointer; line-height: 0; }
.pc-tile .tile-sprite:focus-visible { outline: 2px solid var(--accent, #d64545); border-radius: 8px; }
.pc-tile .name { font-weight: 700; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pc-tile .lv { color: var(--muted, #b8c0d4); }
/* Picked for a release (DECISIONS 274): the danger colour, so it never reads as the hover frame. */
.pc-tile.picked, .pc-tile.picked:hover { border-color: var(--danger, #e5484d); box-shadow: 0 0 0 1px var(--danger, #e5484d); background: rgba(229, 72, 77, 0.14); }
.pc-pick { margin-left: auto; }
.pc-pager { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 12px; }

/* ------------------------------------------------------------------------- */
/* 6. Poké Mart                                                               */
/* ------------------------------------------------------------------------- */

.shop-money { font-weight: 800; color: var(--accent-light, #f08c8c); font-size: 16px; }
.item-icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; }
.item-icon img, .item-icon svg { width: 36px; height: 36px; image-rendering: pixelated; }
.qty-stepper { display: inline-flex; align-items: center; border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.15)); border-radius: 999px; overflow: hidden; }
.qty-stepper button { background: transparent; border: 0; color: inherit; width: 30px; height: 30px; cursor: pointer; font-size: 16px; font-weight: 800; }
.qty-stepper button:hover:not(:disabled) { background: rgba(255, 255, 255, 0.1); }
.qty-stepper button:disabled { opacity: 0.4; cursor: not-allowed; }
/* Override base.css's generic number-input sizing (min-width 80px, 38px tall). */
.qty-stepper input[type="number"] { width: 44px; min-width: 0; min-height: 0; padding: 4px 2px; border-radius: 0; text-align: center; background: transparent; border: 0; box-shadow: none; color: inherit; font: inherit; font-weight: 700; -moz-appearance: textfield; appearance: textfield; }
.qty-stepper input::-webkit-outer-spin-button, .qty-stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qty-stepper input:focus { outline: none; background: var(--accent-soft, rgba(214, 69, 69, 0.16)); }
/* Quick amounts above the stepper (DECISIONS 235): small pills, the chosen one in the accent. */
.qty-quick { display: flex; flex-wrap: wrap; gap: 5px; margin: 8px 0 2px; }
.qty-chip {
  min-width: 40px; height: 28px; padding: 0 9px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.15)); background: rgba(255, 255, 255, 0.04);
  color: inherit; font: inherit; font-size: 0.8rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
.qty-chip:hover:not(:disabled) { background: rgba(255, 255, 255, 0.1); }
.qty-chip.active { border-color: var(--accent); background: var(--accent-soft, rgba(214, 69, 69, 0.16)); color: var(--accent-light, #f08c8c); }
.qty-chip:disabled { opacity: 0.45; cursor: not-allowed; }

/* ------------------------------------------------------------------------- */
/* 7. Bag                                                                     */
/* ------------------------------------------------------------------------- */


/* ------------------------------------------------------------------------- */
/* 8. Pokédex                                                                 */
/* ------------------------------------------------------------------------- */

.dex-head { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.dex-counts { display: flex; gap: 16px; align-items: baseline; flex-wrap: wrap; }
.dex-counts b { font-size: 20px; color: var(--accent-light, #f08c8c); }
.dex-progress { position: relative; height: 10px; border-radius: 999px; background: rgba(255, 255, 255, 0.1); overflow: hidden; }
.dex-progress .seen { position: absolute; inset: 0 auto 0 0; background: rgba(255, 255, 255, 0.35); transition: width 0.6s ease; }
.dex-progress .caught { position: absolute; inset: 0 auto 0 0; background: linear-gradient(90deg, #62d26f, #4fa8ff); transition: width 0.6s ease; }
.dex-filters { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* base.css gives inputs/selects width: 100%; in this flex row they share the line. */
.dex-filters input[type="search"] { width: auto; flex: 1 1 180px; min-width: 140px; }
.dex-filters select { width: auto; max-width: 180px; }
.dex-chip {
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.15));
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
.dex-chip.active { background: var(--accent, #d64545); color: var(--accent-text, #fff); border-color: transparent; }
.dex-chip:focus-visible { outline: 2px solid var(--accent, #d64545); outline-offset: 2px; }
.dex-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 8px; }
.dex-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 8px 4px 6px;
  border-radius: var(--radius-sm, 8px);
  background: var(--panel-2, rgba(255, 255, 255, 0.05));
  border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.1));
  color: inherit;
  font: inherit;
  font-size: 12px;
  text-align: center;
  cursor: default;
  position: relative;
}
.dex-tile.seen, .dex-tile.caught { cursor: pointer; }
.dex-tile.seen:hover, .dex-tile.caught:hover, .dex-tile:focus-visible { border-color: var(--accent-light, #f08c8c); outline: none; }
.dex-tile .dex-no { font-size: 11px; color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; }
.dex-tile .dex-img { height: 56px; display: flex; align-items: center; justify-content: center; }
.dex-tile .dex-img img { max-height: 56px; max-width: 72px; }
/* Seen-but-not-caught: a dark silhouette, like the unregistered entries in the games. */
.dex-tile.seen .dex-img img { filter: brightness(0); opacity: 0.55; }
.dex-tile.unknown { opacity: 0.55; }
.dex-tile.unknown .dex-img { font-size: 28px; font-weight: 900; color: var(--muted, #b8c0d4); }
.dex-tile .dex-name { font-weight: 700; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dex-tile .types-row .type-badge { font-size: 9px; padding: 0 5px; }
.dex-tile.caught::after {
  content: "";
  position: absolute;
  top: 6px;
  right: 6px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: linear-gradient(180deg, #ff4b4b 50%, #fff 50%);
  border: 1px solid #222;
  box-sizing: border-box;
}
.dex-pager { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 12px; }
.dex-entry { display: flex; gap: 16px; align-items: center; }
.dex-entry .sprite-wrap { transform: scale(1.5); transform-origin: center; margin: 16px; }
.dex-entry h3 { margin: 0 0 6px; }
/* The four forms of a species: met ones in colour, the rest greyed out (DECISIONS.md 113). */
.dex-forms-title { margin: 12px 0 6px; font-size: 12px; font-weight: 600; color: var(--muted, #b8c0d4); }
.dex-forms { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.dex-form {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 6px 4px 5px;
  border-radius: var(--radius-sm, 8px);
  background: var(--panel-2, rgba(255, 255, 255, 0.05));
  border: 1px solid transparent;
  text-align: center;
}
.dex-form-art { height: 48px; display: flex; align-items: center; justify-content: center; }
.dex-form .sprite-wrap img { width: 44px; height: 44px; object-fit: contain; }
.dex-form-name { font-size: 10.5px; font-weight: 600; }
.dex-form-when { font-size: 9.5px; color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; }
/* The species page (DECISIONS 400): a hero band in the first type's colour, the sprite on a round pad (a
   silhouette until caught, a "?" until seen), then "Where to find" per region and the other ways, with the
   forms beside it on a wide screen and under it on a phone. */
.dex-modal .modal-body { padding-top: 0; }
.dex-page { display: flex; flex-direction: column; gap: 14px; }
.dex-hero {
  --dex-tint: #6b7689;
  display: flex; align-items: center; gap: 16px; padding: 16px 14px;
  margin: 0 -16px;
  background: linear-gradient(135deg, color-mix(in srgb, var(--dex-tint) 55%, #10162a), color-mix(in srgb, var(--dex-tint) 18%, #10162a) 70%, rgba(16, 22, 42, 0.6));
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.dex-hero.type-normal { --dex-tint: var(--type-normal); }     .dex-hero.type-fire { --dex-tint: var(--type-fire); }
.dex-hero.type-water { --dex-tint: var(--type-water); }       .dex-hero.type-electric { --dex-tint: var(--type-electric); }
.dex-hero.type-grass { --dex-tint: var(--type-grass); }       .dex-hero.type-ice { --dex-tint: var(--type-ice); }
.dex-hero.type-fighting { --dex-tint: var(--type-fighting); } .dex-hero.type-poison { --dex-tint: var(--type-poison); }
.dex-hero.type-ground { --dex-tint: var(--type-ground); }     .dex-hero.type-flying { --dex-tint: var(--type-flying); }
.dex-hero.type-psychic { --dex-tint: var(--type-psychic); }   .dex-hero.type-bug { --dex-tint: var(--type-bug); }
.dex-hero.type-rock { --dex-tint: var(--type-rock); }         .dex-hero.type-ghost { --dex-tint: var(--type-ghost); }
.dex-hero.type-dragon { --dex-tint: var(--type-dragon); }     .dex-hero.type-dark { --dex-tint: var(--type-dark); }
.dex-hero.type-steel { --dex-tint: var(--type-steel); }       .dex-hero.type-fairy { --dex-tint: var(--type-fairy); }
.dex-pad {
  flex: none; width: 112px; height: 112px; display: grid; place-items: center; border-radius: 50%;
  background: radial-gradient(circle at 50% 40%, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.05) 60%, transparent 72%);
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.14);
}
.dex-pad .sprite-wrap { transform: scale(1.35); transform-origin: center; }
.dex-pad.is-silhouette .sprite-wrap img { filter: brightness(0); opacity: 0.6; }
.dex-pad-q { font-size: 56px; font-weight: 900; color: rgba(255, 255, 255, 0.5); line-height: 1; }
.dex-hero-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.dex-hero-no { font-size: 12px; font-weight: 700; letter-spacing: 0.08em; color: rgba(255, 255, 255, 0.65); font-variant-numeric: tabular-nums; }
.dex-hero-name { margin: 0; font-size: 22px; line-height: 1.1; font-weight: 800; }
.dex-hero-status { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 2px; }
.dex-status { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; background: rgba(0, 0, 0, 0.35); color: var(--muted, #b8c0d4); }
.dex-status.seen { color: #fff; }
.dex-status.caught { background: rgba(98, 210, 111, 0.22); color: #9df0a6; }
.dex-body { display: grid; grid-template-columns: minmax(0, 1fr) 250px; gap: 18px; align-items: start; }
.dex-col { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.dex-section-title { display: flex; align-items: center; gap: 4px; margin: 0; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted, #b8c0d4); }
.dex-where { display: flex; flex-direction: column; gap: 10px; }
.dex-region { display: flex; flex-direction: column; gap: 6px; }
.dex-region-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.dex-region-name { font-weight: 800; font-size: 14px; }
.dex-region-lock { display: inline-flex; align-items: center; gap: 2px; font-size: 11px; color: var(--muted, #b8c0d4); }
.dex-region.is-closed .dex-areas { opacity: 0.7; }
.dex-areas { display: flex; flex-wrap: wrap; gap: 6px; }
.dex-area {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px 5px 8px;
  border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.05);
  color: inherit; font: inherit; font-size: 12px; text-align: left; max-width: 100%;
}
.dex-area.is-link { cursor: pointer; }
.dex-area.is-link:hover, .dex-area.is-link:focus-visible { border-color: var(--accent-light, #f08c8c); background: rgba(214, 69, 69, 0.14); outline: none; }
.dex-area-icon { display: inline-flex; color: var(--muted, #b8c0d4); }
.dex-area-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dex-area-lv { color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Three dots: how common it is there (filled = common, one = rare). */
.dex-rarity { display: inline-flex; gap: 2px; margin-left: 2px; }
.dex-rarity i { width: 6px; height: 6px; border-radius: 50%; background: rgba(255, 255, 255, 0.18); }
.dex-rarity.r3 i, .dex-rarity.r2 i:nth-child(-n+2), .dex-rarity.r1 i:first-child { background: #62d26f; }
.dex-rarity.r2 i, .dex-rarity.r1 i:first-child { background: #ffd86b; }
.dex-rarity.r2 i:nth-child(3) { background: rgba(255, 255, 255, 0.18); }
.dex-rarity.r1 i:first-child { background: #f08c8c; }
.dex-ways { display: flex; flex-direction: column; gap: 6px; }
.dex-way { display: flex; align-items: flex-start; gap: 8px; font-size: 12.5px; line-height: 1.35; }
.dex-way-icon { display: inline-flex; flex: none; margin-top: 2px; color: var(--accent-light, #f08c8c); }
.dex-col.is-unobtainable .dex-ways .muted { color: var(--accent-light, #f08c8c); }
.dex-col-forms .dex-forms { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 640px) {
  .dex-body { grid-template-columns: 1fr; }
  .dex-col-forms .dex-forms { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (max-width: 480px) {
  .dex-hero { gap: 12px; padding: 12px 10px; margin: 0 -12px; }
  .dex-pad { width: 88px; height: 88px; }
  .dex-pad .sprite-wrap { transform: scale(1.1); }
  .dex-hero-name { font-size: 19px; }
  .dex-area { width: 100%; }
  .dex-area-name { flex: 1; }
  .dex-col-forms .dex-forms { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Not met yet: a silhouette, like an unknown dex entry in the games. */
.dex-form:not(.met) .sprite-wrap img { filter: grayscale(1) brightness(0.35); opacity: 0.7; }
.dex-form:not(.met) .dex-form-name { color: var(--muted, #b8c0d4); }
.dex-form.met { border-color: rgba(255, 255, 255, 0.14); }
.dex-form.owned { border-color: var(--success, #4cd964); }
.dex-form.owned .dex-form-when { color: var(--success, #4cd964); }

/* ------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------- */
/* 8b. Pokémon market (js/ui/market.js, DECISIONS 370)                       */
/* ------------------------------------------------------------------------- */

/* One dark panel is the whole marketplace (owner's design, 25 September): the tabs, the
 * search row with its Filters drawer, the form chips and the product cards all live inside
 * it, so the page gradient behind never competes with the listings. */
.market-panel { display: flex; flex-direction: column; gap: 10px; }
.market-tabs { max-width: 520px; }
.market-tab-body { display: flex; flex-direction: column; gap: 10px; }
/* Search box with the magnifier inside, the Filters button and the sort order on one row. */
.market-toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.market-search { position: relative; flex: 1 1 220px; min-width: 160px; display: block; }
.market-search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); display: flex; opacity: 0.55; pointer-events: none; }
.market-search input[type="search"] { width: 100%; padding-left: 32px; }
.market-toolbar select { width: auto; max-width: 190px; }
.market-filters-btn { display: inline-flex; align-items: center; gap: 6px; }
.market-filters-btn.active, .market-filters-btn.has-filters { border-color: var(--accent, #d64545); color: var(--text, #fff); }
.market-filters-btn .badge-count { min-width: 16px; height: 16px; font-size: 10px; }
/* The Filters drawer: level, IV, price, type, gender, a move and the seller. */
.market-adv {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px 10px;
  padding: 10px; border-radius: var(--radius-sm, 8px);
  background: rgba(0, 0, 0, 0.22); border: 1px solid rgba(255, 255, 255, 0.08);
}
.market-adv .field { margin: 0; gap: 3px; }
.market-adv .field label { font-size: 0.72rem; }
.market-adv .input { padding: 5px 8px; font-size: 0.82rem; }
.market-adv .range { display: flex; align-items: center; gap: 4px; color: var(--muted, #b8c0d4); }
.market-adv .range .input { min-width: 0; flex: 1; }
.market-adv-reset { grid-column: 1 / -1; justify-self: end; }
.market-chips { display: flex; gap: 4px; flex-wrap: wrap; }
.market-chips .chip {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--panel-2, rgba(255, 255, 255, 0.05));
  border: 1px solid rgba(255, 255, 255, 0.12);
  color: var(--muted, #b8c0d4);
  font-size: 11.5px;
  font-weight: 600;
  min-height: 24px;
}
.market-chips .chip:hover { color: var(--text, #fff); }
.market-chips .chip.active { background: var(--accent-soft, rgba(214, 69, 69, 0.16)); border-color: var(--accent, #d64545); color: var(--text, #fff); }
/* Each form chip carries a hint of its own colour, the same cue as on the cards. */
.market-chips .chip-shiny.active { background: rgba(255, 216, 107, 0.2); border-color: var(--variant-shiny, #ffd86b); }
.market-chips .chip-golden.active { background: rgba(255, 176, 32, 0.2); border-color: var(--variant-golden, #ffb020); }
.market-chips .chip-dark.active { background: rgba(167, 139, 250, 0.2); border-color: var(--variant-dark, #a78bfa); }
.market-chips .chip-frozen.active { background: rgba(143, 227, 255, 0.2); border-color: var(--variant-frozen, #8fe3ff); }
.market-chips .chip-shadow.active { background: rgba(200, 204, 216, 0.16); border-color: var(--variant-shadow, #c9ccd6); }
.market-chips .chip-crystal.active { background: rgba(191, 239, 255, 0.18); border-color: var(--variant-crystal, #bfefff); }
/* At most four cards per row (sprites and move names need the room), fewer when the column
 * is narrower: each track is at least a quarter of the width minus the gaps, and never
 * under 220 px. Rows stretch, so every card in a row is as tall as the tallest. */
.market-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(max(220px, calc(25% - 9px)), 1fr)); gap: 12px; align-items: stretch; }
.market-empty { grid-column: 1 / -1; }
.market-more { grid-column: 1 / -1; display: flex; justify-content: center; padding: 4px 0; }   /* "Load more" under the cards (MASTER EC-04) */
/* A product card: sprite stage, body, price footer. The whole card is a button (detail window). */
.market-card {
  display: flex;
  flex-direction: column;
  border-radius: var(--radius, 12px);
  background: var(--panel-2, rgba(255, 255, 255, 0.05));
  border: 1px solid rgba(255, 255, 255, 0.1);
  overflow: hidden;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.08s ease, box-shadow 0.15s ease;
}
.market-card:hover, .market-card:focus-visible {
  border-color: rgba(255, 255, 255, 0.28);
  transform: translateY(-1px);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
  outline: none;
}
.market-card.mine { border-color: color-mix(in srgb, var(--accent, #d64545) 65%, transparent); }
/* The stage: ~125 px, a soft floor glow under the sprite, the form label and the Legendary
 * tag in the corners. Golden, Dark, Frozen and Shadow each get their own quiet aura (the sprite
 * stays the hero); Shiny and normal stand on the neutral stage (owner, 27 September).
 * The card carries `form-<variant>`, NOT `variant-<variant>`: the bare `.variant-golden` /
 * `.variant-dark` / ... rules at the top of this file are the sprite's colour FILTER, and on
 * the card they tinted everything - the move chips of a golden Gyarados went gold too (owner,
 * 25 September). */
.market-art {
  position: relative;
  display: flex; align-items: flex-end; justify-content: center;
  height: 126px; padding-bottom: 8px;
  background:
    radial-gradient(ellipse at 50% 82%, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.03) 55%, transparent 72%),
    linear-gradient(180deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.04));
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.market-art .sprite-wrap { transform: scale(1.25); transform-origin: bottom center; }
.market-art .variant-label { position: absolute; top: 8px; right: 8px; font-size: 9.5px; }
.market-legend { position: absolute; top: 8px; left: 8px; font-size: 9px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: #ffd86b; }
/* The yellow-gold aura belongs to Golden, not to Shiny (owner, 27 September: "bij shiny een
   geel/goude gloed, dat moet bij golden zo zijn ... maar niet bij shiny en normaal"). Shiny and
   normal keep the neutral stage above: a shiny's own palette and sparkle already mark it. */
.market-card.form-golden .market-art, .market-detail.form-golden .market-art { background: radial-gradient(ellipse at 50% 82%, rgba(255, 216, 107, 0.34), rgba(255, 216, 107, 0.07) 55%, transparent 72%), linear-gradient(180deg, rgba(70, 55, 10, 0.4), rgba(0, 0, 0, 0.04)); }
.market-card.form-dark .market-art, .market-detail.form-dark .market-art { background: radial-gradient(ellipse at 50% 82%, rgba(167, 139, 250, 0.28), rgba(167, 139, 250, 0.05) 55%, transparent 72%), linear-gradient(180deg, rgba(40, 20, 80, 0.45), rgba(0, 0, 0, 0.04)); }
.market-card.form-frozen .market-art, .market-detail.form-frozen .market-art { background: radial-gradient(ellipse at 50% 82%, rgba(143, 227, 255, 0.3), rgba(143, 227, 255, 0.05) 55%, transparent 72%), linear-gradient(180deg, rgba(10, 60, 90, 0.4), rgba(0, 0, 0, 0.04)); }
.market-card.form-shadow .market-art, .market-detail.form-shadow .market-art { background: radial-gradient(ellipse at 50% 82%, rgba(92, 60, 130, 0.42), rgba(92, 60, 130, 0.08) 55%, transparent 72%), linear-gradient(180deg, rgba(20, 10, 40, 0.6), rgba(0, 0, 0, 0.08)); }
.market-card.form-crystal .market-art, .market-detail.form-crystal .market-art { background: radial-gradient(ellipse at 50% 82%, rgba(190, 240, 255, 0.32), rgba(190, 240, 255, 0.06) 55%, transparent 72%), linear-gradient(180deg, rgba(12, 45, 60, 0.45), rgba(0, 0, 0, 0.04)); }
.market-body { display: flex; flex-direction: column; gap: 6px; padding: 9px 11px 8px; flex: 1 1 auto; min-width: 0; }
/* Name left, level + gender right, on one line. */
.market-title { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; min-width: 0; }
.market-name { font-size: 14px; font-weight: 700; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.market-lv { flex: none; display: inline-flex; align-items: center; gap: 4px; color: var(--muted, #b8c0d4); font-weight: 600; font-size: 12px; font-variant-numeric: tabular-nums; }
.market-lv .gender { font-size: 13px; }
.market-body .types-row .type-badge { font-size: 9.5px; padding: 1px 6px; }
/* HP / IV / EXP in three fixed columns: the label above, the value below. */
.market-stats { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 4px; font-size: 10px; color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; }
.market-stats > span { display: flex; flex-direction: column; gap: 1px; letter-spacing: 0.04em; }
.market-stats b { color: var(--text, #fff); font-weight: 700; font-size: 12.5px; letter-spacing: 0; }
/* The four moves, two per row, always two rows high so the cards line up. */
.market-moves { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; min-height: 44px; align-content: start; }
.market-move {
  --move-color: #8a8a8a;
  padding: 2px 6px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--move-color) 55%, transparent);
  background: color-mix(in srgb, var(--move-color) 18%, transparent);
  font-size: 9.5px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: center;
}
.market-move.type-normal { --move-color: var(--type-normal); }
.market-move.type-fire { --move-color: var(--type-fire); }
.market-move.type-water { --move-color: var(--type-water); }
.market-move.type-electric { --move-color: var(--type-electric); }
.market-move.type-grass { --move-color: var(--type-grass); }
.market-move.type-ice { --move-color: var(--type-ice); }
.market-move.type-fighting { --move-color: var(--type-fighting); }
.market-move.type-poison { --move-color: var(--type-poison); }
.market-move.type-ground { --move-color: var(--type-ground); }
.market-move.type-flying { --move-color: var(--type-flying); }
.market-move.type-psychic { --move-color: var(--type-psychic); }
.market-move.type-bug { --move-color: var(--type-bug); }
.market-move.type-rock { --move-color: var(--type-rock); }
.market-move.type-ghost { --move-color: var(--type-ghost); }
.market-move.type-dragon { --move-color: var(--type-dragon); }
.market-move.type-dark { --move-color: var(--type-dark); }
.market-move.type-steel { --move-color: var(--type-steel); }
.market-move.type-fairy { --move-color: var(--type-fairy); }
.market-seller { margin-top: auto; padding-top: 2px; font-size: 11px; color: var(--muted, #b8c0d4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The footer: price left (the strongest thing on the card), the action right. */
.market-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 11px;
  background: rgba(0, 0, 0, 0.24);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.market-price { font-size: 15px; font-weight: 800; color: #ffd86b; font-variant-numeric: tabular-nums; white-space: nowrap; }
.market-short { font-size: 11px; font-weight: 600; color: var(--muted, #b8c0d4); white-space: nowrap; }   /* you cannot afford this one */
.market-balance { font-weight: 700; color: #ffd86b; font-variant-numeric: tabular-nums; }
/* The detail window (a click on a card). */
.market-detail { display: flex; flex-direction: column; gap: 10px; }
.market-detail-top { display: grid; grid-template-columns: minmax(150px, 200px) minmax(0, 1fr); gap: 14px; align-items: start; }
.market-detail-art { height: 160px; border-radius: var(--radius-sm, 8px); border: 1px solid rgba(255, 255, 255, 0.08); }
.market-detail-art .sprite-wrap { transform: scale(1.5); }
.market-detail-head { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.market-detail-name { font-size: 18px; font-weight: 800; line-height: 1.2; }
.market-facts { display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; margin: 4px 0 0; font-size: 12.5px; }
.market-facts dt { color: var(--muted, #b8c0d4); font-weight: 600; }
.market-facts dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.market-detail-label { font-size: 0.78rem; font-weight: 700; color: var(--muted, #b8c0d4); letter-spacing: 0.04em; text-transform: uppercase; }
.market-detail-ivs { display: grid; grid-template-columns: minmax(0, 1fr) minmax(150px, 200px); gap: 6px 14px; align-items: center; }
.market-detail-ivs .market-detail-label { grid-column: 1 / -1; }
.market-ivs { display: grid; grid-template-columns: 1fr 1fr; gap: 3px 12px; font-size: 12px; color: var(--muted, #b8c0d4); }
.market-ivs span { display: flex; justify-content: space-between; gap: 8px; }
.market-ivs b { color: var(--text, #fff); font-variant-numeric: tabular-nums; }
.market-radar { padding: 0; }
.market-radar .iv-radar { max-width: 200px; }
.market-detail-moves { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; }
.market-detail-move { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.market-detail-move .market-move { font-size: 11px; padding: 3px 8px; text-align: left; }
.market-detail-seller { font-size: 12.5px; color: var(--muted, #b8c0d4); }
.market-detail-price { display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; border-radius: var(--radius-sm, 8px); background: rgba(0, 0, 0, 0.24); font-weight: 600; }
.market-detail-price .market-price { font-size: 18px; }
.market-detail-short { font-size: 12px; color: var(--accent-light, #f08c8c); }
/* The "Sold" tab: one row per finished deal. */
.market-history { display: flex; flex-direction: column; gap: 6px; }
.market-history-title { display: flex; align-items: center; gap: 8px; margin: 4px 0 0; font-size: 0.85rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted, #b8c0d4); }
.market-history-list { display: flex; flex-direction: column; gap: 4px; }
.market-deal { display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 6px 10px; border-radius: var(--radius-sm, 8px); background: var(--panel-2, rgba(255, 255, 255, 0.05)); }
.market-deal-name { font-weight: 700; display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.market-deal-name .variant-label { font-size: 9px; }
.market-deal-price { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }
/* The sell wizard: three steps, the body of the step, then Back / Continue. */
.market-wiz { display: flex; flex-direction: column; gap: 12px; }
.market-steps { display: flex; gap: 4px; margin: 0; padding: 0; list-style: none; counter-reset: none; }
.market-step { flex: 1 1 0; display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; color: var(--muted, #b8c0d4); background: rgba(0, 0, 0, 0.2); white-space: nowrap; overflow: hidden; }
.market-step-n { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; background: rgba(255, 255, 255, 0.1); font-size: 11px; font-weight: 800; }
.market-step.active { color: var(--text, #fff); background: var(--accent-soft, rgba(214, 69, 69, 0.16)); box-shadow: inset 0 0 0 1px rgba(214, 69, 69, 0.5); }
.market-step.active .market-step-n { background: var(--accent, #d64545); color: #fff; }
.market-step.done { color: var(--text, #fff); }
.market-step.done .market-step-n { background: rgba(88, 180, 110, 0.5); }
.market-step-label { overflow: hidden; text-overflow: ellipsis; }
.market-wiz-body { display: flex; flex-direction: column; gap: 10px; }
.market-wiz-nav { display: flex; justify-content: space-between; gap: 8px; padding-top: 4px; border-top: 1px solid rgba(255, 255, 255, 0.08); }
.market-wiz-nav .btn:only-child { margin-left: auto; }
/* Step 1: your Pokémon as picks, two per row. */
.market-pick-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 6px; max-height: 50vh; overflow: auto; padding-right: 2px; }
.market-pick {
  display: flex; align-items: center; gap: 8px; padding: 6px 10px; text-align: left;
  border-radius: var(--radius-sm, 8px); border: 1px solid rgba(255, 255, 255, 0.1);
  background: var(--panel-2, rgba(255, 255, 255, 0.05)); color: var(--text, #fff); cursor: pointer;
}
.market-pick:hover { border-color: rgba(255, 255, 255, 0.28); }
.market-pick.selected { border-color: var(--accent, #d64545); background: var(--accent-soft, rgba(214, 69, 69, 0.16)); }
.market-pick[hidden] { display: none; }
.market-pick-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.market-pick-name { font-weight: 700; font-size: 13px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.market-pick-sub { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted, #b8c0d4); }
.market-pick-sub .variant-label { font-size: 9px; }
/* Step 2: the chosen one, recent sales, the price box and the sums. */
.market-wiz-chosen { display: flex; align-items: center; gap: 10px; }
.market-similar { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; padding: 8px 10px; border-radius: var(--radius-sm, 8px); background: rgba(0, 0, 0, 0.2); font-size: 12.5px; }
.market-similar .loading-block { min-height: 0; padding: 0; }
.market-similar-label { color: var(--muted, #b8c0d4); font-weight: 600; }
.market-similar b { color: #ffd86b; font-variant-numeric: tabular-nums; }
.market-price-field { margin: 0; }
.market-price-cap.is-error { color: #ff9a9d; }   /* "Highest price: ₽9,999,999." (MASTER EC-06) */
.market-price-row { display: flex; align-items: center; gap: 8px; }
.market-price-row .input { flex: 1; min-width: 0; font-size: 1.05rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.market-price-yen { font-size: 1.1rem; font-weight: 800; color: #ffd86b; }
.market-sum { display: flex; flex-direction: column; gap: 3px; padding: 8px 10px; border-radius: var(--radius-sm, 8px); background: rgba(0, 0, 0, 0.2); font-size: 12.5px; }
.market-sum-row { display: flex; justify-content: space-between; gap: 10px; color: var(--muted, #b8c0d4); }
.market-sum-val { color: var(--text, #fff); font-weight: 600; font-variant-numeric: tabular-nums; }
.market-sum-val.strong { font-weight: 800; color: #ffd86b; }
/* Step 3: the sprite on its stage next to the sums. */
.market-confirm { display: grid; grid-template-columns: minmax(140px, 180px) minmax(0, 1fr); gap: 14px; align-items: start; }
.market-confirm-art { height: 150px; border-radius: var(--radius-sm, 8px); border: 1px solid rgba(255, 255, 255, 0.08); }
.market-confirm-art .sprite-wrap { transform: scale(1.4); }
.market-confirm-main { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.picker-row.selected { border-color: var(--accent, #d64545); background: var(--accent-soft, rgba(214, 69, 69, 0.16)); }
@media (max-width: 640px) {
  .market-detail-top, .market-detail-ivs, .market-confirm { grid-template-columns: 1fr; }
  .market-detail-art, .market-confirm-art { max-width: 220px; }
  .market-radar .iv-radar { max-width: 240px; }
}
@media (max-width: 480px) {
  .market-tabs { max-width: none; }
  .market-tabs .tab { padding: 6px 4px; font-size: 0.78rem; }
  .market-toolbar select { max-width: none; flex: 1 1 100%; }
  .market-steps { gap: 3px; }
  .market-step { padding: 4px 6px; font-size: 11px; }
  .market-step:not(.active) .market-step-label { display: none; }   /* only the active step keeps its words */
  .market-step:not(.active) { flex: 0 0 auto; }
  .market-wiz-nav { flex-wrap: wrap; }
  .market-pick-grid { grid-template-columns: 1fr; }
}

/* 9. Responsive                                                              */
/* ------------------------------------------------------------------------- */

@media (max-width: 900px) {
  .battle { --arena-h: 250px; }
  .battle-sprite .sprite-wrap { transform: scale(calc(1.4 * var(--grow, 1))); }
  .battle-sprite.opp .sprite-wrap { transform: scale(calc(1.25 * var(--grow, 1))); }
  .battle-info { padding: 6px 9px; }
  .pkm { grid-template-columns: 1fr; }
  .team-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }   /* two cards wide on a laptop */
  .shop-row { grid-template-columns: 40px 1fr auto; }
  .shop-row .item-price { grid-column: 2; }
  .shop-row .item-total { grid-column: 3; }
}

@media (max-width: 600px) {
  /* Phones: moves in a 2x2 grid, the party strip 3+3, the arena stays big (docs/BATTLE-DECISION.md 6.2). */
  .move-bar { grid-template-columns: 1fr 1fr; }
  .party-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .battle-secondary { justify-content: stretch; }
  .battle-secondary .btn { flex: 1; }
  /* Phones: one column. Owner (21 September, evening): the arena stays put and the part under
     it - moves, party, text panel (after the battle: outcome, text panel) - scrolls on its
     own, so a scrollbar only ever runs beside those and the page itself never scrolls. The
     text panel comes last, under the party, so the moves sit right under the Pokemon. */
  .battle {
    --arena-h: 240px;
    display: flex;
    flex-direction: column;
    margin-top: 0;
    height: calc(100vh - var(--header-h, 52px));
    height: calc(100dvh - var(--header-h, 52px));
    min-height: 0;   /* no overflow:hidden here: the arena bleeds 8 px into the column padding */
  }
  .battle-arena { flex: none; min-height: 220px; }
  .battle-below {
    display: flex; flex-direction: column; gap: 8px;
    flex: 1 1 auto; min-height: 0;
    padding: 8px 0 var(--main-pad, 8px);
    overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  }
  .battle-below > * { flex: none; margin: 0; }
  .battle-below .battle-actions, .battle-below .battle-outcome { order: 1; }
  .battle-below .party-strip { order: 2; }
  .battle-below .battle-msg { order: 3; }
  .view-scroll:has(> .battle) { overflow-y: hidden; padding-bottom: 0; }
  /* A fixed 132 px box in the flex column (containment would collapse it to 0 here). */
  .battle-msg, .battle-msg.expanded { margin-top: 0; height: 132px; max-height: 132px; contain: none; border-radius: var(--radius, 12px); }
  .battle-actions, .battle-outcome { border-radius: var(--radius, 12px); }
  .battle-sprite { padding-bottom: 8px; }
  .battle-sprite .sprite-wrap { transform: scale(calc(1.2 * var(--grow, 1))); }
  .battle-sprite.opp .sprite-wrap { transform: scale(calc(1.1 * var(--grow, 1))); }
  /* Smaller sprites (scale 1.2 / 1.1) get smaller bases; feet at 18 px here (8 px padding). */
  .battle-platform.opp img { width: 160px; margin-bottom: -2px; }
  .battle-platform.you img { width: 230px; margin-bottom: -6px; }
  /* Owner (21 September): on a phone both boxes were a sliver - 30% of a 360 px arena is
     about 110 px, so the name, HP and EXP no longer fitted. They now take a bit over half the
     arena each (they sit in opposite corners, so they never meet) with smaller type. */
  .battle-info { width: min(220px, 56%); padding: 4px 7px; border-radius: 10px 4px 10px 4px; }
  .battle-info.you { width: calc(min(220px, 56%) + max(0px, var(--val-w, 0px) - 44px)); padding: 4px 7px; }
  .battle-info.opp { border-radius: 4px 10px 4px 10px; }
  /* Review (21 September): at 56 % the boxes covered the top of the foe and the feet of your
     own Pokemon, because both sat in the corner of their own sprite. On a phone they now sit
     in the OPPOSITE corner, as the handhelds draw them: the foe's box top-left (its sprite
     stands right), yours bottom-right (your sprite stands left). Desktop keeps its layout. */
  .battle-foe-col { right: auto; left: 8px; align-items: flex-start; width: calc(min(220px, 56%) + max(0px, var(--val-w, 0px) - 44px)); }
  .battle-foe-col .battle-info { width: 100%; }
  .battle-info.you { left: auto; right: 10px; border-radius: 4px 10px 4px 10px; }
  .battle-info.opp { border-radius: 10px 4px 10px 4px; }
  .battle-info-name { font-size: 12px; }
  .battle-info-level { font-size: 12px; }
  .battle-info-row { gap: 5px; margin-top: 4px; }
  .battle-info-label { width: 19px; font-size: 9px; }
  .battle-info-val { width: max(44px, var(--val-w, 0px)); font-size: 9.5px; }
  .battle-info-trainer { font-size: 11px; margin-bottom: 3px; }
  .battle-info-trainer img { width: 20px; height: 20px; }
  .battle-info-extras { gap: 5px; }
  .battle-log { max-height: 220px; }
  /* Owner: on a phone you see a lot of log lines at once, so they go a notch smaller
     (12 -> 10.5 px) and sit tighter. DECISIONS 118. */
  .battle-log-lines { font-size: 10.5px; line-height: 1.4; }
  .battle-log-line { padding: 1px 8px 1px 14px; }
  .battle-log-turn { padding: 2px 8px; font-size: 11px; }
  .log-time { font-size: 9.5px; margin-right: 6px; }
  .battle-msg-line { font-size: 12.5px; }
  .team-grid, .team-list { grid-template-columns: 1fr; }
  .team-row { grid-template-columns: 18px 40px minmax(0, 1fr); }
  /* The 72 px sprite in the 40 px column fell over the name and the labels ("ectric", "5" for "Lv. 5");
     it scales to its column now, and a long nickname stays inside its card (bug round 28 September, PK-10). */
  .team-row .team-sprite { width: 40px; height: 48px; }
  .team-row .team-sprite img { max-width: 40px; max-height: 48px; }
  .team-row .team-name { min-width: 0; }
  .team-row .team-name > :first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
  .team-row .team-actions { justify-content: flex-end; padding-top: 4px; }
  .pc-grid { grid-template-columns: repeat(3, 1fr); }
  .dex-grid { grid-template-columns: repeat(3, 1fr); }
  .dex-filters input[type="search"] { min-width: 0; width: 100%; }
  .shop-row { grid-template-columns: 40px 1fr; }
  .shop-row .item-price, .shop-row .item-total, .shop-row .qty-stepper, .shop-row .btn { grid-column: 2; }
  .bag-row { grid-template-columns: 40px 1fr; }
  .bag-row .btn, .bag-row .in-battle-tag { grid-column: 2; }
  .outcome-grid { grid-template-columns: 1fr; }
}

/* Team row helpers: the "Move up" arrow is the chevron-down icon flipped. */
.btn.flip svg { transform: rotate(180deg); }

/* ------------------------------------------------------------------------- */
/* VS cut-in (js/ui/battle.js showVsSplash): the slanted "VS" band Black 2/White 2   */
/* show before Gym Leader battles — accent band, foe portrait from the right.       */
/* ------------------------------------------------------------------------- */
.vs-splash {
  position: absolute; inset: 0; z-index: 20;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; cursor: pointer;
  background: rgba(8, 10, 16, 0.45);
  animation: vsFadeIn 0.15s ease-out both;
}
.vs-splash.leaving { animation: vsFadeOut 0.25s ease-in both; pointer-events: none; }
.vs-band {
  position: relative;
  width: 120%; height: 140px; padding: 0 12%; box-sizing: border-box;
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14px;
  background: linear-gradient(90deg, var(--accent-strong), var(--accent) 45%, var(--accent-hover));
  color: var(--accent-text);
  /* The diagonal slash of the cut-in; the cells un-skew so text and art stay upright. */
  transform: skewY(-8deg);
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.9), 0 12px 32px rgba(0, 0, 0, 0.45);
  animation: vsBandIn 0.3s cubic-bezier(0.2, 0.9, 0.3, 1.1) both;
}
.vs-cell { transform: skewY(8deg); min-width: 0; }
.vs-cell-portrait { display: flex; justify-content: flex-end; align-items: flex-end; height: 128px; }
.vs-text {
  display: inline-block;
  font-size: 46px; font-weight: 900; font-style: italic; line-height: 1; letter-spacing: 0.04em;
  text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.35);
  animation: vsPop 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.4) 0.1s both;
}
.vs-name { font-size: 22px; font-weight: 800; line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35); }
.vs-sub { font-size: 14px; opacity: 0.92; }
.vs-portrait {
  /* gen5 strips are 128×64 → integer 2× (256×128); a fallback battle sprite is scaled to the same height. */
  height: 128px; width: auto; max-width: 256px;
  image-rendering: pixelated;
  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.45));
  animation: vsSlideIn 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) 0.05s both;
}
/* The Gen 6-9 leaders' busts (versus/bust/, scripts/build_vs_busts.py) are painted portraits, not
   pixel art: scaled smoothly. Matched on the file, so a fallback battle sprite stays pixelated. */
.vs-portrait[src*="/bust/"] { image-rendering: auto; }
/* Static version (prefers-reduced-motion): no motion at all, shown briefly then removed. */
.vs-splash.static, .vs-splash.static .vs-band, .vs-splash.static .vs-text, .vs-splash.static .vs-portrait { animation: none; }
@keyframes vsFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes vsFadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes vsBandIn { from { transform: skewY(-8deg) translateX(-110%); } to { transform: skewY(-8deg) translateX(0); } }
@keyframes vsPop { from { transform: scale(2.4); opacity: 0; } 60% { transform: scale(0.92); opacity: 1; } to { transform: scale(1); opacity: 1; } }
@keyframes vsSlideIn { from { transform: translateX(160%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .vs-splash, .vs-splash.leaving, .vs-band, .vs-text, .vs-portrait { animation: none; }
}
@media (max-width: 600px) {
  .vs-band { height: 112px; padding: 0 10%; gap: 10px; }
  .vs-text { font-size: 32px; }
  .vs-name { font-size: 16px; }
  .vs-sub { font-size: 12px; }
  .vs-cell-portrait { height: 96px; }
  .vs-portrait { height: 96px; max-width: 192px; }
}

/* Reusable VS banner: a gen5 strip (128×64) at integer 2×. */
.vs-banner { display: inline-block; width: 256px; height: 128px; }
.vs-banner img { width: 256px; height: 128px; image-rendering: pixelated; }

/* Gym rows (js/ui/gyms.js): the strip as a compact right-aligned banner (1×, height 64) so the
 * row stays under ~72 px; the art sits against the strip's right edge, hence object-position. */
.vs-banner-gym { flex: none; width: 192px; height: 64px; border-radius: var(--radius-xs, 6px); background: linear-gradient(90deg, transparent, var(--accent-soft)); }
.vs-banner-gym img { width: 100%; height: 64px; object-fit: contain; object-position: right center; image-rendering: pixelated; }
@media (max-width: 700px) { .vs-banner-gym { width: 128px; } }

/* Incoming PvP challenge (js/ui/notifications.js): the challenger's sprite on a small accent band. */
.vs-challenge { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: var(--radius-xs, 6px); background: linear-gradient(90deg, var(--accent-strong), var(--accent)); color: var(--accent-text); }
.vs-challenge b { color: #fff; }
.vs-challenge-avatar { width: 32px; height: 32px; object-fit: contain; object-position: center bottom; image-rendering: pixelated; flex: none; }
.vs-challenge-text { font-size: 0.8rem; line-height: 1.3; min-width: 0; }

/* --------------------------------------------------------------------------
   Item cards: the Bag pockets and the Poke Mart shelves (DECISIONS 120)
   --------------------------------------------------------------------------
   Owner (21 September): both screens were "te simpel" - a flat list of rows. They now use
   the same card: art in a rounded tile with the count (bag) or the price (shop) on it, the
   name and description underneath, and the action on its own line at the bottom. One set of
   rules for both so they cannot drift apart. */
.bag-panel { padding: 4px 14px 14px; }

/* Bag pockets as stacked sections (DECISIONS 140). The pocket tab bar of DECISIONS 120 is
 * gone (its `.bag-tab*` rules were replaced by these): the owner wanted to scroll past every
 * pocket instead of clicking one open. Each section has a header row - the pocket icon in a
 * coloured circle, the name with a count chip, a one-line hint - and its item cards under it.
 * The pocket colours follow the pocket tabs of FRLG/HGSS: green medicine, purple stones (a
 * deviation: the games keep stones in Items), red balls, blue items. `--pocket` is the
 * single knob: the icon circle, the count chip and the art tiles all mix from it. */
.bag-pocket { --pocket: #a9b3c7; padding: 12px 0 2px; }
.bag-pocket + .bag-pocket { border-top: 1px solid rgba(255, 255, 255, 0.08); margin-top: 6px; }
.bag-pocket.cat-medicine { --pocket: #62d26f; }
.bag-pocket.cat-evolution { --pocket: #c98fe0; }
.bag-pocket.cat-ball { --pocket: #ff5c5c; }
.bag-pocket.cat-misc { --pocket: #6cb4ff; }
.bag-pocket.cat-tm { --pocket: #f5b25e; }   /* TMs (DECISIONS 367) */
/* A TM card and a TM row: the move's type badge and numbers under its name. */
.item-tm-meta, .tm-row-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 2px 0; }
/* The Mart's TM shelf (js/ui/shop.js tmPanel, DECISIONS 367): one line per TM and a search field,
   because a late Mart sells up to 91 of them. */
.shop-tms { display: grid; gap: 8px; }
.shop-tms-head { display: flex; align-items: center; gap: 8px 12px; flex-wrap: wrap; }
.shop-tms-head h3 { margin: 0; display: inline-flex; align-items: center; gap: 6px; }
.shop-tms-head .tm-search { margin-left: auto; min-width: 200px; flex: 0 1 260px; }
.tm-list { display: grid; gap: 4px; }
.tm-row {
  display: grid; grid-template-columns: 32px minmax(0, 1fr) auto auto; align-items: center; gap: 10px;
  padding: 6px 10px; border-radius: var(--radius-sm); background: var(--panel-2);
}
.tm-row[hidden] { display: none; }
.tm-row .item-icon { width: 28px; height: 28px; }
.tm-row .item-icon img { width: 100%; height: 100%; }
.tm-row-main { min-width: 0; }
.tm-row-name { padding: 0; border: 0; background: none; color: var(--text); font: inherit; font-weight: 700; cursor: pointer; text-align: left; }
.tm-row-name:hover { text-decoration: underline; }
.tm-row-price { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Who of your party can use a TM, right on the shelf (owner, 25 September, DECISIONS 377):
   lit = can learn it, check = already knows it, grey = cannot (title says which). */
.tm-row-party { display: flex; gap: 3px; margin-top: 3px; }
.tm-row-party .tm-can { position: relative; width: 24px; height: 24px; flex: 0 0 auto; }
.tm-row-party .tm-can-icon { width: 24px; height: 24px; image-rendering: pixelated; }
.tm-row-party .tm-can.cant { opacity: 0.25; filter: grayscale(1); }
.tm-row-party .tm-can.knows { opacity: 0.6; }
.tm-row-party .tm-can.knows::after { content: '✓'; position: absolute; right: -3px; bottom: -2px; font-size: 11px; font-weight: 700; color: #7ad97a; text-shadow: 0 0 3px rgba(0, 0, 0, 0.9); }
@media (max-width: 520px) {
  .tm-row { grid-template-columns: 28px minmax(0, 1fr) auto; }
  .tm-row-price { grid-column: 2; grid-row: 2; }
  .shop-tms-head .tm-search { margin-left: 0; flex: 1 1 100%; }
}
/* A TM a Pokémon can't learn or already knows, in the summary's TM list: greyed out. */
.move-row.is-off { opacity: 0.55; }
.bag-pocket-head { display: flex; align-items: center; gap: 11px; margin-bottom: 10px; }
.bag-pocket-icon {
  flex: none; width: 36px; height: 36px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--pocket) 20%, transparent);
  color: var(--pocket);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--pocket) 45%, transparent);
}
.bag-pocket-text { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.bag-pocket-title { margin: 0; display: flex; align-items: center; gap: 8px; font-size: 0.98rem; font-weight: 800; letter-spacing: 0.01em; }
.bag-pocket-count {
  min-width: 22px; padding: 0 7px; border-radius: 999px;
  background: color-mix(in srgb, var(--pocket) 22%, transparent);
  color: var(--pocket);
  font-size: 0.7rem; font-weight: 800; line-height: 18px; text-align: center;
  font-variant-numeric: tabular-nums;
}
.bag-pocket-hint { font-size: 0.74rem; color: var(--muted); }
/* The art tile takes a whisper of the pocket colour, so a card tells its pocket on its own. */
.bag-pocket .item-card-art { background: color-mix(in srgb, var(--pocket) 14%, rgba(255, 255, 255, 0.05)); }
.bag-pocket .item-card:hover .item-card-art { background: color-mix(in srgb, var(--pocket) 22%, rgba(255, 255, 255, 0.06)); }
.bag-pocket .item-card:hover { border-color: color-mix(in srgb, var(--pocket) 45%, transparent); }
/* Nothing in any pocket: one small centred panel with the way to the Poké Mart. */
.bag-empty { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 26px 16px; text-align: center; }
.bag-empty-icon { display: inline-flex; width: 56px; height: 56px; border-radius: 50%; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.07); color: var(--muted); margin-bottom: 4px; }
.bag-empty-title { font-weight: 800; font-size: 1rem; }
.bag-empty .btn { margin-top: 8px; }

.bag-grid, .shop-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(212px, 1fr));
  gap: 10px;
}
.item-card {
  display: flex; flex-direction: column;
  padding: 12px 12px 10px;
  border: var(--panel-border); border-radius: var(--radius-sm, 8px);
  background: rgba(255, 255, 255, 0.045);
  transition: border-color 0.15s, background 0.15s;
}
.item-card:hover { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.18); }
.item-card-art { position: relative; width: 52px; height: 52px; display: flex; align-items: center; justify-content: center; border-radius: 12px; background: rgba(255, 255, 255, 0.07); }
.item-card-art .item-icon { width: 40px; height: 40px; }
.item-card-art .item-icon img, .item-card-art .item-icon svg { width: 36px; height: 36px; }
.item-qty-badge, .item-price-badge {
  position: absolute; right: -6px; bottom: -6px;
  padding: 1px 7px; border-radius: 999px;
  background: var(--accent); color: #fff;
  font-size: 0.7rem; font-weight: 800; white-space: nowrap;
  box-shadow: var(--shadow-sm);
}
.item-price-badge { background: #1f2a3f; color: var(--accent-light); border: 1px solid rgba(255, 255, 255, 0.14); }
.item-card-body { flex: 1 1 auto; margin: 10px 0 8px; min-width: 0; }
.item-card .item-name { font-weight: 800; font-size: 0.92rem; line-height: 1.2; }
.item-card .item-desc { margin-top: 3px; font-size: 0.74rem; line-height: 1.35; color: var(--muted); }
.item-card-foot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.item-card-foot .btn { margin-left: auto; }
.item-tag {
  display: inline-flex; align-items: center;
  padding: 2px 8px; border-radius: 999px;
  background: rgba(255, 75, 75, 0.16); color: #ffb3b3;
  font-size: 0.68rem; font-weight: 700;
}
.item-tag.quiet { background: rgba(255, 255, 255, 0.08); color: var(--muted); }

/* Shop: the wallet bar above the shelves, so the money is never the quietest thing here. */
.shop-wallet { display: flex; align-items: center; gap: 12px; padding: 12px 14px; }
.shop-wallet-icon {
  flex: none; width: 38px; height: 38px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(246, 201, 69, 0.16); color: #f6c945;
}
.shop-wallet-text { display: flex; flex-direction: column; line-height: 1.1; }
.shop-wallet-label { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.01em; color: var(--muted); }
.shop-wallet .shop-money { font-size: 1.5rem; font-weight: 800; color: #f6c945; }
.shop-wallet-hint { margin-left: auto; text-align: right; }
.shop-card .item-card-foot { border-top: 1px solid rgba(255, 255, 255, 0.07); padding-top: 8px; }
.shop-card .item-total { font-weight: 800; color: var(--accent-light); white-space: nowrap; }
.shop-card .item-total.too-expensive { color: var(--danger); }
.money-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 10px; border-radius: 999px;
  background: rgba(246, 201, 69, 0.14); color: #f6c945;
  font-weight: 800; font-size: 0.82rem;
}
/* The Trainer Shop (js/ui/trainershop.js, DECISIONS 472 + 489; docs/PLAN-CHAT-SPRITE.md, docs/PLAN-TRAINERSHOP.md):
   the Poké Mart's wallet bar above the tabs. A wide card (.ts-card) has a preview on the left - for the chat sprite
   your own chat line in the chat's real markup - and the text, the price and the button on the right; below 700 px
   the preview goes on top. Avatars and Card are a grid of small cards (.ts-grid, further down). */
.ts-page > .panel + .panel { margin-top: 10px; }
.ts-card { display: grid; grid-template-columns: minmax(0, 300px) minmax(0, 1fr); gap: 18px; align-items: start; }
.ts-preview { display: flex; flex-direction: column; gap: 6px; }
.ts-preview .chat-messages { max-height: none; min-height: 0; font-size: 0.8rem; padding: 8px; gap: 4px; }
.ts-preview-label { font-size: 0.68rem; font-weight: 700; color: var(--muted); letter-spacing: 0.01em; }
.ts-title { margin: 0; font-size: 1.15rem; font-weight: 800; }
.ts-desc { margin: 6px 0 14px; font-size: 0.86rem; line-height: 1.45; color: var(--muted); max-width: 52ch; }
.ts-buy { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.ts-buy .btn { min-height: 40px; padding: 8px 18px; font-size: 0.95rem; }
.ts-buy .muted { font-size: 0.78rem; }
.ts-owned {
  display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; border-radius: 999px;
  background: rgba(76, 217, 100, 0.16); color: #9df0ab; font-weight: 800; font-size: 0.9rem;
}
.ts-owned svg { width: 16px; height: 16px; }
.ts-hint { font-size: 0.8rem; color: var(--muted); }
@media (max-width: 699px) {
  .ts-card { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .ts-buy .btn { flex: 1 1 auto; }
}
/* Tabs, group chips and the grid of looks (picture 01/02 of docs/PLAN-TRAINERSHOP.md). */
.ts-shop { padding: 12px 14px 14px; }
.ts-tabs { margin: 0 0 12px; }
.ts-tabs .tab { flex: 1 1 0; min-width: 0; }
.ts-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.ts-chip {
  display: inline-flex; align-items: baseline; gap: 2px; padding: 4px 11px; border-radius: 999px; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.14); background: var(--panel-2); color: var(--text); font: inherit; font-size: 0.76rem;
}
.ts-chip:hover, .ts-chip:focus-visible { border-color: var(--accent); outline: none; }
.ts-chip.on { border-color: var(--accent); background: var(--accent-soft); font-weight: 700; }
.ts-chip-price { color: #f6c945; font-weight: 700; }
.ts-lead { margin: 0 0 10px; }
.ts-note { margin: 12px 0 0; }
.ts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; }
.ts-grid.wide { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.ts-item {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 0;
  padding: 10px 8px 9px; border-radius: 12px; background: var(--panel-2); border: 1px solid rgba(255, 255, 255, 0.08); text-align: center;
}
.ts-art { display: flex; align-items: flex-end; justify-content: center; width: 100%; height: 76px; }
.ts-art > img { height: 72px; width: auto; max-width: 100%; object-fit: contain; image-rendering: pixelated; filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.35)); }
.ts-item-name { max-width: 100%; font-weight: 800; font-size: 0.84rem; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts-item-sub { max-width: 100%; min-height: 1.3em; font-size: 0.68rem; line-height: 1.3; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts-item > .btn, .ts-item > .ts-inuse, .ts-item > .ts-lock, .ts-item > .ts-owned { margin-top: 5px; width: 100%; min-height: 30px; justify-content: center; }
.ts-item > .btn { font-size: 0.8rem; }
.ts-item > .ts-owned { padding: 4px 8px; font-size: 0.72rem; }
.ts-item.is-owned { border-color: rgba(76, 217, 100, 0.35); }
.ts-item.is-used { border-color: rgba(76, 217, 100, 0.75); box-shadow: inset 0 0 0 1px rgba(76, 217, 100, 0.35); background: rgba(76, 217, 100, 0.07); }
.ts-item.is-locked .ts-art > img { filter: grayscale(1) brightness(0.5) drop-shadow(0 2px 2px rgba(0, 0, 0, 0.35)); }
.ts-inuse { display: inline-flex; align-items: center; gap: 4px; border-radius: 8px; background: rgba(76, 217, 100, 0.16); color: #9df0ab; font-weight: 800; font-size: 0.76rem; }
.ts-inuse svg { width: 14px; height: 14px; }
.ts-lock { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 8px; background: rgba(246, 201, 69, 0.14); color: #f6c945; font-size: 0.68rem; font-weight: 700; line-height: 1.25; }
.ts-lock svg { width: 11px; height: 11px; flex: none; }
.ts-badge-mark { position: absolute; top: 6px; left: 6px; width: 20px; height: 20px; object-fit: contain; image-rendering: pixelated; }
.ts-badge-mark.off { filter: grayscale(1) opacity(0.6); }
.ts-foot-row { display: flex; gap: 6px; width: 100%; margin-top: 5px; }
.ts-foot-row > * { flex: 1 1 0; min-height: 30px; justify-content: center; }
/* Card: the background behind a mini head of your own card. */
.ts-cardbg {
  position: relative; display: flex; align-items: center; gap: 8px; width: 100%; height: 76px; padding: 0 10px;
  border-radius: 9px; background-size: cover; background-position: center 65%; overflow: hidden; isolation: isolate;
}
.ts-cardbg::before { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(90deg, rgba(10, 14, 26, 0.8), rgba(10, 14, 26, 0.1)); }
.ts-cardbg-avatar { display: inline-flex; align-items: flex-end; justify-content: center; width: 52px; height: 52px; border-radius: 50%; background: rgba(10, 14, 26, 0.45); box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.55); overflow: hidden; flex: none; }
.ts-cardbg-avatar img { height: 50px; image-rendering: pixelated; }
.ts-cardbg-name { font-weight: 800; font-size: 0.9rem; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Map: who follows you, and the emote pack. */
.ts-stack { display: grid; gap: 12px; }
.ts-stack > .panel.ts-card { margin: 0; background: var(--panel-2); box-shadow: none; }
.ts-follow-walk { display: flex; align-items: flex-end; justify-content: center; gap: 10px; min-height: 96px; padding: 10px; border-radius: 10px; background: linear-gradient(180deg, #4d9a4f, #3f8a43); }
.ts-follow-trainer img { height: 72px; image-rendering: pixelated; }
.ts-follow-mon { width: 44px; height: 44px; image-rendering: pixelated; order: -1; }
.ts-follow-picks { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.ts-follow-pick {
  display: inline-flex; flex-direction: column; align-items: center; gap: 2px; width: 72px; padding: 5px 4px; border-radius: 10px; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.12); background: var(--panel-2); color: var(--text); font: inherit; font-size: 0.68rem;
}
.ts-follow-pick span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts-follow-pick.auto span:first-child { display: inline-flex; height: 36px; align-items: center; color: var(--muted); }
.ts-follow-icon { width: 36px; height: 36px; image-rendering: pixelated; }
.ts-follow-pick.on { border-color: #5aa9ff; box-shadow: 0 0 0 2px rgba(90, 169, 255, 0.35); background: rgba(90, 169, 255, 0.12); }
.ts-follow-pick:hover, .ts-follow-pick:focus-visible { border-color: var(--accent); outline: none; }
.ts-emote-row { display: flex; flex-wrap: wrap; gap: 6px; padding: 12px; border-radius: 10px; background: linear-gradient(180deg, #4d9a4f, #3f8a43); }
.ts-emote-row .emote-btn { cursor: default; }
.ts-wp-grid { padding: 8px; border-radius: 10px; background: rgba(255, 255, 255, 0.04); }
/* The picture in the buy question (js/util.js confirmDialog `art`). */
.confirm-art { display: flex; align-items: center; gap: 14px; }
.confirm-art > p { margin: 0; line-height: 1.45; }
.confirm-art > .avatar { flex: none; width: 96px; height: 96px; }
.ts-confirm-bg { flex: none; width: 120px; height: 80px; border-radius: 10px; background-size: cover; background-position: center; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); }
@media (max-width: 699px) {
  .ts-shop { padding: 10px; }
  .ts-tabs .tab { padding-left: 4px; padding-right: 4px; }
  .ts-grid { grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 8px; }
  .ts-grid.wide { grid-template-columns: minmax(0, 1fr); }
  .confirm-art { flex-direction: column; text-align: center; }
}
@media (max-width: 600px) {
  .bag-grid, .shop-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
  .item-card { padding: 10px 10px 9px; }
  .item-card-art { width: 46px; height: 46px; }
  .shop-wallet .shop-money { font-size: 1.25rem; }
  .shop-wallet-hint { display: none; }
}

/* Phones in portrait: give the HUD boxes the last bit of room they need. */
@media (max-width: 420px) {
  .battle-info { width: min(210px, 62%); }
  .battle-info.you { width: calc(min(210px, 62%) + max(0px, var(--val-w, 0px) - 40px)); }
  .battle-info-name { font-size: 11.5px; }
  .battle-info-val { width: max(40px, var(--val-w, 0px)); }
  /* Owner (26 September): "Def ▼1 Atk ▲1" wrapped the top row on a phone, the box grew and the
     HP/EXP rows slid over the opponent's sprite. Tighter gaps + smaller tags so two stage chips
     plus a status still fit on one line; the name gives way first (it already ellipsizes). */
  .battle-info-top { gap: 5px; }
  .battle-info-extras { gap: 4px; }
  .battle-info-extras .variant-label { font-size: 8px; }
  .battle-info-level { font-size: 10.5px; }
  .battle-info .stage-chip { font-size: 8px; padding: 0 3px; line-height: 13px; }
  .battle-info .status-tag { font-size: 8px; padding: 1px 4px; }
}

/* Move list on the Pokemon page: the order can be changed with the two little arrows, and
   moves parked with "Save for later" wait underneath (DECISIONS 130). */
.move-order { display: flex; flex-direction: column; gap: 2px; margin-left: 6px; }
.move-order .btn { min-height: 20px; padding: 0 6px; font-size: 11px; line-height: 1; }
/* No coloured rule on the left where a card is shown on its own (owner, 21 September). */
.move-row.no-rule { border-left: 0; padding-left: 12px; }
.move-row-btn { width: 100%; text-align: left; cursor: pointer; border: 0; font: inherit; color: inherit; }
.move-row-btn:hover { background: rgba(255, 255, 255, 0.1); }
.saved-moves { margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08); display: flex; flex-direction: column; gap: 6px; }
.saved-moves h4 { margin: 0 0 2px; font-size: 12px; color: var(--muted, #b8c0d4); font-weight: 700; }
.saved-moves .move-stats { display: flex; gap: 6px; }
/* The Move Reminder (DECISIONS 255): its button under the moves, and the list in its dialog. */
.relearn-row { margin-top: 10px; display: flex; justify-content: flex-end; align-items: center; gap: 8px; flex-wrap: wrap; text-align: right; }   /* the line next to it: DECISIONS 327 */
.relearn-list { display: flex; flex-direction: column; gap: 6px; max-height: 60vh; overflow: auto; margin-top: 8px; }
.relearn-list .move-stats { display: flex; align-items: center; gap: 8px; }
.move-info-btn { text-align: left; background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer; min-width: 0; }
.move-info-btn:hover .move-name { text-decoration: underline; }

/* --------------------------------------------------------------------------
   Moves on the summary as cards (owner, 24 September, DECISIONS 314)
   -------------------------------------------------------------------------- */
/* The battle move buttons' language (.move-btn): a soft tint of the type colour (--move-color,
   set inline from --type-<type>) and an edge in it - no stripe on the left (owner, 24 September).
   The name and its category on top, the type and three values under it; the order rail or
   Teach / Discard on the right edge. The learn prompt and the Move Reminder keep .move-row. */
.moves-hint { margin: -2px 0 10px; font-size: 12px; color: var(--muted, #b8c0d4); }
.move-cards { display: flex; flex-direction: column; gap: 8px; }
.move-card {
  --move-color: #8a8a8a;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--move-color) 45%, transparent);
  background: linear-gradient(100deg, color-mix(in srgb, var(--move-color) 20%, rgba(18, 26, 44, 0.92)) 0%, rgba(18, 26, 44, 0.92) 78%);
  overflow: hidden;
  transition: border-color 0.15s ease;
}
.move-card:hover { border-color: color-mix(in srgb, var(--move-color) 75%, transparent); }
.move-card-main {
  display: flex; flex-direction: column; gap: 7px; min-width: 0;
  padding: 10px 12px;
  background: none; border: 0; color: inherit; font: inherit; text-align: left; cursor: pointer;
  transition: background 0.15s ease;
}
.move-card-main:hover { background: rgba(255, 255, 255, 0.035); }
.move-card-main:focus-visible { outline: 2px solid var(--move-color); outline-offset: -2px; border-radius: 10px 0 0 10px; }
.move-card-top { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* The name in Nunito (the rounded font of the intro page, @font-face in css/layout.css), extra bold:
   the round letters of the menus in the newer games (owner, 24 September). */
.move-card-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: 'Nunito', var(--font); font-size: 16px; font-weight: 800; letter-spacing: 0.01em;
}
/* The category, the physical/special split of the games since Gen 4. */
.move-cat {
  flex: none; margin-left: auto;
  padding: 2px 8px; border-radius: 999px;
  font-size: 9.5px; font-weight: 800; line-height: 1.3; letter-spacing: 0.07em; text-transform: uppercase;
  color: #c8ceda; background: rgba(200, 206, 218, 0.1); border: 1px solid rgba(200, 206, 218, 0.28);
}
.move-cat.physical { color: #ffb48c; background: rgba(240, 110, 60, 0.15); border-color: rgba(240, 110, 60, 0.42); }
.move-cat.special  { color: #a9c8ff; background: rgba(90, 140, 255, 0.15); border-color: rgba(90, 140, 255, 0.42); }
.move-card-stats { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 14px; }
.move-card-stats .type-badge { font-size: 9.5px; padding: 2px 8px; }
.mstat { display: inline-flex; align-items: baseline; gap: 5px; font-size: 13px; }
.mstat small { font-size: 9.5px; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; color: var(--muted, #b8c0d4); }
.mstat b { font-weight: 700; font-variant-numeric: tabular-nums; }
.mstat.pp-low b { color: var(--danger, #ff6b6b); }
/* The order rail: two halves on the right edge (one chevron up, one down), one tap each. Clearly
   visible (owner, 24 September): a light fill, a bright thick chevron and a clear line between. */
.move-order-rail { display: flex; flex-direction: column; border-left: 1px solid rgba(255, 255, 255, 0.14); }
.move-order-btn {
  flex: 1 1 0; display: flex; align-items: center; justify-content: center;
  width: 40px; min-height: 28px; padding: 0;
  border: 0; background: rgba(255, 255, 255, 0.08); color: #e6ebf5;
  cursor: pointer; transition: background 0.15s ease, color 0.15s ease;
}
.move-order-btn + .move-order-btn { border-top: 1px solid rgba(255, 255, 255, 0.14); }
.move-order-btn:hover:not(:disabled) { background: color-mix(in srgb, var(--move-color) 38%, rgba(255, 255, 255, 0.08)); color: #fff; }
.move-order-btn:focus-visible { outline: 2px solid var(--accent, #d64545); outline-offset: -2px; }
.move-order-btn:disabled { opacity: 0.3; cursor: default; background: rgba(255, 255, 255, 0.03); }
.move-order-btn svg { width: 18px; height: 18px; stroke-width: 2.75; }
.move-order-btn.up svg { transform: rotate(180deg); }
/* Saved moves: the same card, Teach / Discard stacked on the right. */
.move-card-actions { display: flex; flex-direction: column; justify-content: center; gap: 6px; padding: 8px 10px; border-left: 1px solid rgba(255, 255, 255, 0.07); }
.move-card-actions .btn { justify-content: center; }
@media (max-width: 480px) {
  .move-card-main { padding: 9px 10px; }
  .move-card-stats { gap: 5px 10px; }
  .move-order-btn { width: 36px; }
}

/* --------------------------------------------------------------------------
   Evolution sequence (js/ui/evolution.js, DECISIONS 131)
   --------------------------------------------------------------------------
   Full-screen, above the battle and the modals. The stage holds a glow disc, turning light
   rays and the two sprites; sparks are absolutely placed spans driven by --x/--y. The text
   box is the white dialogue box of the games with a blinking ▼ once a sentence is done. */
.evo-overlay {
  position: fixed; inset: 0; z-index: 1150;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 22px; padding: 20px;
  background: radial-gradient(circle at 50% 40%, #2a3470 0%, #141a3a 62%, #0a0d20 100%);
  color: #fff;
  animation: evo-in 0.35s ease;
  overflow: hidden;
}
.evo-overlay::before {
  /* the darkening while the light gathers */
  content: ""; position: absolute; inset: 0; background: #04050c;
  opacity: 0; transition: opacity 1.4s ease; pointer-events: none;
}
.evo-overlay.dark::before { opacity: 0.86; }
.evo-overlay.closing { animation: evo-out 0.22s ease forwards; }
@keyframes evo-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes evo-out { to { opacity: 0; } }
body.evo-open { overflow: hidden; }

.evo-stage {
  position: relative; z-index: 1;
  width: min(68vw, 300px); aspect-ratio: 1;
  display: flex; align-items: center; justify-content: center;
}
.evo-glow {
  position: absolute; inset: 8%; border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 244, 200, 0.45) 0%, rgba(255, 216, 77, 0.18) 45%, transparent 70%);
  opacity: 0; transition: opacity 1.2s ease;
}
.evo-rays {
  position: absolute; inset: -48%;
  background: repeating-conic-gradient(rgba(255, 255, 255, 0.15) 0 7deg, transparent 7deg 18deg);
  -webkit-mask: radial-gradient(circle, #000 14%, transparent 62%);
          mask: radial-gradient(circle, #000 14%, transparent 62%);
  opacity: 0; transition: opacity 1.2s ease;
  animation: evo-turn 16s linear infinite;
}
.evo-stage.active .evo-rays, .evo-stage.active .evo-glow { opacity: 1; }
.evo-overlay.done .evo-glow { opacity: 0.7; }
@keyframes evo-turn { to { transform: rotate(360deg); } }
.evo-sprites { position: absolute; inset: 0; }
.evo-stage.active .evo-sprites { animation: evo-breathe 1.1s ease-in-out infinite; }
@keyframes evo-breathe { 50% { transform: scale(1.06); } }
.evo-sprite {
  position: absolute; left: 12%; top: 12%; width: 76%; height: 76%;
  object-fit: contain; image-rendering: pixelated;
  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.45));
  transition: filter 0.9s ease, opacity 0.07s linear, transform 0.07s linear;
}
/* The form tints (.variant-* at the top of this file) lose to the drop shadow above at equal
   specificity, so they are restated for the coloured states: the shared pipeline tint from
   css/base.css, the form's own evolution glow, then the one shadow every sprite has here. */
.evo-sprite.variant-golden { filter: var(--form-golden-tint) var(--form-evo-shadow); }
.evo-sprite.variant-dark   { filter: var(--form-dark-tint) var(--form-evo-shadow); }
.evo-sprite.variant-frozen { filter: var(--form-frozen-tint) var(--form-frozen-evo-glow) var(--form-evo-shadow); }
.evo-sprite.variant-shadow { filter: var(--form-shadow-tint) var(--form-shadow-evo-glow) var(--form-evo-shadow); }   /* DECISIONS 330 */
.evo-sprite.variant-crystal { filter: var(--form-crystal-tint) var(--form-crystal-evo-glow) var(--form-evo-shadow); }   /* task 8 */
.evo-sprite.silhouette { filter: brightness(0) invert(1) drop-shadow(0 0 14px #fff) drop-shadow(0 0 28px rgba(255, 216, 77, 0.8)); }
.evo-sprite.hidden { opacity: 0; transform: scale(0.86); }
/* the dark / golden variant tints stay off while it is a silhouette */
.evo-sprite.silhouette.variant-dark, .evo-sprite.silhouette.variant-golden, .evo-sprite.silhouette.variant-shadow { filter: brightness(0) invert(1) drop-shadow(0 0 14px #fff); }

.evo-spark {
  position: absolute; left: 50%; top: 50%;
  width: 6px; height: 6px; border-radius: 50%;
  background: #fff; box-shadow: 0 0 8px #fff, 0 0 16px #ffd84d;
  animation-duration: 1.3s; animation-fill-mode: both; pointer-events: none;
}
.evo-spark.star { width: auto; height: auto; background: none; box-shadow: none; color: #ffe27a; font-size: 14px; line-height: 1; text-shadow: 0 0 8px #ffd84d; }
.evo-spark.in { animation-name: evo-suck; animation-timing-function: ease-in; }
.evo-spark.out { animation-name: evo-burst; animation-timing-function: ease-out; }
@keyframes evo-suck {
  0%   { transform: translate(calc(-50% + var(--x)), calc(-50% + var(--y))); opacity: 0; }
  25%  { opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(0.3); opacity: 0; }
}
@keyframes evo-burst {
  0%   { transform: translate(-50%, -50%); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--x)), calc(-50% + var(--y))) scale(0); opacity: 0; }
}

.evo-text {
  position: relative; z-index: 1;
  width: min(92vw, 520px); min-height: 84px;
  padding: 14px 44px 14px 18px;
  background: #fbfbf7; color: #2b2f3a;
  border: 4px solid #2b2f3a; border-radius: 10px;
  box-shadow: inset 0 0 0 3px #d6d8de, 0 10px 30px rgba(0, 0, 0, 0.35);
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 1.05rem; line-height: 1.5; font-weight: 600;
}
.evo-cursor { position: absolute; right: 14px; bottom: 10px; font-size: 12px; color: #2b2f3a; opacity: 0; }
.evo-text.idle .evo-cursor { animation: evo-blink 0.9s steps(1) infinite; }
@keyframes evo-blink { 50% { opacity: 1; } }

.evo-skip { position: absolute; top: 14px; right: 14px; z-index: 2; color: rgba(255, 255, 255, 0.75); border-color: rgba(255, 255, 255, 0.35); }
.evo-skip[hidden], .evo-continue[hidden] { display: none; }
.evo-continue { position: relative; z-index: 1; min-width: 180px; animation: evo-pop 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.2); }
@keyframes evo-pop { from { transform: scale(0.6); opacity: 0; } }

.evo-flash { position: fixed; inset: 0; z-index: 3; background: #fff; opacity: 0; pointer-events: none; }
.evo-flash.on { animation: evo-flash 1.3s ease-out; }
@keyframes evo-flash { 15% { opacity: 1; } 100% { opacity: 0; } }

@media (max-width: 600px) {
  .evo-overlay { gap: 16px; padding: 16px; }
  .evo-stage { width: min(62vw, 240px); }
  .evo-text { font-size: 0.95rem; min-height: 76px; }
}
@media (prefers-reduced-motion: reduce) {
  .evo-rays, .evo-stage.active .evo-sprites, .evo-flash.on, .evo-continue { animation: none; }
  .evo-spark { display: none; }
}

/* --------------------------------------------------------------------------
   EXP bar that fills up (docs/PLAN-EXP-BAR.md, DECISIONS 133)
   -------------------------------------------------------------------------- */
/* Level-up beat: the bar glows for a moment ... */
.expbar.ding { box-shadow: 0 0 0 2px rgba(124, 196, 255, 0.55), 0 0 14px rgba(124, 196, 255, 0.9); animation: exp-ding 0.65s ease-out; }
@keyframes exp-ding { 0% { filter: brightness(1.9); } 100% { filter: brightness(1); } }
/* ... and the level in the box pops to its new value. */
.battle-info-level { display: inline-block; }
.battle-info-level.pop { animation: level-pop 0.45s cubic-bezier(0.2, 0.9, 0.3, 1.3); }
@keyframes level-pop { 0% { transform: scale(1.45); color: #ffe27a; } 100% { transform: scale(1); } }
/* End screen: a thin bar under "+N EXP". */
.outcome-gain { display: flex; flex-direction: column; align-items: flex-end; gap: 0; }
.outcome-exp { height: 4px; width: 76px; margin-top: 4px; }
.outcome-exp .expbar-fill { transition: width 0.9s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .expbar.ding, .battle-info-level.pop { animation: none; }
  .outcome-exp .expbar-fill { transition: none; }
}

/* --------------------------------------------------------------------------
   Maximum level: the EXP bar says MAX (owner, 24 September, DECISIONS 305)
   -------------------------------------------------------------------------- */
/* At the level cap the bar is full for good, so it turns gold, says MAX and breathes: a soft gold
   glow that slowly comes and goes, and now and then a light that sweeps over it, the way a maxed
   bar shines in a game. Only opacity and transform move, so the browser composites it for next
   to nothing (DECISIONS 286). expBar(progress, level) in js/ui/pokemon.js builds it; battle.js
   and layout.js put `is-max` on their own bars. */
.expbar.is-max {
  position: relative;
  height: 12px;
  overflow: visible;                      /* the glow reaches past the edge */
  background: #4a3000;
}
.expbar.is-max::before {                  /* the glow, fading in and out */
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 0 10px 2px rgba(255, 196, 60, 0.8);
  opacity: 0.2;
  pointer-events: none;
  animation: exp-max-glow 2.4s ease-in-out infinite;
}
.expbar.is-max .expbar-fill {
  position: relative;
  overflow: hidden;                       /* keeps the sweep inside the bar */
  background: linear-gradient(90deg, #c98a00, #f5b400 28%, #ffe27a 50%, #f5b400 72%, #c98a00);
  box-shadow: inset 0 1px 0 rgba(255, 250, 225, 0.55);
  transition: none;
}
.expbar.is-max .expbar-fill::after {      /* the light sweep */
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 45%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.7), transparent);
  transform: translateX(-110%);
  animation: exp-max-sweep 3.2s ease-in-out infinite;
}
.expbar-max {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-left: 0.2em;                    /* the letter-spacing trails the X */
  font-size: 8.5px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0.2em;
  color: #3d2700;
  text-shadow: 0 1px 0 rgba(255, 244, 200, 0.55);
  pointer-events: none;
}
@keyframes exp-max-glow { 0%, 100% { opacity: 0.2; } 50% { opacity: 0.9; } }
@keyframes exp-max-sweep { 0% { transform: translateX(-110%); } 60%, 100% { transform: translateX(240%); } }
/* Per place: a notch bigger on the summary; over the percentage's column on the team and PC
   cards; the HP bar's height in the battle box, where "MAX" stands next to it; a gold hairline in
   the party lists, where no word fits. */
.pkm-section .expbar.is-max { height: 14px; }
.pkm-section .expbar-max { font-size: 9.5px; }
.team-row .team-bars .expbar.is-max { grid-column: 2 / -1; height: 12px; }
.battle-info-row .expbar.is-max { height: 6px; }
.party-row .party-exp.is-max, .party-slot .expbar.is-max { height: 3px; }
.party-row .party-exp.is-max::before, .party-slot .expbar.is-max::before { box-shadow: 0 0 6px 1px rgba(255, 196, 60, 0.7); }
.battle-info-row .expbar-max, .party-slot .expbar-max { display: none; }
.battle-info-val.is-max, .market-stats .exp-max { color: #ffc53d; font-weight: 800; letter-spacing: 0.06em; }
@media (prefers-reduced-motion: reduce) {
  .expbar.is-max::before { animation: none; opacity: 0.5; }
  .expbar.is-max .expbar-fill::after { animation: none; display: none; }
}

/* --------------------------------------------------------------------------
   Move effects made visible (docs/PLAN-MOVES.md, DECISIONS 137)
   -------------------------------------------------------------------------- */
/* Floating numbers over the sprite stage (.battle-sprite is position:absolute). */
.dmg-pop {
  position: absolute; left: 50%; top: 22%; z-index: 4;
  transform: translate(-50%, 0);
  font-weight: 900; font-size: 17px; line-height: 1;   /* owner: a notch smaller than the first 20 px */
  color: #fff; text-shadow: 0 0 4px rgba(0, 0, 0, 0.7), 0 2px 2px rgba(0, 0, 0, 0.5);
  pointer-events: none;
  animation: dmg-pop-up 1.1s ease-out forwards;
}
.dmg-pop.cause-attack { color: #ffd7d7; }
.dmg-pop.cause-poison { color: #d68be0; }
.dmg-pop.cause-burn { color: #ffab5e; }
.dmg-pop.cause-leech-seed { color: #9de29b; }
.dmg-pop.cause-recoil { color: #cfd4de; font-size: 15px; }
.dmg-pop.cause-confusion { color: #e6b3ff; }   /* "It hurt itself in its confusion!" (DECISIONS 150) */
.dmg-pop.cause-sacrifice { color: #cfd4de; }   /* Memento */
.dmg-pop.gain, .dmg-pop.cause-heal, .dmg-pop.cause-drain { color: #7ef29a; }
@keyframes dmg-pop-up {
  0%   { opacity: 0; transform: translate(-50%, 10px) scale(0.7); }
  15%  { opacity: 1; transform: translate(-50%, 0) scale(1.15); }
  70%  { opacity: 1; transform: translate(-50%, -22px) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -38px) scale(0.95); }
}
/* Stat-stage chips in the info box. */
.stage-chip {
  display: inline-flex; align-items: center;
  padding: 0 5px; border-radius: 4px;
  font-size: 9.5px; font-weight: 800; line-height: 15px;
  background: rgba(255, 255, 255, 0.12); color: var(--text, #fff);
  vertical-align: middle;
}
.stage-chip.up { background: rgba(76, 217, 100, 0.22); color: #9df0ad; }
.stage-chip.down { background: rgba(229, 72, 77, 0.24); color: #ffb0b3; }
.status-tag.seeded { background: #3f9d4f; text-transform: none; }
.status-tag.confused { background: #9b59c9; text-transform: none; }   /* DECISIONS 150 */
.status-tag.drowsy { background: #5c6bc0; text-transform: none; }
/* Frozen form (DECISIONS 149): Iced = next attack lost; frost = guard against a second Iced. */
.status-tag.iced { background: #5fc9f5; color: #06202e; text-transform: none; }
.status-tag.frost { background: #2b6f8f; color: #dff5ff; text-transform: none; }
@media (max-width: 600px) {
  .dmg-pop { font-size: 14px; }
  .stage-chip { font-size: 9px; padding: 0 4px; }
}
@media (prefers-reduced-motion: reduce) {
  .dmg-pop { animation: dmg-pop-still 1s linear forwards; }
}
@keyframes dmg-pop-still { 0%, 80% { opacity: 1; } 100% { opacity: 0; } }

/* --------------------------------------------------------------------------
   Live PvP (DECISIONS 138): turn clock, "has chosen", rematch, toast action
   -------------------------------------------------------------------------- */
.turn-clock {
  display: inline-flex; align-items: center; gap: 8px;
  margin: 6px auto 4px; padding: 3px 10px;
  border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.07);
  font-size: 12px; color: var(--muted, #b8c0d4);
}
.turn-clock[hidden] { display: none; }
.turn-clock-time { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--text, #fff); font-size: 13px; }
.turn-clock.urgent { border-color: rgba(229, 72, 77, 0.6); background: rgba(229, 72, 77, 0.16); color: #ffb0b3; animation: clock-pulse 1s ease-in-out infinite; }
.turn-clock.urgent .turn-clock-time { color: #ff8a8e; }
@keyframes clock-pulse { 50% { box-shadow: 0 0 0 4px rgba(229, 72, 77, 0.18); } }
.battle-waiting + .turn-clock { display: flex; width: max-content; }
.pvp-turn-head { display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; margin-bottom: 4px; }
.pvp-turn-head .turn-clock { margin: 0; }
.pvp-ready { font-size: 12px; color: #9df0ad; font-weight: 600; }
.battle-waiting.ready { color: #9df0ad; }
.toast-action { flex: none; margin-left: 6px; }
@media (prefers-reduced-motion: reduce) { .turn-clock.urgent { animation: none; } }

/* The chat box on the PvP result screen (js/ui/battlechat.js, DECISIONS 206): the same message
   rows as the normal chat panel, but inside an outcome card. It spans the whole grid, because a
   conversation squeezed next to the EXP column reads badly. */
.outcome-card.battle-chat { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 7px; }
.battle-chat .battle-chat-messages {
  display: flex; flex-direction: column; gap: 2px;
  min-height: 54px; max-height: 132px; overflow-y: auto;
  padding: 6px; border-radius: var(--radius-sm, 8px);
  background: rgba(0, 0, 0, 0.22);
  font-size: 0.8rem;
}
.battle-chat .battle-chat-quick { display: flex; flex-wrap: wrap; gap: 6px; }
.battle-chat .chat-form { display: flex; gap: 6px; }
.battle-chat .chat-form input { flex: 1 1 auto; min-width: 0; }

/* --------------------------------------------------------------------------
   Battle Hub dashboard (js/ui/hub.js, DECISIONS 142)
   --------------------------------------------------------------------------
   Every hub rule of this round lives here, not in css/layout.css, which keeps the DECISIONS
   121 base rules (.hub-grid, .hub-stat*, .hub-team*, .hub-next*, .challenge-*). This file is
   loaded after layout.css, so the rules below win wherever they overlap. */

/* Hero band: avatar + name + one status line on the left, the record tiles on the right -
 * the top of the Trainer Card, which the games open with your picture, name and badges.
 * A soft red wash on the left corner marks it as the head of the page. */
.hub-hero {
  display: flex; align-items: center; gap: 18px;
  padding: 14px 18px; margin-bottom: 12px;
  background: linear-gradient(120deg, rgba(214, 69, 69, 0.26), rgba(214, 69, 69, 0.04) 46%, transparent 70%), var(--panel);
}
.hub-hero-id { display: flex; align-items: center; gap: 14px; min-width: 0; flex: 0 1 auto; }
.hub-hero-avatar { flex: none; }
.hub-hero-avatar .avatar.lg { width: 64px; height: 64px; background: rgba(255, 255, 255, 0.1); }
.hub-hero-text { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.hub-hero-name { font-size: 1.25rem; font-weight: 800; line-height: 1.15; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-hero-status { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 0.8rem; color: var(--muted); }
.hub-hero-status .hub-hero-loc { display: inline-flex; align-items: center; gap: 2px; color: var(--text); font-weight: 600; }
.hub-hero-status svg { color: var(--accent-light); }
.hub-hero-sep { opacity: 0.5; }
/* The record tiles sit inside the band: flat tiles instead of the old standalone strip of
 * five small panels (layout.css gives .hub-stat a full panel background and a margin). */
/* The money tile (the 4th) gets more room and shrinks its digits on a narrow band: "12,345,678" ran over
   "Caught" between 900 and 1400 px (MASTER UD-13). */
.hub-hero .hub-stats { flex: 1 1 auto; margin: 0; grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(0, 1.6fr) minmax(0, 1fr); gap: 6px; }
.hub-hero .hub-stat { background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08); padding: 8px 10px; box-shadow: none; min-width: 0; }
.hub-hero .hub-stat-value { font-size: 1.1rem; font-variant-numeric: tabular-nums; }
.hub-hero .hub-stat.money .hub-stat-value { font-size: clamp(0.8rem, 1.3vw, 1.1rem); }

/* Card heads: icon + title on the left, one quiet ghost button or a count on the right. */
.hub-card .panel-head { margin-bottom: 8px; min-height: 28px; }
.hub-card .panel-head .panel-title { margin: 0; }
.hub-count { margin-left: 6px; letter-spacing: 0; font-size: 10px; padding: 0 6px; }
.hub-count[hidden] { display: none; }   /* [hidden] alone loses to .tag's display: inline-flex */
.hub-card .empty { padding: 14px 12px; }

/* Next badge: badge art (dark until earned, layout.css), text, the leader's B2W2 VS strip
 * as a right-aligned banner (same idea as .vs-banner-gym in the gym rows), the button. */
.hub-next { gap: 14px; }
.hub-next-banner {
  flex: none; width: 192px; height: 64px; border-radius: var(--radius-xs, 6px);
  background: linear-gradient(90deg, transparent, var(--accent-soft)); overflow: hidden;
}
.hub-next-banner img { display: block; width: 100%; height: 64px; object-fit: contain; object-position: right center; image-rendering: pixelated; }
.hub-next.locked .hub-next-banner { filter: grayscale(0.6); opacity: 0.8; }
.hub-next .btn { flex: none; }
.hub-next-badge-name { font-weight: 600; }

/* League progress under the next badge: label, the eight chips, the count. It reuses the
 * .badge-chip look of the Trainer Card (layout.css) but pins every size here, so the hub
 * never depends on layout.css changes (the duplicate rule there is gone since the review). */
.hub-league { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08); }
.hub-league-label { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.01em; color: var(--muted); }   /* no caps lock (DECISIONS 127) */
.hub-league-case { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.hub-league .badge-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; border-radius: 50%; gap: 0;
  background: rgba(255, 255, 255, 0.04); border: 1px solid transparent; opacity: 1;
}
.hub-league .badge-chip.earned { background: rgba(214, 69, 69, 0.14); border-color: rgba(214, 69, 69, 0.45); box-shadow: 0 0 8px rgba(214, 69, 69, 0.35); }
.hub-league .badge-chip.locked { border-style: dashed; border-color: rgba(255, 255, 255, 0.16); }
.hub-league .badge-chip-art { width: 26px; height: 26px; object-fit: contain; }
.hub-league .badge-chip.locked .badge-chip-art { filter: grayscale(1) brightness(0.22) contrast(1.5); opacity: 0.95; }
.hub-league-count { margin-left: auto; font-variant-numeric: tabular-nums; }

/* Your team: every row is a <button> that opens the summary. The base look (padding, the
 * fainted tint) comes from layout.css; here the button reset plus hover / focus. */
button.hub-team-row {
  width: 100%; text-align: left; color: inherit; font: inherit; cursor: pointer;
  border: 1px solid transparent;
  transition: background 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s;
}
button.hub-team-row:hover, button.hub-team-row:focus-visible { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.2); transform: translateX(2px); outline: none; }
button.hub-team-row:focus-visible { box-shadow: var(--focus); }
button.hub-team-row.fainted:hover, button.hub-team-row.fainted:focus-visible { background: rgba(229, 72, 77, 0.2); border-color: rgba(229, 72, 77, 0.45); }
.hub-team-row .hub-team-go { flex: none; display: inline-flex; color: var(--muted); opacity: 0.55; transition: opacity 0.15s, color 0.15s; }
.hub-team-row:hover .hub-team-go, .hub-team-row:focus-visible .hub-team-go { opacity: 1; color: var(--accent-light); }
.hub-team-row .hpbar { margin-top: 3px; height: 6px; }
.hub-team-row .hub-team-hp { font-variant-numeric: tabular-nums; }

/* Challenges: a playable feature (FEATURES[id].ready) shows an Open button where the others
 * carry the "soon" tag; the row itself is the button, so the label only looks like one. */
.challenge-row.ready .challenge-icon { color: var(--success); background: rgba(76, 217, 100, 0.16); }
.challenge-row.ready:hover .challenge-icon { color: #b8f5c2; background: rgba(76, 217, 100, 0.26); }
.challenge-row .challenge-open { pointer-events: none; flex: none; }
.challenge-row:not(.ready) .challenge-title { color: var(--muted); }

/* Responsive: the grid itself (2 columns above 1100 px, 1 below) lives in layout.css. The
 * hero band stacks the tiles under the name on a laptop and folds to 3 / 2 tiles a row on a
 * phone; the next-badge row wraps so the button gets its own line. */
@media (max-width: 900px) {
  .hub-hero { flex-direction: column; align-items: stretch; gap: 12px; }
}
@media (max-width: 700px) {
  .hub-hero { padding: 12px 14px; }
  .hub-hero .hub-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .hub-next { flex-wrap: wrap; }
  .hub-next-banner { width: 128px; }
  .hub-next .btn { width: 100%; }
  .hub-league-count { margin-left: 0; }
}
@media (max-width: 480px) {
  .hub-hero .hub-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .hub-hero-avatar .avatar.lg { width: 52px; height: 52px; }
  .hub-hero-name { font-size: 1.1rem; }
}

/* ---------------------------------------------------------------------------
 * Watching a battle from the stands (DECISIONS 257, js/ui/battle.js view "watch")
 * The pill and the cheers show for the trainers too; the watch bar only for a watcher.
 * ------------------------------------------------------------------------- */
/* "3 watching": top-left of the arena on a desktop (the foe's box is top-right, yours
   bottom-left); on a phone the boxes swap corners, so it goes top-right there. */
.watch-pill {
  position: absolute; top: 10px; left: 10px; z-index: 4;
  display: inline-flex; flex-wrap: wrap; align-items: center; gap: 2px 6px;
  max-width: min(260px, 42%);
  padding: 4px 10px; border-radius: 999px;
  background: rgba(14, 21, 38, 0.86); border: 1px solid rgba(255, 255, 255, 0.16);
  color: var(--text, #eef2fb); font: inherit; font-size: 0.78rem; font-weight: 700; line-height: 1.25;
  box-shadow: var(--shadow-sm); cursor: pointer; text-align: left;
}
.watch-pill .watch-eye, .watch-head .watch-eye { display: inline-flex; color: var(--accent-light, #8ab4ff); }
/* The names: on hover (mouse) or after a tap (phone), under the count. */
.watch-pill-names { display: none; flex-basis: 100%; font-weight: 500; color: var(--muted, #b8c0d4); white-space: normal; }
.watch-pill.open { border-radius: 12px; }
.watch-pill.open .watch-pill-names { display: block; }
@media (hover: hover) {
  .watch-pill:hover { border-radius: 12px; }
  .watch-pill:hover .watch-pill-names { display: block; }
}
/* The Speed Tower chip sits top-centre: the pill steps under it. */
.battle.has-tower .watch-pill { top: 42px; }

/* A cheer: the emoji and who sent it, rising from the middle of the field and fading. */
.cheer-float {
  position: absolute; bottom: 20%; z-index: 6; pointer-events: none;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  transform: translateX(-50%);
  animation: cheer-rise 1.9s ease-out forwards;
}
.cheer-float-emoji { font-size: 32px; line-height: 1; filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45)); }
/* "Berk says" under the emoji. */
.cheer-float-name {
  max-width: 170px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 2px 8px; border-radius: 999px;
  background: rgba(14, 21, 38, 0.85); color: #fff; font-size: 0.74rem; font-weight: 700;
}
@keyframes cheer-rise {
  0% { opacity: 0; transform: translate(-50%, 14px) scale(0.6); }
  14% { opacity: 1; transform: translate(-50%, 0) scale(1.12); }
  28% { transform: translate(-50%, -14px) scale(1); }
  80% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -120px) scale(1); }
}
@keyframes cheer-fade { 0% { opacity: 0; } 15% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .cheer-float { animation-name: cheer-fade; }
}

/* The watcher's action zone: "Watching Berk vs Brock", what happens now, four cheers, Leave. */
.watch-bar { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; }
.watch-head { display: flex; align-items: center; gap: 8px; font-size: 0.95rem; min-width: 0; }
.watch-title { min-width: 0; overflow-wrap: anywhere; }
.watch-bar .battle-waiting { padding: 2px 0; justify-content: flex-start; font-size: 0.9rem; }
.cheer-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.cheer-btn { min-height: 46px; padding: 6px; font-size: 1.5rem; line-height: 1; transition: transform 0.12s ease, opacity 0.2s ease; }
.cheer-btn:not(:disabled):hover { transform: translateY(-2px); }
.cheer-btn:not(:disabled):active { transform: scale(0.94); }
.cheer-btn:disabled { opacity: 0.45; cursor: default; }
.watch-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px 12px; flex-wrap: wrap; }
/* The end screen for a watcher: "Berk won!" above Back, in the usual banner colours. */
.watch-outcome { flex-wrap: wrap; }
.watch-outcome-note { flex-basis: 100%; margin-bottom: 6px; text-align: center; font-size: 1.05rem; font-weight: 800; }
/* The Watch button on the trainer card, after "In a battle right now". */
.profile-online .btn-watch { margin-left: 8px; vertical-align: middle; }

@media (max-width: 600px) {
  .watch-pill { left: auto; right: 8px; top: 8px; max-width: 52%; font-size: 0.74rem; padding: 3px 9px; }
  .watch-bar { padding: 8px 10px; gap: 6px; }
  .cheer-btn { min-height: 44px; font-size: 1.35rem; }
  .watch-foot .btn { flex: 1 0 100%; min-height: 40px; }
}

/* EXP Boost (DECISIONS 387): the charm card in the Bag glows gold while the boost runs and says how long;
   the "x2" chip sits next to EXP in the battle info box for as long as it runs. */
.item-card.boost .item-card-art { background: radial-gradient(circle at 35% 30%, rgba(255, 244, 194, 0.35), rgba(255, 210, 74, 0.18) 60%, rgba(255, 255, 255, 0.06)); }
.item-card.boost.active { border-color: rgba(255, 210, 74, 0.55); box-shadow: 0 0 0 1px rgba(255, 210, 74, 0.25), 0 0 18px rgba(255, 210, 74, 0.18); }
.item-boost-active { margin-top: 6px; font-size: 0.74rem; font-weight: 800; color: #ffd24a; }
.battle-boost-pill {
  position: absolute; top: -10px; right: 8px; z-index: 1;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 7px 1px 4px; border-radius: 999px;
  background: linear-gradient(180deg, #ffe08a, #f6c24a); color: #3b2400;
  border: 1px solid #8a5a12; box-shadow: 0 0 0 2px rgba(14, 20, 36, 0.9), 0 0 10px rgba(255, 210, 74, 0.45);
  font-size: 0.62rem; font-weight: 900; letter-spacing: 0.02em; line-height: 1.5; white-space: nowrap;
}
.battle-boost-pill img { width: 12px; height: 12px; }

/* A party member that sits a level-rule battle out (Defeat & Own cap, Dungeon cap, Prestige range,
   DECISIONS 397): greyed in its own place in the strip, with the reason as a small tag. */
.party-slot.benched { opacity: 0.5; filter: grayscale(0.6); cursor: default; }
.party-slot-out { display: inline-block; margin-top: 3px; padding: 0 6px; border-radius: 999px; background: rgba(255, 255, 255, 0.12); color: var(--muted); font-size: 0.62rem; font-weight: 700; white-space: nowrap; }
.benched-list { margin: 0 0 10px; padding-left: 18px; line-height: 1.5; }   /* the "Not everyone can join" question (DECISIONS 397) */

/* A second chance at a rare form (DECISIONS 430, js/ui/chase.js): the offer's Pokémon with its form chip. */
.chase { text-align: center; }
.chase p { margin: 6px 0; }
.chase-art { display: flex; flex-direction: column; align-items: center; gap: 4px; margin: 2px 0 10px; }
.chase-art img { height: 96px; width: auto; max-width: 100%; filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.35)); }
.chase-lead { font-weight: 600; }
.chase-ask { font-weight: 800; font-size: 1.05rem; }

/* ------------------------------------------------------------------------- */
/* The battle on a phone: the owner's pick of 27 September (DECISIONS 462,   */
/* docs/PLAN-MOBIEL-GEVECHT.md). In the fight "A · Opgeruimd"                */
/* (docs/img/mobiel/1a-alt-opgeruimd.png) with two changes of the owner: the  */
/* party row shows small sprites with level, HP and EXP, and Pokémon opens    */
/* design B's bottom sheet (1b, third phone) with EXP and the form added. At  */
/* the end "A · Opgeknapt" (2a) without the trophy and with a lighter         */
/* Continue. Only the game's own battle (.view-scroll > .battle): the wiki's  */
/* static battle picture keeps its look. A desktop never gets these rules.    */
/* ------------------------------------------------------------------------- */
@media (max-width: 600px) {
  /* A1. The arena takes the room that is left (at least 220 px); the part below keeps its natural
     height, so nothing scrolls on a normal phone. A short screen shrinks the arena first, then the
     part below scrolls as before. */
  .view-scroll > .battle .battle-arena { flex: 1 1 0; height: auto; min-height: 220px; }
  .view-scroll > .battle .battle-below { flex: 0 1 auto; gap: 6px; padding-top: 6px; }
  /* The damage number over the foe starts above its head, not up under the foe's status box. */
  .view-scroll > .battle .battle-sprite.opp .dmg-pop { top: 38%; }
  /* A2. The text right under the arena (it was last, under the party, often below the fold): two
     lines, the newest on top. Opened as the history it gets more room. */
  .view-scroll > .battle .battle-below .battle-msg { order: 0; height: 64px; max-height: 64px; }
  .view-scroll > .battle .battle-below .battle-msg.expanded { height: 180px; max-height: 180px; }
  .view-scroll > .battle .battle-below .party-strip { order: 1; }
  .view-scroll > .battle .battle-below .battle-actions { order: 2; }

  /* A3 + owner: the party in one row of six - a small version of each Pokémon's own sprite (not the
     menu icon), its level, and an HP and an EXP bar. A tap still sends that Pokémon out. */
  .view-scroll > .battle .party-strip { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 5px; padding: 6px; }
  .view-scroll > .battle .party-slot { flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 2px; min-height: 0; padding: 3px 4px 5px; }
  .view-scroll > .battle .party-slot > .sprite-wrap { align-self: center; }
  /* The sprites are cut tight and differ in size (36 to 110 px): each is fitted into the same box. */
  .view-scroll > .battle .party-slot > .sprite-wrap img { width: 44px; height: 34px; object-fit: contain; object-position: 50% 100%; }
  .view-scroll > .battle .party-slot-num, .view-scroll > .battle .party-slot-name-text, .view-scroll > .battle .party-slot-out { display: none; }
  .view-scroll > .battle .party-slot-body { flex: none; width: 100%; }
  .view-scroll > .battle .party-slot-name { justify-content: center; gap: 3px; }
  .view-scroll > .battle .party-slot-lv { margin-left: 0; font-size: 10px; color: var(--text, #fff); }
  .view-scroll > .battle .party-slot-name .status-tag { font-size: 8px; padding: 0 3px; }
  .view-scroll > .battle .party-slot .hpbar { height: 4px; margin-top: 2px; }
  .view-scroll > .battle .party-slot .expbar { height: 3px; margin-top: 2px; }
  .view-scroll > .battle .party-slot.empty { align-items: center; justify-content: center; }
  /* The loading placeholder has the sprite's size, so the row does not grow when the data lands. */
  .view-scroll > .battle .party-slot .battle-skel-icon { width: 44px; height: 34px; }
  .view-scroll > .battle .party-slot-outchip { align-self: center; margin-top: 3px; padding: 0 5px; border-radius: 999px; background: rgba(255, 255, 255, 0.12); color: var(--muted, #b8c0d4); font-size: 9px; font-weight: 700; white-space: nowrap; }
  /* The Bag drawer scrolls inside the zone instead of growing it (the arena shrank by 70 px). */
  .view-scroll > .battle .battle-actions:not(.as-sheet) .battle-sub { max-height: 158px; }

  /* A4. Bigger move cards; the key numbers 1-4 go (no keyboard on a phone), so the text gets their room. */
  /* minmax(0, 1fr): with a plain 1fr a long meta line pushed the right card 15 px past a 360 px screen. */
  .view-scroll > .battle .move-bar { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; grid-auto-rows: 1fr; }
  .view-scroll > .battle .move-btn { min-height: 64px; justify-content: center; gap: 5px; padding: 9px 11px; }
  .view-scroll > .battle .move-btn-name { font-size: 15.5px; }
  .view-scroll > .battle .move-key { display: none; }
  /* The PP must never be cut (review, 27 September): one line when it fits, else the PP drops under it. */
  .view-scroll > .battle .move-btn-meta { flex-wrap: wrap; gap: 2px 6px; min-width: 0; max-width: 100%; font-size: 11px; white-space: nowrap; }
  /* 11 px text and a slimmer type badge: badge + "90 Pow" + "20/20 PP" fit one line in a 390 px card. */
  .view-scroll > .battle .move-btn-meta .type-badge { padding: 1px 6px; font-size: 9.5px; }
  /* A5. Bag / Pokémon / Run 44 px high (they were 28 px, under the 44 px a thumb needs). */
  .view-scroll > .battle .battle-secondary .btn { min-height: 44px; font-size: 14px; }
  /* One height for the moves and the drawers, so opening one never resizes the arena above it. */
  .view-scroll > .battle:not(.watching) .battle-actions { display: flex; flex-direction: column; min-height: 246px; }
  .view-scroll > .battle:not(.watching) .battle-actions > div,
  .view-scroll > .battle:not(.watching) .battle-menu,
  .view-scroll > .battle:not(.watching) .move-bar { flex: 1 1 auto; }
  .view-scroll > .battle .battle-menu { gap: 8px; }
  .view-scroll > .battle .battle-actions-title .btn { min-height: 44px; padding: 4px 16px; margin: -4px -6px -4px 0; }
  /* A6. The status boxes never wrap: one top line, the name gives way first (it has the ellipsis). */
  .view-scroll > .battle .battle-info-top { flex-wrap: nowrap; }
  .view-scroll > .battle .battle-info-extras { flex: none; }

  /* Pokémon (owner: design B's switch): the party slides up as a sheet over the lower part of the
     screen, the rest dims; X, a tap on the dimmed arena or Esc closes it. The action zone keeps its
     place under it (js: setSheet), so nothing behind the sheet moves. */
  .view-scroll > .battle .battle-actions.as-sheet > div {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
    max-height: calc(100dvh - var(--header-h, 52px) - 24px); overflow-y: auto; overscroll-behavior: contain;
    gap: 10px; padding: 20px 14px calc(16px + env(safe-area-inset-bottom, 0px));
    border-radius: 22px 22px 0 0; border-top: 1px solid rgba(255, 255, 255, 0.14);
    background: #161e33; box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.55);
    animation: sheet-up 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  .view-scroll > .battle .battle-actions.as-sheet > div::before {
    content: ""; position: absolute; top: 7px; left: 50%; width: 40px; height: 4px; margin-left: -20px;
    border-radius: 2px; background: rgba(255, 255, 255, 0.28);
  }
  .battle-sheet-shade {
    position: fixed; left: 0; right: 0; bottom: 0; top: var(--header-h, 52px); z-index: 49;
    background: rgba(6, 10, 20, 0.55); animation: fade-in 0.15s ease;
  }
  /* While a sheet is up the part under the arena is no scroll box: iPhone Safari drew the shade over a
     fixed sheet inside an overflow:auto box, so after a faint nothing could be sent out (js: setSheet,
     DECISIONS 490). */
  .view-scroll > .battle.sheet-open .battle-below { overflow: visible; }
  .view-scroll > .battle .sheet-head { align-items: flex-start; }
  .view-scroll > .battle .sheet-head strong { font-size: 17px; }
  .view-scroll > .battle .sheet-head .muted { display: block; margin-top: 1px; font-size: 12px; font-weight: 400; }
  .view-scroll > .battle .battle-actions-title .sheet-close {
    flex: none; width: 44px; height: 44px; min-height: 44px; padding: 0; margin: -4px -2px 0 0;
    border-radius: 50%; background: rgba(255, 255, 255, 0.08); color: var(--text, #fff);
  }
  .view-scroll > .battle .sheet-close svg { width: 20px; height: 20px; }
  /* The party cards (the sheet, the forced "next Pokémon" and a medicine's target): six cards 2 x 3,
     full name, level, HP bar, EXP bar (owner), numbers and why one cannot come in; the form as a plate
     under the icon (owner: "of het golden, shadow, shiny is") and the Exp. Share beside the name. */
  .view-scroll > .battle .party-grid { gap: 8px; }
  .view-scroll > .battle .party-card { min-height: 66px; padding: 8px 10px; gap: 10px; border-radius: 12px; }
  /* A fixed icon column, so names and bars line up from card to card whatever the form plate says. */
  .view-scroll > .battle .party-card-art { position: relative; flex: none; width: 48px; display: flex; flex-direction: column; align-items: center; }
  .view-scroll > .battle .party-card .sprite-wrap.size-sm img { width: 40px; height: 40px; }
  /* The 48 px icon column holds the word but not the icon too: no icon here, the padding of a chip without one (DECISIONS 473). */
  .view-scroll > .battle .party-card-art .variant-label { position: relative; z-index: 1; margin-top: -5px; padding: 0 calc(1em - 1px); font-size: 8px; }
  .view-scroll > .battle .party-card-art .variant-label::before { display: none; }
  /* The full name (owner: "full names"): the level drops under it when both do not fit on one line. */
  .view-scroll > .battle .party-card-name { flex-wrap: wrap; row-gap: 0; font-size: 14px; white-space: normal; }
  .view-scroll > .battle .party-card-name-text { max-width: 100%; white-space: nowrap; }
  .view-scroll > .battle .party-card-hp { flex-wrap: wrap; align-items: center; gap: 0 6px; }
  .view-scroll > .battle .party-card-hp .status-tag { font-size: 9px; padding: 0 4px; }
  .view-scroll > .battle .party-card-why { margin-left: auto; }
  /* A medicine's target list as a sheet: no 240 px box of its own inside it. */
  .view-scroll > .battle .as-sheet .battle-sub { max-height: none; overflow: visible; }
  /* The party row: a status tag (FNT, PAR) sits on the sprite's corner, so "Lv. 15" is never cut. */
  .view-scroll > .battle .party-slot { position: relative; }
  .view-scroll > .battle .party-slot-name .status-tag { position: absolute; top: 3px; left: 3px; z-index: 1; }
  /* The Exp. Share sits on the icon's corner, so a name like "Caterpie Lv. 2" keeps its room. */
  .view-scroll > .battle .party-card-art .party-held { position: absolute; top: -5px; left: -7px; z-index: 1; width: 16px; height: 16px; margin: 0; }
  .view-scroll > .battle .party-card .hpbar { height: 7px; margin-top: 4px; }
  .view-scroll > .battle .party-card .expbar { height: 3px; margin-top: 3px; }
  .view-scroll > .battle .party-card .expbar-max { display: none; }
  .view-scroll > .battle .party-card-hp { font-size: 11.5px; }

  /* The end screen keeps today's order - the result on top, the text last - and the arena's 240 px. */
  .view-scroll > .battle.finished .battle-arena { flex: none; height: var(--arena-h, 240px); }
  /* No padding on top of the scroller: the sticky banner sits flush under the arena, with nothing
     showing through above it; the gap is the panel's own margin. */
  .view-scroll > .battle.finished .battle-below { flex: 1 1 auto; padding-top: 0; }
  .view-scroll > .battle.finished .battle-below > .battle-outcome { margin-top: 6px; }
  .view-scroll > .battle.finished .battle-below .battle-msg { order: 3; height: 132px; max-height: 132px; }
  .view-scroll > .battle .battle-outcome:has(> .outcome-banner.ob) { padding: 0 0 8px; }
}
/* On a tall phone the arena has the room, and the Pokémon stand a size bigger there (the phone's
   1.2 / 1.1 were for 240 px); a short screen (iPhone Safari: 390 x 664 visible) keeps them. */
@media (max-width: 600px) and (min-height: 760px) {
  .view-scroll > .battle:not(.finished) .battle-sprite .sprite-wrap { transform: scale(calc(1.45 * var(--grow, 1))); }
  .view-scroll > .battle:not(.finished) .battle-sprite.opp .sprite-wrap { transform: scale(calc(1.3 * var(--grow, 1))); }
  .view-scroll > .battle:not(.finished) .battle-platform.opp img { width: 190px; margin-bottom: -4px; }
  .view-scroll > .battle:not(.finished) .battle-platform.you img { width: 270px; margin-bottom: -7px; }
}
/* A short screen (iPhone Safari shows 390 x 664): the moves keep their own 64 px instead of filling a
   246 px zone, so Bag / Pokémon / Run still fit under the 220 px arena without scrolling. */
@media (max-width: 600px) and (max-height: 759px) {
  .view-scroll > .battle:not(.watching) .battle-actions { min-height: 0; }
}
.battle.finished .battle-sheet-shade { display: none; }
/* The name in a party card gives way first, so the level stays in view. */
.party-card-name-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* The end screen on a phone, "A · Opgeknapt" (js/ui/battle.js outcomeHead / phoneExpCard). The markup only
   exists there, so these rules need no media query. The banner sticks to the top of the scroller, so
   Continue never scrolls away (owner: the button you press sits on top). */
.outcome-banner.ob {
  position: sticky; top: 0; z-index: 3;
  display: flex; flex-direction: column; align-items: stretch; gap: 8px;
  margin: 0 0 8px; padding: 9px 10px 10px;
  border-radius: 12px 12px 10px 10px; box-shadow: 0 6px 14px rgba(8, 12, 24, 0.35);
}
/* Opaque versions of the result colours: a sticky banner must hide what scrolls under it. */
.outcome-banner.ob.win    { background: linear-gradient(90deg, #22573b 0%, #2f3f47 55%, #5c2b35 100%); }
.outcome-banner.ob.caught { background: linear-gradient(90deg, #5c2b35, #6b3b52); }
.outcome-banner.ob.lose   { background: #4a2330; }
.outcome-banner.ob.draw   { background: #2a3348; }
.outcome-banner.ob.escape { background: #1f3456; }
.ob-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ob-text { flex: 1; min-width: 0; }
.ob-title { font-size: 16px; font-weight: 800; line-height: 1.15; }
.ob-sub { font-size: 11.5px; color: rgba(255, 255, 255, 0.72); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Owner: Continue not so bold - the normal button weight, a little smaller than the mock-up's 17 px / 800. */
.outcome-banner.ob .btn { flex: none; width: 100%; max-width: none; min-width: 0; min-height: 46px; font-size: 15px; font-weight: 600; }
/* A catch after a won raid or the Pokémon of the Day comes first, big and gold, Continue under it. */
.outcome-banner.ob .btn-daily-catch.gold { min-height: 50px; font-size: 16px; font-weight: 700; background: linear-gradient(180deg, #f6c945, #d9a520); border-color: #f6c945; color: #2b2100; }
/* A reward chip: the prize money in gold, money lost in red. */
.rw-chip {
  flex: none; display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px;
  font-size: 11.5px; font-weight: 800; line-height: 1.5; white-space: nowrap;
  background: rgba(0, 0, 0, 0.28); border: 1px solid rgba(255, 255, 255, 0.2); color: #fff;
}
.rw-chip.money { color: #ffe38a; background: rgba(246, 201, 69, 0.16); border-color: rgba(246, 201, 69, 0.55); }
.rw-chip.money.lost { color: #ffb3b3; background: rgba(255, 107, 107, 0.16); border-color: rgba(255, 107, 107, 0.5); }
/* The trainer's last line ("Not bad at all..."), as the games show it before the prize money. */
.ob-quote { display: flex; align-items: flex-end; gap: 6px; margin-top: -2px; }
.ob-quote img { flex: none; width: 34px; height: 38px; object-fit: cover; object-position: top; image-rendering: pixelated; }
.ob-quote p {
  margin: 0; padding: 5px 9px; border-radius: 10px 10px 10px 2px;
  background: #fbfbf7; color: #2b2f3a; font-size: 12px; line-height: 1.35; font-weight: 600;
}
/* The exp list: one line per Pokémon that only gained exp, a lit-up block for one that grew. */
.outcome-card.xp-card { padding: 7px 10px 4px; }
.xp-card h4 { display: flex; align-items: center; gap: 6px; margin: 0 0 2px; }
.xp-card h4 .xp-share { margin-left: auto; display: inline-flex; align-items: center; gap: 3px; font-size: 11px; font-weight: 600; color: var(--muted, #a9b3c7); }
.xp-card .held { width: 18px; height: 18px; image-rendering: pixelated; vertical-align: middle; }
.xp-row {
  display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; column-gap: 8px; row-gap: 5px;
  padding: 5px 0; border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.xp-row:first-of-type { border-top: 0; }
.xp-row > .sprite-wrap, .xp-row > .party-ball { justify-self: center; }
.xp-row > .sprite-wrap img { width: 32px; height: 32px; }
.xp-main { min-width: 0; }
.xp-name { display: flex; align-items: center; gap: 6px; min-width: 0; font-size: 13.5px; line-height: 1.2; }
.xp-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xp-main .outcome-exp { width: auto; height: 5px; margin-top: 5px; }
.xp-gain { color: #7cc4ff; font-weight: 800; font-size: 13px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.xp-gain small { font-size: 10px; font-weight: 700; opacity: 0.8; }
.xp-row > .st-row, .xp-row > .learn-card, .xp-row > .evo-line, .xp-row > .xp-note { grid-column: 2 / -1; }
.xp-row.up {
  margin: 3px -6px; padding: 7px 6px; border: 1px solid rgba(255, 210, 74, 0.35); border-radius: 10px;
  background: linear-gradient(180deg, rgba(255, 210, 74, 0.10), rgba(255, 210, 74, 0.03));
}
.xp-row.up + .xp-row { border-top: 0; }
.xp-row.up .outcome-exp { box-shadow: 0 0 0 1px rgba(124, 196, 255, 0.5), 0 0 8px rgba(124, 196, 255, 0.6); }
/* The level pill: "Lv. 15 → 16", "+4" when it grew more than one. */
.xp-lv { display: inline-flex; align-items: center; gap: 3px; flex: none; font-size: 11.5px; font-weight: 700; color: var(--muted, #a9b3c7); white-space: nowrap; }
.xp-lv.up { padding: 0 7px; border-radius: 999px; color: #2b2100; background: linear-gradient(180deg, #ffe27a, #f6c945); font-weight: 800; }
.xp-lv.up i { font-style: normal; opacity: 0.7; }
.xp-lv .plus { margin-left: 2px; font-weight: 700; opacity: 0.75; }
/* The stats that grew, as small chips: "HP +2", "Atk +2" ... (the games' level-up window). */
.st-row { display: flex; flex-wrap: wrap; gap: 4px; }
.st { display: inline-flex; align-items: baseline; gap: 3px; padding: 1px 5px; border-radius: 5px; font-size: 11px; background: rgba(255, 255, 255, 0.07); color: #d5dbe8; white-space: nowrap; }
.st small { font-size: 10px; font-weight: 700; color: var(--muted, #a9b3c7); }
.st b { color: #9df0a6; font-weight: 800; }
.st.neg b { color: #ff9a9a; }
/* A 360 px phone: the six chips of a level-up stay on one line. */
@media (max-width: 379px) { .st-row { gap: 3px; } .st { padding: 1px 4px; } }
/* "Evolved into Metapod!" in the purple of the evolution scene. */
.rw-chip.evo { color: #d9c8ff; background: rgba(150, 110, 255, 0.18); border-color: rgba(150, 110, 255, 0.55); }
/* Only the banner (a run, a draw): no empty strip of panel under it. */
.battle-outcome:has(> .outcome-banner.ob:last-child) { padding-bottom: 0; }
.battle-outcome > .outcome-banner.ob:last-child { margin-bottom: 0; }
.xp-note { font-size: 12px; font-weight: 600; line-height: 1.3; color: #9df0a6; }
.xp-note.wait { font-weight: 500; color: var(--muted, #a9b3c7); }
.evo-line { display: flex; align-items: center; gap: 8px; font-size: 12.5px; }
.evo-line b { color: #d9c8ff; }
/* The new move as a card in its row, instead of the pop-up over the list. */
.learn-card {
  --move-color: #8a8a8a;
  padding: 7px 8px 8px; border-radius: 9px;
  border: 1px solid color-mix(in srgb, var(--move-color) 55%, transparent);
  background: linear-gradient(180deg, color-mix(in srgb, var(--move-color) 20%, rgba(18, 26, 44, 0.9)), rgba(18, 26, 44, 0.9));
}
.learn-card.busy { opacity: 0.6; pointer-events: none; }
.learn-head { font-size: 12.5px; font-weight: 700; line-height: 1.3; }
.learn-move { display: flex; align-items: center; gap: 6px; min-width: 0; }
.learn-move:not(:empty) { margin-top: 5px; }
.learn-move .type-badge { font-size: 9.5px; padding: 1px 7px; }
.learn-move .move-cat { margin-left: 0; font-size: 8.5px; padding: 1px 6px; }
.learn-meta { font-size: 11px; color: var(--muted, #a9b3c7); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.learn-actions { display: flex; gap: 6px; margin-top: 7px; }
.learn-actions .btn { flex: 1 1 0; min-height: 40px; font-size: 13px; }
.learn-actions .btn.grow2 { flex-grow: 1.7; }

/* The TM shelf's type chips and "Only for my party" (DECISIONS 532, player idea #45). The chips are the PC
   filter's .pc-chip (css/pc-boxes.css) so players know them; the count sits after the badge. */
.shop-tms .tm-types { display: flex; align-items: center; gap: 8px 16px; flex-wrap: wrap; margin: 10px 0 8px; }
.shop-tms .tm-types .pc-chips { flex: 1 1 420px; }
.tm-chip-n { margin-left: 3px; font-size: 0.66rem; color: var(--muted, #b8c0d4); font-variant-numeric: tabular-nums; }
.tm-mine { display: inline-flex; align-items: center; gap: 6px; font-size: 0.8rem; white-space: nowrap; cursor: pointer; }
.shop-tms .tm-count { margin-top: 8px; }
.shop-tms .pc-chip.type { padding-right: 6px; }
.shop-tms .tm-count[hidden] { display: none; }

/* The favourite heart top-right on a summary (DECISIONS 532, player idea #30): grey = not a favourite, pink = one.
   Pink is nowhere else in the game's accents, so the heart reads the same on the summary and the PC square. */
.pkm-hero { position: relative; }
.pkm-fav {
  position: absolute; top: 10px; right: 10px; z-index: 1;
  width: 34px; height: 34px; padding: 0; border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.06); color: #a9b3c7;
  font-size: 18px; line-height: 1; cursor: pointer;
}
.pkm-fav:hover:not(:disabled) { color: #ff7aa8; border-color: rgba(255, 122, 168, 0.5); }
.pkm-fav.on { color: #ff7aa8; border-color: rgba(255, 122, 168, 0.6); background: rgba(255, 122, 168, 0.14); }
.pkm-fav:disabled { opacity: 0.6; cursor: wait; }
.pc-tile .pc-mini-fav { font-size: 0.8em; }

/* Toss (DECISIONS 532, player idea #41): a small bin after the card's action; after a tag it goes to the right. */
.item-card-foot .item-toss { margin-left: 0; min-width: 30px; padding: 3px 7px; color: var(--muted, #b8c0d4); }
.item-card-foot .item-toss:hover:not(:disabled) { color: var(--danger, #e5484d); }
.item-card-foot .item-tag + .item-toss { margin-left: auto; }
.toss-qty { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.toss-qty .toss-qty-input { width: 76px; text-align: center; }

/* Heal next to Bag in a battle (DECISIONS 532, player idea #25): green like HP, the potion count quiet, the heal
   key as the usual .kbd-hint (hidden on a touch screen). */
.battle-secondary .btn-heal { color: #9df0ab; }
.battle-secondary .btn-heal .btn-label { display: inline-flex; align-items: center; gap: 4px; }
.btn-heal-n { color: var(--muted, #b8c0d4); font-weight: 600; font-size: 0.72rem; }
/* Held for the list (DECISIONS 537): no text selection or callout from the long press on a phone. */
.battle-secondary .btn-heal { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; touch-action: manipulation; }
/* The Heal list: every potion as a button with its count, the one a plain tap uses outlined. */
.heal-picker-list { display: flex; flex-wrap: wrap; gap: 6px; }
.heal-picker-list .btn .btn-label { display: inline-flex; align-items: center; gap: 4px; }
.heal-picker-list .btn.is-last { border-color: rgba(157, 240, 171, 0.7); }

/* The effectiveness label on a move card (DECISIONS 532, player idea #10; Sun/Moon's line under the move): green
   Super effective, grey Not very effective, red No effect. One line, no wrap: it fits the card's free room. */
.move-eff { display: inline-flex; align-items: center; gap: 3px; white-space: nowrap; font-size: 10.5px; font-weight: 800; letter-spacing: 0.01em; line-height: 1.2; }
.move-eff.eff-super { color: #7ee89a; }
.move-eff.eff-weak { color: #c9cfdc; opacity: 0.85; }
.move-eff.eff-none { color: #ff8a8a; }
.move-eff-mark { font-size: 9px; }

/* Mastered (DECISIONS 532, player idea #46): all six forms of a species caught - a gold star top-right on the
   Pokédex tile (top-left: the caught ball sits top-right), a gold chip in its window, the count beside Caught / Seen. */
.dex-tile { position: relative; }
.dex-master { position: absolute; top: 4px; left: 6px; color: #ffd24a; font-size: 13px; line-height: 1; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); }
.dex-master-count { color: #ffd24a; }
.mastery-chip { display: inline-flex; align-items: center; padding: 1px 9px; border-radius: 999px; background: linear-gradient(135deg, #ffe07a, #f6b93b); color: #2a1f00; font-size: 0.72rem; font-weight: 800; }

/* ---- Medals on the Trainer Card (DECISIONS 532, player idea #51; js/ui/medals.js) ------------------------
   A medal is an SVG: ribbon in the group's colour, a metal disc, the game's art in the middle. Not won yet = grey;
   ready to claim = a soft glow and a green edge; won = full colour. Two per row, one on a phone. */
.medals .panel-title { display: flex; align-items: center; gap: 6px; }
.medal-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 0 10px; }
.medal-ready { margin: 0 0 8px; font-size: 0.8rem; font-weight: 700; color: #9df0ab; }
.medal-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.medal { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; background: var(--panel-2); border: 1px solid rgba(255, 255, 255, 0.08); min-width: 0; }
.medal-pic { flex: none; display: inline-flex; }
.medal-art { display: block; overflow: visible; filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45)); }
.medal-art image { image-rendering: pixelated; }
.medal.is-locked .medal-art { filter: grayscale(0.75) brightness(0.88) drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45)); opacity: 0.9; }
.medal.is-claim { border-color: rgba(76, 217, 100, 0.6); background: rgba(76, 217, 100, 0.08); }
.medal.is-claim .medal-art { animation: medalGlow 1.8s ease-in-out infinite; }
@keyframes medalGlow {
  0%, 100% { filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45)); }
  50% { filter: drop-shadow(0 0 7px rgba(255, 224, 122, 0.85)); }
}
@media (prefers-reduced-motion: reduce) { .medal.is-claim .medal-art { animation: none; } }
.medal-body { min-width: 0; flex: 1; }
.medal-name { font-weight: 800; font-size: 0.86rem; }
.medal-what { font-size: 0.72rem; color: var(--muted); line-height: 1.3; }
.medal-bar { height: 5px; margin: 6px 0 4px; border-radius: 3px; background: var(--hp-track); overflow: hidden; }
.medal-bar > span { display: block; height: 100%; background: #f6c945; }
.medal.is-done .medal-bar > span, .medal.is-claim .medal-bar > span { background: var(--success); }
.medal-foot { display: flex; align-items: center; justify-content: space-between; gap: 6px; font-size: 0.7rem; font-variant-numeric: tabular-nums; }
.medal-foot .btn { min-height: 24px; padding: 2px 8px; font-size: 0.72rem; }
.medal-got { color: #9df0ab; font-weight: 700; }
.medal-strip { display: flex; flex-wrap: wrap; gap: 4px; }
.medal-mini { display: inline-flex; }
@media (max-width: 600px) { .medal-grid { grid-template-columns: 1fr; } }
