Move the UI to be vertical.

This commit is contained in:
Greyson Parrelli
2026-07-24 22:16:28 -04:00
parent 0a1e2f9bee
commit 95499026d7
5 changed files with 350 additions and 409 deletions
+216 -172
View File
@@ -531,10 +531,13 @@ h3 {
min-height: 0;
}
/* A centred portrait column — the app is laid out for a tall, narrow play
field, so it stays compact even on a wide desktop (the event log docks
alongside it). */
.table-main {
flex: 1;
padding: 26px 20px 48px;
max-width: 1100px;
padding: 22px 16px 48px;
max-width: 620px;
width: 100%;
margin: 0 auto;
}
@@ -668,24 +671,94 @@ h3 {
box-shadow: inset 0 0 0 1px rgba(246, 201, 78, 0.45);
}
@media (max-width: 860px) {
.table-body {
flex-direction: column;
/* On wide screens the dock wrapper is transparent to layout, so the log docks
as a right column exactly as its own rules describe. */
.event-log-dock {
display: contents;
}
/* The floating log button and drawer backdrop only exist on narrow screens. */
.log-fab,
.log-backdrop {
display: none;
}
/* Narrow screens: the event log becomes a drawer that slides in from the right
over the play field, opened by a floating button. */
@media (max-width: 1000px) {
.event-log-dock {
display: block;
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: min(340px, 88vw);
z-index: 80;
transform: translateX(100%);
transition: transform 240ms ease;
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.5);
}
.event-log {
.event-log-dock.is-open {
transform: translateX(0);
}
.event-log-dock .event-log {
width: 100%;
height: 100%;
max-height: none;
position: static;
max-height: 220px;
border-left: none;
border-top: 3px solid rgba(0, 0, 0, 0.35);
border-left: 1px solid rgba(0, 0, 0, 0.4);
}
.event-log.is-collapsed {
.event-log-dock .event-log.is-collapsed {
width: 100%;
}
.event-log.is-collapsed .event-log-head {
.event-log-dock .event-log.is-collapsed .event-log-head {
writing-mode: horizontal-tb;
height: auto;
}
.log-backdrop {
display: block;
position: fixed;
inset: 0;
z-index: 75;
background: rgba(10, 20, 14, 0.5);
backdrop-filter: blur(1.5px);
animation: result-in 200ms ease;
}
.log-fab {
display: grid;
place-items: center;
position: fixed;
right: 14px;
bottom: 14px;
width: 52px;
height: 52px;
padding: 0;
border-radius: 50%;
z-index: 70;
font-size: 1.4rem;
background: linear-gradient(180deg, var(--wood-light), var(--wood));
color: var(--cream);
border: none;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
0 4px 0 var(--wood-dark),
0 6px 14px rgba(0, 0, 0, 0.45);
}
.log-fab:active {
transform: translateY(3px);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 1px 0 var(--wood-dark),
0 2px 6px rgba(0, 0, 0, 0.4);
}
}
/* ---------- lobby ---------- */
@@ -1567,79 +1640,42 @@ h3 {
gap: 20px;
}
.arrange-row {
/* A vertical battle line laid into a felt tray: card 0 (fights first) sits at
the top and later pets flow downward. */
.arrange-col {
position: relative;
display: flex;
/* Battle line reads right-to-left, wrapping downward: card 0 (fights first)
sits top-right and later pets flow left, then onto rows below. The wide
row-gap leaves a band for the wrap-around connector arrows. */
direction: rtl;
column-gap: 14px;
row-gap: 56px;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
justify-content: flex-start;
gap: 12px;
background:
radial-gradient(ellipse 80% 120% at 50% 0%, rgba(255, 255, 255, 0.04), transparent 70%),
radial-gradient(ellipse 120% 80% at 50% 0%, rgba(255, 255, 255, 0.04), transparent 70%),
rgba(0, 0, 0, 0.24);
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 18px;
/* Extra right padding reserves the strip the "first" marker sits in, so its
width doesn't shove the top row left of the wrapped rows below it. */
padding: 20px 52px 20px 18px;
padding: 16px 18px 20px;
min-height: 224px;
box-shadow: var(--tray-inset);
}
/* Pinned out of flow to the reserved strip beside the first (rightmost) card,
vertically centred on the top row, so it never affects card alignment. */
/* The "first to fight" label sits at the top of the column, above the first
card. It's a static first child (no data-flip-key), so the FLIP animator
ignores it. */
.arrange-marker {
position: absolute;
top: 20px;
right: 16px;
height: 148px;
display: flex;
align-items: center;
z-index: 2;
direction: ltr;
font-family: var(--font-display);
color: var(--gold);
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 0.95rem;
letter-spacing: 0.04em;
opacity: 0.85;
padding-bottom: 2px;
}
/* SVG overlay for the wrap-around connector arrows. Absolute so it sits out of
the flex flow; pointer-events off so it never blocks dragging. overflow is
visible because the curve control points can bow just past the tray edge. */
.arrange-connectors {
position: absolute;
inset: 0;
z-index: 3;
overflow: visible;
pointer-events: none;
}
.arrange-connector {
fill: none;
stroke: rgba(190, 194, 200, 0.55);
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.arrange-arrowhead-shape {
fill: rgba(190, 194, 200, 0.55);
}
/* Each row: reorder arrows on the left, the card on the right. */
.arrange-card {
direction: ltr;
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
gap: 8px;
gap: 12px;
cursor: grab;
}
@@ -1649,7 +1685,8 @@ h3 {
.arrange-arrows {
display: flex;
gap: 5px;
flex-direction: column;
gap: 6px;
}
/* ---------- battle ---------- */
@@ -1710,18 +1747,19 @@ h3 {
.battlefield {
position: relative;
display: flex;
align-items: center;
flex-direction: column;
align-items: stretch;
justify-content: center;
gap: 8px;
gap: 6px;
background:
radial-gradient(ellipse 55% 80% at center, rgba(246, 201, 78, 0.12), transparent 66%),
radial-gradient(ellipse 80% 55% at center, rgba(246, 201, 78, 0.12), transparent 66%),
linear-gradient(180deg, rgba(0, 0, 0, 0.28), rgba(0, 0, 0, 0.18));
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 20px;
/* Roomy top/bottom padding: pets keep their central row while set-aside
cards fan above and attached foods fan below. The bottom needs plenty of
room since a pet can carry a big stack of apples (Manatee, Fire Ant, …). */
padding: 132px 14px 268px;
/* Side padding leaves room for set-aside pets (left) and attached foods
(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;
overflow-x: auto;
box-shadow:
@@ -1735,18 +1773,17 @@ h3 {
flex-shrink: 0;
display: grid;
place-items: center;
min-width: 2.4rem;
align-self: stretch;
min-height: 2.4rem;
}
/* A soft seam down the middle of the arena. */
/* A soft seam across the middle of the arena, where the two sides clash. */
.battle-center::before {
content: '';
position: absolute;
top: 8%;
bottom: 8%;
width: 2px;
background: linear-gradient(180deg, transparent, rgba(246, 201, 78, 0.25), transparent);
left: 8%;
right: 8%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(246, 201, 78, 0.25), transparent);
}
.battle-center-bolt {
@@ -1754,12 +1791,20 @@ h3 {
filter: drop-shadow(0 0 8px rgba(246, 201, 78, 0.5));
}
/* One player's half. The opponent (top) stacks deck-over-pet; you (bottom)
stack pet-over-deck, so both decks sit on the outer edge and the two pets
meet at the centre seam. */
.battle-side {
display: flex;
gap: 12px;
flex-direction: column;
align-items: center;
flex: 1;
gap: 10px;
min-width: 0;
}
/* Centres the deck stack under (you) / over (opponent) the pet. */
.battle-deck {
display: flex;
justify-content: center;
}
@@ -1838,53 +1883,57 @@ h3 {
pointer-events: none;
}
/* Spawn arcs out from the pet (inner edge) onto the deck. The pet sits to the
right of the deck on the left side and to the left on the right side, so
each side pulls its spawn in from the opposite direction. */
@keyframes summon-from-right {
/* Spawn arcs out from the pet onto the deck. The pet sits toward the centre
seam — above your deck (bottom half) and below the opponent's (top half) — so
each spawn is pulled in from its own pet's side. */
@keyframes summon-to-bottom {
0% {
opacity: 0;
transform: translate(120px, 8px) scale(0.5) rotate(10deg);
transform: translateY(-70px) scale(0.5);
}
25% {
opacity: 1;
transform: translate(64px, -22px) scale(1.05) rotate(5deg);
transform: translateY(-30px) scale(1.05);
}
70% {
opacity: 1;
transform: translate(0, -12px) scale(1) rotate(0);
transform: translateY(4px) scale(1);
}
100% {
opacity: 0;
transform: translate(0, 2px) scale(0.72);
transform: translateY(14px) scale(0.72);
}
}
@keyframes summon-from-left {
@keyframes summon-to-top {
0% {
opacity: 0;
transform: translate(-120px, 8px) scale(0.5) rotate(-10deg);
transform: translateY(70px) scale(0.5);
}
25% {
opacity: 1;
transform: translate(-64px, -22px) scale(1.05) rotate(-5deg);
transform: translateY(30px) scale(1.05);
}
70% {
opacity: 1;
transform: translate(0, -12px) scale(1) rotate(0);
transform: translateY(-4px) scale(1);
}
100% {
opacity: 0;
transform: translate(0, 2px) scale(0.72);
transform: translateY(-14px) scale(0.72);
}
}
.summon-pop.summon-from-right {
animation-name: summon-from-right;
/* You throw from the bottom (deck below the pet); the opponent from the top. */
.summon-pop.summon-to-bottom {
top: auto;
bottom: calc(100% - 24px);
animation-name: summon-to-bottom;
}
.summon-pop.summon-from-left {
animation-name: summon-from-left;
.summon-pop.summon-to-top {
top: calc(100% - 24px);
animation-name: summon-to-top;
}
/* Apple/bee spawned in the shop, flying out from the creature that made it. */
@@ -1948,27 +1997,27 @@ h3 {
place-items: center;
}
/* Set-aside pets (Blowfish, Badger, …) laid out in a row above the active
pet, kept until their pending effect resolves. */
/* Set-aside pets (Blowfish, Badger, …) laid out in a row to the left of the
active pet, kept until their pending effect resolves. */
.setaside-row {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
right: calc(100% + 6px);
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 6px;
z-index: 5;
}
/* Attached (or pending) foods fanned below the pet: a vertical stack that
overlaps ~¾ so only each card's top edge shows, last card fully on top. */
/* Attached (or pending) foods fanned to the right of the pet: a horizontal row
that overlaps ~¾ so only each card's left edge shows, last card fully on top. */
.food-fan {
position: absolute;
top: calc(100% - 6px);
left: 50%;
transform: translateX(-50%);
left: calc(100% - 6px);
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
z-index: 4;
}
@@ -1978,8 +2027,8 @@ h3 {
}
.food-fan-card:not(:first-child) {
/* Slightly tighter overlap so a tall apple stack fits the play area. */
margin-top: -92px;
/* Overlap so a tall apple stack stays compact beside the pet. */
margin-left: -64px;
}
/* Released cards (a spent set-aside pet, or a used-up food perk) shrink and
@@ -1998,13 +2047,13 @@ h3 {
pointer-events: none;
}
/* A freshly dealt food (apple) sails in from the deck — up and toward the outer
edge — and settles onto the fan, rather than blinking into place. Direction
follows the side the deck sits on (see food-in-left / food-in-right). */
@keyframes food-in-left {
/* A freshly dealt food (apple) sails in from the deck and settles onto the fan,
rather than blinking into place. The deck sits below the pet on your side and
above it on the opponent's, so the apple flies up (bottom) or down (top). */
@keyframes food-in-bottom {
from {
opacity: 0;
transform: translate(-64px, -52px) rotate(-14deg) scale(0.6);
transform: translate(-30px, 70px) rotate(-12deg) scale(0.6);
}
to {
opacity: 1;
@@ -2012,10 +2061,10 @@ h3 {
}
}
@keyframes food-in-right {
@keyframes food-in-top {
from {
opacity: 0;
transform: translate(64px, -52px) rotate(14deg) scale(0.6);
transform: translate(-30px, -70px) rotate(12deg) scale(0.6);
}
to {
opacity: 1;
@@ -2023,27 +2072,27 @@ h3 {
}
}
.food-fan-card.food-in-left {
animation: food-in-left 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
.food-fan-card.food-in-bottom {
animation: food-in-bottom 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
}
.food-fan-card.food-in-right {
animation: food-in-right 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
.food-fan-card.food-in-top {
animation: food-in-top 440ms cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
}
/* A pet that faints with a pending effect rises up out of the arena into the
set-aside row, shrinking from its arena size to the smaller set-aside size. */
/* A pet that faints with a pending effect slides out of the arena into the
set-aside row on its left, shrinking to the smaller set-aside size. */
@keyframes setaside-in {
from {
opacity: 0;
transform: translateY(150px) scale(1.2);
transform: translateX(120px) scale(1.2);
}
55% {
opacity: 1;
}
to {
opacity: 1;
transform: translateY(0) scale(1);
transform: translateX(0) scale(1);
}
}
@@ -2071,61 +2120,68 @@ h3 {
}
/* A pet flips face-up off its deck: it starts over the stack (outer edge) still
turned edge-on, then slides in toward its arena slot while turning to face us.
The deck sits to the outer side of each unit, so each side pulls the reveal in
from its own edge. */
@keyframes unit-reveal-left {
turned edge-on, then rises in toward its arena slot while turning to face us.
The deck sits below your pet and above the opponent's, so each side pulls the
reveal in from its own edge. */
@keyframes unit-reveal-bottom {
from {
opacity: 0;
transform: translateX(-108px) perspective(760px) rotateY(-92deg) scale(0.92);
transform: translateY(108px) perspective(760px) rotateX(-92deg) scale(0.92);
}
45% {
opacity: 1;
}
to {
opacity: 1;
transform: translateX(0) perspective(760px) rotateY(0deg) scale(1);
transform: translateY(0) perspective(760px) rotateX(0deg) scale(1);
}
}
@keyframes unit-reveal-right {
@keyframes unit-reveal-top {
from {
opacity: 0;
transform: translateX(108px) perspective(760px) rotateY(92deg) scale(0.92);
transform: translateY(-108px) perspective(760px) rotateX(92deg) scale(0.92);
}
45% {
opacity: 1;
}
to {
opacity: 1;
transform: translateX(0) perspective(760px) rotateY(0deg) scale(1);
transform: translateY(0) perspective(760px) rotateX(0deg) scale(1);
}
}
.battle-unit.unit-reveal-left {
transform-origin: left center;
animation: unit-reveal-left 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
.battle-unit.unit-reveal-bottom {
transform-origin: center bottom;
animation: unit-reveal-bottom 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
.battle-unit.unit-reveal-right {
transform-origin: right center;
animation: unit-reveal-right 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
.battle-unit.unit-reveal-top {
transform-origin: center top;
animation: unit-reveal-top 520ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
/* --- rock dice --- */
/* A little tray of dice, drawn just below the throwing side's deck (see
.stack-dice). Fixed slots: the dice scramble in place, then settle showing
the rolled rock faces. */
/* A little tray of dice, drawn beside the throwing side's deck on the side that
faces the centre seam — above your deck, below the opponent's. Fixed slots:
the dice scramble in place, then settle showing the rolled rock faces. */
.stack-dice {
position: absolute;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
z-index: 8;
pointer-events: none;
}
.battle-deck-bottom .stack-dice {
bottom: calc(100% + 8px);
}
.battle-deck-top .stack-dice {
top: calc(100% + 8px);
}
.dice-roll {
display: flex;
gap: 7px;
@@ -2247,42 +2303,43 @@ h3 {
filter: grayscale(1);
}
@keyframes clash-left {
/* You (bottom) lunge up into the seam; the opponent (top) lunges down. */
@keyframes clash-bottom {
0% {
transform: translateX(0);
transform: translateY(0);
}
35% {
transform: translateX(26px) rotate(4deg);
transform: translateY(-26px) rotate(2deg);
}
60% {
transform: translateX(-6px);
transform: translateY(6px);
}
100% {
transform: translateX(0);
transform: translateY(0);
}
}
@keyframes clash-right {
@keyframes clash-top {
0% {
transform: translateX(0);
transform: translateY(0);
}
35% {
transform: translateX(-26px) rotate(-4deg);
transform: translateY(26px) rotate(-2deg);
}
60% {
transform: translateX(6px);
transform: translateY(-6px);
}
100% {
transform: translateX(0);
transform: translateY(0);
}
}
.battle-unit.clash-left {
animation: clash-left 500ms ease;
.battle-unit.clash-bottom {
animation: clash-bottom 500ms ease;
}
.battle-unit.clash-right {
animation: clash-right 500ms ease;
.battle-unit.clash-top {
animation: clash-top 500ms ease;
}
@keyframes dying {
@@ -2462,9 +2519,6 @@ h3 {
width: 92px;
height: 128px;
}
.arrange-marker {
height: 128px;
}
.card-lg {
width: 104px;
height: 146px;
@@ -2624,22 +2678,12 @@ h3 {
max-width: min(60vw, 560px);
}
/* first-right: the first pet sits on the right (matches your own arrangement).
first-left: the first pet sits on the left (a rival, who faces you from the
right of the board). The "⚔️ first" marker is the first DOM child, so it
lands next to the first card at whichever end. */
.deck-peek-cards.first-right {
direction: rtl;
}
/* first-left: the lineup runs first-to-last, left to right, with the "⚔️ first"
marker as the first DOM child so it lands next to the first card. */
.deck-peek-cards.first-left {
direction: ltr;
}
/* Keep each card's own contents left-to-right regardless of row direction. */
.deck-peek-cards > * {
direction: ltr;
}
/* Compact echo of the arrangement phase's "⚔️ first" indicator. */
.deck-peek-first {
display: flex;