Hold the battle arena at a fixed height.

The play area grew and shrank as cards entered and left play. Two slots
were sized independently of the cards that fill them: the pet zone
reserved 150px for a 162px battle card, and the empty-deck placeholder
kept its desktop 84x116 on phones where the face-down card is 56x78. So
each faint/reveal moved the arena 12px per side on desktop, and a deck
running dry moved it 38px per side on a phone.

Every arena slot now derives from one set of --unit-*/--deck-* metrics on
.battle, and holds that size whether or not a card occupies it. Measured
across a battle's states (no pets, both revealed, one fainted, fans of
apples, decks dry), the arena height spread goes from 24px desktop /
76px phone to 0.

Also hide the arena's horizontal scrollbar: a wide apple fan made it
appear and vanish mid-battle, and with a content-driven height that alone
jumped the arena by the scrollbar's thickness. Touch, trackpad and
shift+wheel still scroll it.
This commit is contained in:
Greyson Parrelli
2026-07-27 16:24:52 -04:00
parent b5a143d059
commit a2024229ff
2 changed files with 49 additions and 29 deletions
+4 -4
View File
@@ -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 <body> 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 <body> so it floats over the whole window instead.
const [peek, setPeek] = useState<{ seat: number; pos: 'top' | 'bottom'; rect: DOMRect } | null>(
null,
)
+45 -25
View File
@@ -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;