diff --git a/web/src/components/BattlePhase.tsx b/web/src/components/BattlePhase.tsx index 6b430ab..a645b3c 100644 --- a/web/src/components/BattlePhase.tsx +++ b/web/src/components/BattlePhase.tsx @@ -295,10 +295,10 @@ export function BattlePhase({ view, send, step, setStep }: Props) { // The result dialog pops when the replay ends. "Not yet" dismisses it so the // battle can be rewatched; a toolbar button then remains to advance the round. const [resultDismissed, setResultDismissed] = useState(false) - // The deck-peek popover lives inside .battlefield, which sets overflow-x - // (and thus overflow-y) to auto — so an absolutely-positioned popover gets - // clipped to the battlefield. We anchor it to the hovered stack's viewport - // rect and portal it to so it floats over the whole window instead. + // The deck-peek popover lives inside .battlefield, which scrolls on one axis + // and hides the other — so an absolutely-positioned popover gets clipped to + // the battlefield. We anchor it to the hovered stack's viewport rect and + // portal it to so it floats over the whole window instead. const [peek, setPeek] = useState<{ seat: number; pos: 'top' | 'bottom'; rect: DOMRect } | null>( null, ) diff --git a/web/src/styles.css b/web/src/styles.css index d68ec74..bd14b32 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1990,6 +1990,15 @@ h3 { animation divides its base duration by this, so the visuals quicken in lockstep with the JS step timers. Fallback keeps calc() valid if unset. */ --battle-speed: 1; + /* Arena slot metrics. Every slot in the battlefield — the pet in play and the + deck beneath/above it — is sized to these exactly, whether or not a card + currently occupies it. That keeps the arena a fixed reserved area so its + height never shifts as cards enter and leave play. Phones override them in + the media query below; nothing else should hardcode these sizes. */ + --unit-w: 96px; + --unit-h: 162px; + --deck-w: 84px; + --deck-h: 116px; display: flex; flex-direction: column; gap: 18px; @@ -2067,13 +2076,24 @@ h3 { (right) to fan out beside each pet; a big apple stack (Manatee, Fire Ant) scrolls horizontally rather than clipping. */ padding: 18px 14px; - min-height: 150px; + /* That sideways scroll keeps its scrollbar hidden: on classic-scrollbar + platforms it would appear and vanish as fans grow and shrink, and since the + arena's height is content-driven, that alone made it jump by the + scrollbar's thickness mid-battle. Touch, trackpad and shift+wheel still + scroll it. Vertical is hidden outright — pops that reach past the felt + (spawns, ailment badges) were never scrollable to anyway. */ overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; box-shadow: var(--tray-inset), inset 0 0 60px rgba(0, 0, 0, 0.25); } +.battlefield::-webkit-scrollbar { + display: none; +} + .battle-center { position: relative; font-size: 1.7rem; @@ -2109,10 +2129,12 @@ h3 { min-width: 0; } -/* Centres the deck stack under (you) / over (opponent) the pet. */ +/* Centres the deck stack under (you) / over (opponent) the pet. Its height is + the deck slot's, held whether the deck still has cards or has run dry. */ .battle-deck { display: flex; justify-content: center; + height: var(--deck-h); } /* --- deck stacks --- */ @@ -2125,8 +2147,8 @@ h3 { /* The face-down deck: a walnut-backed card with a woven diamond pattern and a little stacked-depth shadow beneath it. */ .card-back { - width: 84px; - height: 116px; + width: var(--deck-w); + height: var(--deck-h); border-radius: var(--card-radius); border: 2px solid var(--cocoa); background: @@ -2156,9 +2178,11 @@ h3 { box-shadow: inset 0 0 0 2px rgba(255, 230, 200, 0.25); } +/* The empty deck slot holds the face-down card's footprint exactly, so a deck + running dry doesn't resize the arena. */ .stack-empty { - width: 84px; - height: 116px; + width: var(--deck-w); + height: var(--deck-h); } @keyframes summon-pop { @@ -2303,10 +2327,14 @@ h3 { /* --- the pet in play --- */ +/* The pet's slot in the arena: a fixed reserved box the size of a battle card, + held even while it's empty (between a faint and the next reveal) so the + arena's height stays put as cards enter and leave play. Set-aside pets and the + food fan hang off it absolutely, so they never resize it either. */ .battle-unit-zone { position: relative; - min-width: 100px; - min-height: 150px; + width: var(--unit-w); + height: var(--unit-h); display: grid; place-items: center; } @@ -2419,11 +2447,11 @@ h3 { flex-shrink: 0; } +/* Fills its reserved slot. The slot is taller than a shop card so the full + ability text fits without shrinking to an unreadable size during battle. */ .battle-unit .card { - width: 96px; - /* Taller than a shop card so the full ability text fits without shrinking to - an unreadable size during the battle. */ - height: 162px; + width: var(--unit-w); + height: var(--unit-h); } /* Battle cards are short, so give the ability text more of the card by @@ -3205,6 +3233,11 @@ h3 { /* --- battle: fit both sides and the clash between the rails --- */ .battle { gap: 8px; + /* Smaller arena slots; the pet and deck boxes follow automatically. */ + --unit-w: 66px; + --unit-h: 112px; + --deck-w: 56px; + --deck-h: 78px; } .battle-header { gap: 8px; @@ -3227,7 +3260,6 @@ h3 { } .battlefield { padding: 8px 8px; - min-height: 0; gap: 3px; border-radius: 16px; } @@ -3238,14 +3270,6 @@ h3 { .battle-side { gap: 5px; } - .battle-unit .card { - width: 66px; - height: 112px; - } - .battle-unit-zone { - min-width: 72px; - min-height: 112px; - } /* A battle card is small and glanced at, not read — so on a phone strip it to the essentials: big art, the power badge and any damage marker, and the name. No suit dot, tier die, or ability text (the magnified view still shows @@ -3270,10 +3294,6 @@ h3 { .battle-unit .card-art { font-size: 2.1rem; } - .card-back { - width: 56px; - height: 78px; - } .card-back-count { width: 28px; height: 28px;