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 // 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. // battle can be rewatched; a toolbar button then remains to advance the round.
const [resultDismissed, setResultDismissed] = useState(false) const [resultDismissed, setResultDismissed] = useState(false)
// The deck-peek popover lives inside .battlefield, which sets overflow-x // The deck-peek popover lives inside .battlefield, which scrolls on one axis
// (and thus overflow-y) to auto — so an absolutely-positioned popover gets // and hides the other — so an absolutely-positioned popover gets clipped to
// clipped to the battlefield. We anchor it to the hovered stack's viewport // the battlefield. We anchor it to the hovered stack's viewport rect and
// rect and portal it to <body> so it floats over the whole window instead. // portal it to <body> so it floats over the whole window instead.
const [peek, setPeek] = useState<{ seat: number; pos: 'top' | 'bottom'; rect: DOMRect } | null>( const [peek, setPeek] = useState<{ seat: number; pos: 'top' | 'bottom'; rect: DOMRect } | null>(
null, null,
) )
+45 -25
View File
@@ -1990,6 +1990,15 @@ h3 {
animation divides its base duration by this, so the visuals quicken in animation divides its base duration by this, so the visuals quicken in
lockstep with the JS step timers. Fallback keeps calc() valid if unset. */ lockstep with the JS step timers. Fallback keeps calc() valid if unset. */
--battle-speed: 1; --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; display: flex;
flex-direction: column; flex-direction: column;
gap: 18px; gap: 18px;
@@ -2067,13 +2076,24 @@ h3 {
(right) to fan out beside each pet; a big apple stack (Manatee, Fire Ant) (right) to fan out beside each pet; a big apple stack (Manatee, Fire Ant)
scrolls horizontally rather than clipping. */ scrolls horizontally rather than clipping. */
padding: 18px 14px; 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-x: auto;
overflow-y: hidden;
scrollbar-width: none;
box-shadow: box-shadow:
var(--tray-inset), var(--tray-inset),
inset 0 0 60px rgba(0, 0, 0, 0.25); inset 0 0 60px rgba(0, 0, 0, 0.25);
} }
.battlefield::-webkit-scrollbar {
display: none;
}
.battle-center { .battle-center {
position: relative; position: relative;
font-size: 1.7rem; font-size: 1.7rem;
@@ -2109,10 +2129,12 @@ h3 {
min-width: 0; 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 { .battle-deck {
display: flex; display: flex;
justify-content: center; justify-content: center;
height: var(--deck-h);
} }
/* --- deck stacks --- */ /* --- deck stacks --- */
@@ -2125,8 +2147,8 @@ h3 {
/* The face-down deck: a walnut-backed card with a woven diamond pattern and a /* The face-down deck: a walnut-backed card with a woven diamond pattern and a
little stacked-depth shadow beneath it. */ little stacked-depth shadow beneath it. */
.card-back { .card-back {
width: 84px; width: var(--deck-w);
height: 116px; height: var(--deck-h);
border-radius: var(--card-radius); border-radius: var(--card-radius);
border: 2px solid var(--cocoa); border: 2px solid var(--cocoa);
background: background:
@@ -2156,9 +2178,11 @@ h3 {
box-shadow: inset 0 0 0 2px rgba(255, 230, 200, 0.25); 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 { .stack-empty {
width: 84px; width: var(--deck-w);
height: 116px; height: var(--deck-h);
} }
@keyframes summon-pop { @keyframes summon-pop {
@@ -2303,10 +2327,14 @@ h3 {
/* --- the pet in play --- */ /* --- 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 { .battle-unit-zone {
position: relative; position: relative;
min-width: 100px; width: var(--unit-w);
min-height: 150px; height: var(--unit-h);
display: grid; display: grid;
place-items: center; place-items: center;
} }
@@ -2419,11 +2447,11 @@ h3 {
flex-shrink: 0; 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 { .battle-unit .card {
width: 96px; width: var(--unit-w);
/* Taller than a shop card so the full ability text fits without shrinking to height: var(--unit-h);
an unreadable size during the battle. */
height: 162px;
} }
/* Battle cards are short, so give the ability text more of the card by /* 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: fit both sides and the clash between the rails --- */
.battle { .battle {
gap: 8px; 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 { .battle-header {
gap: 8px; gap: 8px;
@@ -3227,7 +3260,6 @@ h3 {
} }
.battlefield { .battlefield {
padding: 8px 8px; padding: 8px 8px;
min-height: 0;
gap: 3px; gap: 3px;
border-radius: 16px; border-radius: 16px;
} }
@@ -3238,14 +3270,6 @@ h3 {
.battle-side { .battle-side {
gap: 5px; 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 /* 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 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 name. No suit dot, tier die, or ability text (the magnified view still shows
@@ -3270,10 +3294,6 @@ h3 {
.battle-unit .card-art { .battle-unit .card-art {
font-size: 2.1rem; font-size: 2.1rem;
} }
.card-back {
width: 56px;
height: 78px;
}
.card-back-count { .card-back-count {
width: 28px; width: 28px;
height: 28px; height: 28px;