/* ============================================================ Super Auto Pets: The Board Game — cozy tabletop, elevated. The whole app is one lamp-lit game table: a warm spotlight over deep felt, walnut rails, and thick printed cards that cast real shadows. Panels read as recessed felt trays, not floating boxes. Palette stays warm end-to-end — pine, walnut, parchment, honey, coral — so nothing feels generic or cold. ============================================================ */ :root { /* Felt — a lamp-lit pine table, warm in the middle, dark at the rim. */ --felt-900: #0f2c20; --felt-800: #163a2a; --felt-700: #1d4c35; --felt-600: #266145; --felt-lit: #2c6f4d; /* Walnut rails and trays. */ --wood: #5a3a20; --wood-light: #815129; --wood-dark: #3d2614; /* Printed parchment cards. */ --cream: #fbf1d7; --cream-dark: #efdcac; --cocoa: #4a2c14; --cocoa-soft: #6b4426; --ink: #35240f; --ink-soft: rgba(53, 36, 15, 0.66); /* Warm accents. */ --coral: #f4884a; --coral-dark: #d9611a; --gold: #f6c94e; --gold-deep: #e0a52f; --honey: #f0b64a; --sage: #4f9d78; --sage-dark: #327a58; --red: #d94a38; --red-soft: #ffb3a7; --font-display: 'Lilita One', system-ui, sans-serif; --font-body: 'Nunito', system-ui, sans-serif; --card-radius: 14px; /* Reusable warm shadows so depth stays consistent everywhere. */ --shadow-card: 0 10px 20px -6px rgba(20, 12, 4, 0.55), 0 2px 4px rgba(20, 12, 4, 0.4); --tray-inset: inset 0 2px 5px rgba(0, 0, 0, 0.45), inset 0 -1px 0 rgba(255, 255, 255, 0.05); } * { margin: 0; padding: 0; box-sizing: border-box; } html, body, #root { min-height: 100vh; } body { font-family: var(--font-body); color: var(--cream); background-color: var(--felt-900); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; /* A warm overhead lamp, a soft felt weave, fine grain, and a dark rim. */ background-image: radial-gradient(ellipse 90% 60% at 50% -8%, rgba(255, 214, 138, 0.16), transparent 60%), radial-gradient(ellipse 70% 55% at 50% 32%, rgba(64, 150, 104, 0.22), transparent 70%), radial-gradient(ellipse at 50% 116%, rgba(0, 0, 0, 0.55), transparent 62%), repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.012) 0 2px, transparent 2px 4px), repeating-linear-gradient(-45deg, rgba(0, 0, 0, 0.03) 0 2px, transparent 2px 4px), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.045'/%3E%3C/svg%3E"), linear-gradient(165deg, var(--felt-700), var(--felt-900) 78%); background-attachment: fixed; } h1, h2, h3 { font-family: var(--font-display); font-weight: 400; letter-spacing: 0.01em; } .muted { color: rgba(253, 243, 220, 0.52); } .centered { min-height: 60vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; text-align: center; } .hint { font-size: 0.9rem; color: rgba(253, 243, 220, 0.62); text-align: center; } .warn-text { color: var(--gold); font-weight: 800; } /* ---------- buttons ---------- */ /* Tactile printed tokens: a soft top sheen, a pressed edge underneath, and a satisfying travel on click — like pushing a wooden meeple down. */ .btn { position: relative; font-family: var(--font-display); font-size: 1rem; letter-spacing: 0.03em; color: var(--cream); background: linear-gradient(180deg, var(--wood-light), var(--wood)); border: none; border-radius: 12px; padding: 11px 20px; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), inset 0 -2px 3px rgba(0, 0, 0, 0.28), 0 4px 0 var(--wood-dark), 0 6px 10px rgba(0, 0, 0, 0.35); transition: transform 90ms ease, box-shadow 90ms ease, filter 140ms ease; } .btn:hover:not(:disabled) { filter: brightness(1.07) saturate(1.05); } .btn:active:not(:disabled) { transform: translateY(3px); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), inset 0 -1px 2px rgba(0, 0, 0, 0.3), 0 1px 0 var(--wood-dark), 0 2px 4px rgba(0, 0, 0, 0.35); } .btn:disabled { opacity: 0.42; cursor: not-allowed; filter: saturate(0.6); } .btn-primary { background: linear-gradient(180deg, #ffa257, var(--coral-dark)); color: #fff; text-shadow: 0 1px 2px rgba(120, 45, 5, 0.55); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35), inset 0 -2px 3px rgba(150, 55, 0, 0.4), 0 4px 0 #ab4a13, 0 7px 12px rgba(120, 45, 5, 0.4); } .btn-primary:active:not(:disabled) { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), inset 0 -1px 2px rgba(150, 55, 0, 0.4), 0 1px 0 #ab4a13, 0 2px 5px rgba(120, 45, 5, 0.4); } .btn-secondary { background: linear-gradient(180deg, #5cb68c, var(--sage-dark)); color: #f4fff8; text-shadow: 0 1px 2px rgba(15, 60, 40, 0.5); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), inset 0 -2px 3px rgba(15, 60, 40, 0.35), 0 4px 0 #245840, 0 7px 12px rgba(15, 60, 40, 0.35); } .btn-secondary:active:not(:disabled) { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), inset 0 -1px 2px rgba(15, 60, 40, 0.35), 0 1px 0 #245840, 0 2px 5px rgba(15, 60, 40, 0.35); } .btn-ghost { background: rgba(253, 243, 220, 0.06); border: 2px solid rgba(253, 243, 220, 0.22); color: var(--cream); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); } .btn-ghost:hover:not(:disabled) { background: rgba(253, 243, 220, 0.12); filter: none; } .btn-ghost:active:not(:disabled) { transform: translateY(2px); box-shadow: none; } .btn-big { font-size: 1.3rem; padding: 15px 32px; letter-spacing: 0.02em; } .btn-sm { font-size: 0.8rem; padding: 6px 11px; border-radius: 9px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 3px 0 var(--wood-dark), 0 4px 6px rgba(0, 0, 0, 0.3); } .btn-ghost.btn-sm { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); } /* ---------- home ---------- */ .home { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 22px; padding: 24px; } .home-pets { font-size: 2.8rem; letter-spacing: 0.28em; padding-left: 0.28em; filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.45)); animation: float 3s ease-in-out infinite; } @keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } .home-title { font-size: clamp(2.4rem, 6.5vw, 3.9rem); text-align: center; color: var(--gold); text-shadow: 0 2px 0 var(--gold-deep), 0 5px 0 rgba(90, 58, 32, 0.5), 0 8px 14px rgba(0, 0, 0, 0.4); display: flex; flex-direction: column; line-height: 1.02; } .home-subtitle { font-size: 0.4em; color: var(--cream); letter-spacing: 0.34em; padding-left: 0.34em; text-transform: uppercase; text-shadow: 0 2px 3px rgba(0, 0, 0, 0.4); margin-top: 0.15em; } /* The home card is a little parchment sign-in slip pinned to the felt. */ .home-card { position: relative; background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.22)); border: 1px solid rgba(253, 243, 220, 0.14); border-radius: 20px; padding: 30px; display: flex; flex-direction: column; gap: 16px; width: min(384px, 92vw); box-shadow: var(--tray-inset), 0 18px 40px -12px rgba(0, 0, 0, 0.6); } .field { display: flex; flex-direction: column; gap: 7px; font-weight: 800; font-size: 0.82rem; letter-spacing: 0.04em; text-transform: uppercase; color: rgba(253, 243, 220, 0.7); } .field input, .code-input { font-family: var(--font-body); font-size: 1.1rem; font-weight: 700; color: var(--ink); background: linear-gradient(180deg, #fff8e8, var(--cream)); border: 2px solid var(--cocoa-soft); border-radius: 11px; padding: 11px 13px; outline: none; width: 100%; box-shadow: inset 0 2px 4px rgba(74, 44, 20, 0.18); transition: border-color 120ms ease, box-shadow 120ms ease; } .field input::placeholder { color: rgba(53, 36, 15, 0.38); font-weight: 600; } .field input:focus, .code-input:focus { border-color: var(--coral); box-shadow: inset 0 2px 4px rgba(74, 44, 20, 0.18), 0 0 0 3px rgba(244, 136, 74, 0.28); } .code-input { font-family: var(--font-display); letter-spacing: 0.35em; padding-left: calc(13px + 0.35em); text-transform: uppercase; text-align: center; } .home-divider { text-align: center; font-size: 0.82rem; color: rgba(253, 243, 220, 0.5); display: flex; align-items: center; gap: 12px; text-transform: uppercase; letter-spacing: 0.08em; } .home-divider::before, .home-divider::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, transparent, rgba(253, 243, 220, 0.28), transparent); } .home-join { display: flex; gap: 10px; } .home-hint { text-align: center; font-size: 0.9rem; margin: 0; } .home-error { color: var(--red-soft); font-weight: 700; text-align: center; } /* ---------- topbar ---------- */ .table { min-height: 100vh; display: flex; flex-direction: column; } /* A solid walnut rail across the top of the table, with grain and a worn highlight along its top edge. */ .topbar { display: flex; align-items: center; gap: 16px; padding: 11px 18px; background: repeating-linear-gradient(92deg, rgba(0, 0, 0, 0.05) 0 3px, transparent 3px 7px), linear-gradient(180deg, var(--wood-light), var(--wood) 65%, var(--wood-dark)); border-bottom: 3px solid var(--wood-dark); box-shadow: inset 0 1px 0 rgba(255, 220, 170, 0.25), 0 4px 14px rgba(0, 0, 0, 0.4); flex-wrap: wrap; position: sticky; top: 0; z-index: 10; } .topbar-brand { display: flex; align-items: center; gap: 7px; font-family: var(--font-display); font-size: 1.3rem; color: var(--gold); text-shadow: 0 2px 2px rgba(0, 0, 0, 0.45); letter-spacing: 0.04em; } /* The round shows as a die face — 1–6 pips, like the real game's round marker. */ .topbar-round { display: flex; align-items: center; } .topbar-die { width: 26px; height: 26px; display: block; filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.4)); } .topbar-players { display: flex; gap: 12px; flex: 1; flex-wrap: wrap; } /* Player tags read like enamel pins clipped to the rail. */ .topbar-player { display: flex; align-items: center; gap: 7px; background: linear-gradient(180deg, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.3)); border-radius: 999px; padding: 5px 13px; font-size: 0.9rem; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 1px 2px rgba(0, 0, 0, 0.3); } .topbar-player.is-you { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 0 0 2px var(--gold), 0 1px 3px rgba(0, 0, 0, 0.35); } .topbar-name { font-weight: 800; } .chip { font-size: 0.85rem; font-weight: 700; } /* A chip that's actually a button (e.g. peek at the opponent's deck). */ .chip-btn { font-family: var(--font-body); cursor: pointer; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.08); color: inherit; border-radius: 999px; padding: 1px 8px; transition: background 120ms ease, border-color 120ms ease; } .chip-btn:hover { background: rgba(255, 255, 255, 0.18); } .chip-btn.is-active { background: var(--gold); color: var(--cocoa); border-color: var(--gold); } .conn-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25); } .conn-dot.on { background: #6fe08b; box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25), 0 0 6px rgba(111, 224, 139, 0.7); } .conn-dot.off { background: var(--red); } /* Bots swap the connection dot for a little robot face. */ .bot-dot { font-size: 0.85rem; line-height: 1; } /* The room code, stamped like a label on a card tray. Lives inside the ⋯ menu. */ .topbar-code { font-family: var(--font-display); letter-spacing: 0.28em; padding: 5px 10px 5px calc(10px + 0.28em); color: var(--gold); background: rgba(0, 0, 0, 0.28); border: 2px dashed rgba(246, 201, 78, 0.5); border-radius: 8px; } /* The ⋯ menu holds the room code and Leave, pushed to the right of the rail so the middle is just the players and their stats. */ .topbar-menu { position: relative; margin-left: auto; } .topbar-menu-btn { font-size: 1.15rem; line-height: 1; padding: 5px 12px; } /* A full-viewport catcher so a click anywhere off the menu closes it. */ .topbar-menu-backdrop { position: fixed; inset: 0; z-index: 20; } .topbar-menu-pop { position: absolute; right: 0; top: calc(100% + 8px); z-index: 21; min-width: 190px; display: flex; flex-direction: column; gap: 12px; padding: 14px; background: repeating-linear-gradient(92deg, rgba(0, 0, 0, 0.05) 0 3px, transparent 3px 7px), linear-gradient(180deg, var(--wood-light), var(--wood) 70%, var(--wood-dark)); border: 2px solid var(--wood-dark); border-radius: 12px; box-shadow: inset 0 1px 0 rgba(255, 220, 170, 0.25), 0 14px 30px rgba(0, 0, 0, 0.55); } .topbar-menu-code { display: flex; flex-direction: column; gap: 5px; align-items: flex-start; font-size: 0.78rem; } .table-body { flex: 1; display: flex; align-items: flex-start; 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: 22px 16px 48px; max-width: 620px; width: 100%; margin: 0 auto; } /* ---------- event log ---------- */ /* A pinned-up ledger of what happened, paper-warm against the felt. */ .event-log { width: 300px; flex-shrink: 0; align-self: stretch; position: sticky; /* Sit below the now-sticky topbar (~60px tall on wide screens) instead of sliding behind it when the page scrolls. */ top: 60px; max-height: calc(100vh - 68px); display: flex; flex-direction: column; background: linear-gradient(180deg, rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.34)); border-left: 1px solid rgba(0, 0, 0, 0.4); box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.04); } .event-log.is-collapsed { width: 42px; } .event-log-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 10px 12px; font-family: var(--font-display); font-size: 0.92rem; color: var(--gold); background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.15)); border-bottom: 1px solid rgba(0, 0, 0, 0.35); box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.05); white-space: nowrap; } .event-log.is-collapsed .event-log-head { writing-mode: vertical-rl; justify-content: flex-start; gap: 10px; padding: 10px 8px; height: 100%; } .event-log.is-collapsed .event-log-head button { writing-mode: horizontal-tb; } .event-log-body { flex: 1; overflow-y: auto; padding: 10px 10px 14px; display: flex; flex-direction: column; gap: 5px; font-size: 0.82rem; line-height: 1.3; } .log-empty { padding: 8px 2px; font-style: italic; } .log-line { display: flex; gap: 7px; align-items: baseline; padding: 4px 8px; border-radius: 7px; border-left: 3px solid transparent; background: rgba(255, 255, 255, 0.035); } .log-icon { flex-shrink: 0; } .log-text { min-width: 0; word-break: break-word; } /* Card names embedded in a log line: a subtle dotted underline hints they can be hovered to preview the card. */ .log-card-ref { text-decoration: underline dotted rgba(253, 243, 220, 0.45); text-underline-offset: 2px; cursor: help; font-weight: 600; color: var(--cream); } .log-card-ref:hover { text-decoration-color: var(--gold); color: var(--gold); } .log-line.log-you { border-left-color: var(--gold); } .log-line.log-opp { border-left-color: var(--red); } .log-line.log-system { background: none; color: var(--cream-dark); justify-content: center; font-family: var(--font-display); font-size: 0.8rem; letter-spacing: 0.06em; opacity: 0.85; margin-top: 4px; } .log-line.log-divider { border-top: 1px dashed rgba(253, 243, 220, 0.25); padding-top: 8px; color: var(--gold); } .log-line.log-current { background: rgba(246, 201, 78, 0.18); box-shadow: inset 0 0 0 1px rgba(246, 201, 78, 0.45); } /* 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-dock.is-open { transform: translateX(0); } .event-log-dock .event-log { width: 100%; height: 100%; max-height: none; position: static; border-left: 1px solid rgba(0, 0, 0, 0.4); } .event-log-dock .event-log.is-collapsed { width: 100%; } .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 ---------- */ .lobby { max-width: 620px; margin: 0 auto; padding: 8px 16px 32px; display: flex; flex-direction: column; gap: 22px; } .lobby-heading { text-align: center; color: var(--gold); } .lobby-bounce { font-size: 3.2rem; filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.45)); animation: float 2s ease-in-out infinite; } .lobby-removed { gap: 14px; text-align: center; } .lobby-code-row { display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; } .lobby-code { font-family: var(--font-display); font-size: 1.8rem; letter-spacing: 0.3em; padding: 6px 16px 6px calc(16px + 0.3em); color: var(--gold); background: rgba(0, 0, 0, 0.25); border: 3px dashed rgba(246, 201, 78, 0.5); border-radius: 14px; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); box-shadow: var(--tray-inset); } .lobby-section { display: flex; flex-direction: column; gap: 12px; } .lobby-section-title { font-family: var(--font-display); font-size: 1.15rem; color: var(--cream); } .lobby-note, .lobby-waiting { text-align: center; } .lobby-waiting { font-size: 1.05rem; } /* ---------- pack picker ---------- */ .pack-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } .pack-card { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 16px 10px 12px; border-radius: var(--card-radius); border: 3px solid transparent; background: rgba(0, 0, 0, 0.22); color: var(--cream); cursor: pointer; transition: transform 0.12s ease, border-color 0.12s ease; } .pack-card:not(:disabled):hover { transform: translateY(-2px); } .pack-card.is-selected { border-color: var(--gold); background: rgba(246, 201, 78, 0.14); } .pack-card.is-locked { opacity: 0.45; cursor: not-allowed; filter: grayscale(0.7); } .pack-card:disabled { cursor: not-allowed; } .pack-emoji { font-size: 2.4rem; line-height: 1; } .pack-name { font-family: var(--font-display); font-size: 0.95rem; } .pack-tag { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gold); } .pack-card.is-locked .pack-tag { color: var(--cream); } /* ---------- seat list ---------- */ .seat-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; } .seat-row { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: 12px; background: rgba(0, 0, 0, 0.22); box-shadow: var(--tray-inset); } .seat-open { border: 2px dashed rgba(251, 241, 215, 0.25); background: rgba(0, 0, 0, 0.12); box-shadow: none; } .seat-avatar { font-size: 1.5rem; line-height: 1; } .seat-name { font-weight: 800; flex: 1; } .seat-you { color: var(--gold); font-weight: 700; } .seat-badges { display: flex; align-items: center; gap: 8px; } .seat-remove { border: none; background: rgba(217, 74, 56, 0.2); color: var(--red-soft); font-weight: 900; width: 26px; height: 26px; border-radius: 50%; cursor: pointer; line-height: 1; } .seat-remove:hover { background: var(--red); color: #fff; } .seat-open-host { display: flex; flex-direction: column; gap: 10px; width: 100%; } .seat-bot-picker { display: flex; gap: 8px; flex-wrap: wrap; } .seat-bot-picker .btn { flex: 1; white-space: nowrap; } /* ---------- cards ---------- */ /* Thick printed stock: a warm parchment face, a soft ink border with an inner keyline, a physical shadow, and a lit sheen along the top. */ /* Cards echo the printed board-game cards: a chunky golden frame around a landscape illustration up top and a green-framed rule panel below. */ .card { position: relative; width: 114px; height: 154px; background: linear-gradient(165deg, #f8c451, #f0a828 55%, #e2941c); border-radius: var(--card-radius); color: var(--ink); display: flex; flex-direction: column; padding: 5px; gap: 4px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5), inset 0 0 0 2.5px rgba(150, 92, 18, 0.55), var(--shadow-card); flex-shrink: 0; transition: transform 130ms cubic-bezier(0.34, 1.4, 0.5, 1), box-shadow 130ms ease; user-select: none; } .card-lg { width: 140px; height: 190px; } .card-sm { width: 88px; height: 120px; } .card.is-clickable { cursor: pointer; } .card.is-clickable:hover { transform: translateY(-8px) rotate(-1.2deg); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5), inset 0 0 0 2.5px rgba(150, 92, 18, 0.55), 0 18px 30px -8px rgba(20, 12, 4, 0.6), 0 4px 8px rgba(20, 12, 4, 0.4); } .card.is-selected { outline: 3px solid var(--gold); outline-offset: 1px; transform: translateY(-8px); box-shadow: inset 0 0 0 2.5px rgba(150, 92, 18, 0.55), 0 0 20px rgba(246, 201, 78, 0.5), var(--shadow-card); } .card.is-disabled { filter: saturate(0.55) brightness(0.86); } .card.is-dead { filter: grayscale(1); } /* --- Upper illustration: a stylised landscape --- */ .card-scene { position: relative; /* Fixed share of the card so the panel below (and the ability text inside it) stays height-bounded — that's what lets useFitText shrink long abilities. */ flex: 0 0 58%; min-height: 0; border-radius: 7px; overflow: hidden; box-shadow: inset 0 0 0 1.5px rgba(90, 55, 15, 0.35); display: flex; align-items: center; justify-content: center; background: radial-gradient(120% 70% at 50% -8%, rgba(255, 255, 255, 0.4), transparent 55%), linear-gradient( 180deg, #c2e8f8 0%, #a7dcf0 24%, #74bf68 44%, #57a44b 58%, #e0a751 66%, #d5942f 86%, #57a44b 90%, #4c9640 100% ); } /* Distant mountain silhouette in the hills band. */ .card-scene::before { content: ''; position: absolute; left: -2%; right: -2%; top: 24%; height: 22%; background: linear-gradient(180deg, #6ea0c6, #5388b2); clip-path: polygon( 0 100%, 10% 45%, 20% 78%, 34% 22%, 47% 66%, 60% 30%, 73% 72%, 85% 40%, 100% 82%, 100% 100% ); opacity: 0.9; } /* A row of rounded bush tops where the greenery begins. */ .card-scene::after { content: ''; position: absolute; left: 0; right: 0; top: 40%; height: 12%; background: radial-gradient(circle at 50% 100%, #3f8f3a 0 45%, transparent 47%) 0 0 / 21% 100% repeat-x; opacity: 0.92; } /* The pet reads as a die-cut sticker: a white outline hugging the art. */ .card-art { position: relative; z-index: 2; font-size: 2.5rem; line-height: 1; transform: translateY(8%); filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.35)); } /* Pet illustrations render as PNG stickers sized off the art font-size, so the lg/sm/battle overrides below scale them for free. */ .card-art-img { display: block; width: 1.5em; height: 1.5em; object-fit: contain; } .card-lg .card-art { font-size: 3.1rem; } .card-sm .card-art { font-size: 2rem; } /* --- Lower rule panel --- */ .card-panel { flex: 1 1 auto; min-height: 0; background: #fbf7ee; border: 2px solid #4e9a3f; border-radius: 8px; padding: 3px 4px 4px; display: flex; flex-direction: column; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.6); } /* Small cards (no ability text) get a compact name plate and a taller scene. */ .card-sm .card-scene { flex: 1 1 auto; } .card-sm .card-panel { flex: 0 0 auto; } .card-panel-head { display: grid; /* minmax(0, 1fr) lets the name column shrink below its min-content so a long name wraps instead of shoving the tier die out past the card edge. */ grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 4px; border-bottom: 1.5px solid rgba(78, 154, 63, 0.4); padding-bottom: 2px; } /* The suit marker: a plain enamel dot (see .suit-dot), sized to sit opposite the tier die in the name row. */ .card-suit { width: 14px; height: 14px; } .card-die { width: 15px; height: 15px; display: block; justify-self: end; } .card-panel-slot { display: block; width: 15px; } .card-lg .card-suit { width: 16px; height: 16px; } .card-lg .card-die, .card-lg .card-panel-slot { width: 17px; height: 17px; } .card-sm .card-suit { width: 11px; height: 11px; } .card-sm .card-die, .card-sm .card-panel-slot { width: 12px; height: 12px; } .card-name { font-family: var(--font-display); font-size: 0.78rem; line-height: 1.05; color: var(--cocoa); text-align: center; min-width: 0; /* Keep the name to a single line, like the real cards; useFitText shrinks longer names to fit the row instead of letting them wrap. */ white-space: nowrap; overflow: hidden; } .card-lg .card-name { font-size: 0.9rem; } .card-sm .card-name { font-size: 0.66rem; } .card-effect { font-size: 0.52rem; line-height: 1.18; font-weight: 700; color: var(--ink-soft); text-align: center; padding: 2px 1px 0; /* Grow to fill the panel and center short abilities; long ones top-align so the leading words are never the clipped ones. */ flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; align-items: safe center; justify-content: center; overflow: hidden; } /* Each ability on its own line within the effect band. */ .card-effect-line { display: block; } .card-lg .card-effect { font-size: 0.6rem; } .card-sm .card-effect { display: none; } /* Suit markers as glossy enamel dots. */ .suit-dot { display: inline-block; width: 14px; height: 14px; border-radius: 50%; vertical-align: middle; /* Inset ring instead of a real border — see .card-power. */ box-shadow: inset 0 0 0 1.5px rgba(0, 0, 0, 0.3), inset 0 1.5px 1px rgba(255, 255, 255, 0.6), inset 0 -2px 2px rgba(0, 0, 0, 0.25); } .suit-red { background: radial-gradient(circle at 35% 28%, #ff6b5f, #d23124); } .suit-blue { background: radial-gradient(circle at 35% 28%, #6ba6f5, #2f6bd0); } .suit-yellow { background: radial-gradient(circle at 35% 28%, #ffdd6b, #e6ac1f); } /* The power badge is a red starburst floating at the top of the illustration, like a little explosion carrying the pet's attack. The spikes are cut with clip-path, so the drop shadow lives on `filter`. */ .card-power { position: absolute; top: 4%; left: 50%; transform: translateX(-50%); z-index: 3; font-family: var(--font-display); background: radial-gradient(circle at 40% 30%, #ff8a6b 2%, #e23b28 44%, #bd2716); color: #fff; -webkit-text-stroke: 0.5px rgba(120, 20, 5, 0.85); text-shadow: 0 1px 1px rgba(120, 20, 5, 0.8); width: 30px; height: 30px; display: grid; place-items: center; font-size: 0.92rem; clip-path: polygon( 50% 0%, 61% 18%, 82% 12%, 78% 34%, 100% 43%, 83% 58%, 93% 79%, 70% 76%, 62% 98%, 50% 82%, 38% 98%, 30% 76%, 7% 79%, 17% 58%, 0% 43%, 22% 34%, 18% 12%, 39% 18% ); filter: drop-shadow(0 1.5px 2px rgba(20, 12, 4, 0.55)); } .card-power.is-buffed { background: radial-gradient(circle at 40% 30%, #c9ffd4 4%, #4fd07f 42%, #1f9e55); -webkit-text-stroke: 0.5px rgba(15, 60, 30, 0.85); text-shadow: 0 1px 1px rgba(15, 60, 30, 0.7); filter: drop-shadow(0 0 5px rgba(80, 220, 120, 0.75)); } .card-lg .card-power { width: 35px; height: 35px; font-size: 1.02rem; } .card-sm .card-power { width: 24px; height: 24px; font-size: 0.74rem; } .card-damage { position: absolute; top: 5%; right: 5%; z-index: 3; font-family: var(--font-display); color: #fff; background: linear-gradient(180deg, #e85c49, var(--red)); border: 1.5px solid rgba(120, 20, 10, 0.4); border-radius: 8px; padding: 1px 6px; font-size: 0.72rem; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3); } /* Food cards swap the golden frame for a warm terracotta one, ailments a cold violet one, so the three kinds read apart at a glance. */ .card-food { background: linear-gradient(165deg, #f3a67a, #e07f4f 55%, #cf6a3a); } .card-food .card-panel { border-color: #c96a3a; } .card-food .card-effect, .card-food .card-name { color: #8a3320; } .card-ailment .card-panel { border-color: #6f57b0; } .card-ailment .card-effect, .card-ailment .card-name { color: #45308a; } .card-check { position: absolute; top: -10px; right: -10px; background: radial-gradient(circle at 35% 28%, #ffe08a, var(--gold-deep)); color: var(--cocoa); font-weight: 900; border: 2px solid var(--cocoa); border-radius: 50%; width: 27px; height: 27px; display: grid; place-items: center; z-index: 2; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4); } .card-slot-empty { width: 132px; height: 180px; border: 2px dashed rgba(253, 243, 220, 0.22); border-radius: var(--card-radius); flex-shrink: 0; background: rgba(0, 0, 0, 0.12); box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.25); } /* ---------- shop ---------- */ .shop { display: flex; flex-direction: column; gap: 24px; } .shop-status { text-align: center; font-size: 1.2rem; font-weight: 900; min-height: 1.6em; } .status-hot { color: var(--gold); text-shadow: 0 1px 6px rgba(246, 201, 78, 0.4); animation: pulse 1.6s ease-in-out infinite; } /* The "it's your turn" prompt as a solid golden badge with light text, so it reads as a call to action rather than bare pulsing text. */ .shop-status-pill { display: inline-block; font-family: var(--font-display); font-size: 0.92em; color: #fff8e6; background: linear-gradient(180deg, var(--gold), var(--gold-deep)); padding: 0.22em 0.85em; border-radius: 999px; border: 2px solid rgba(255, 255, 255, 0.5); text-shadow: 0 1px 3px rgba(120, 78, 8, 0.5); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), inset 0 -4px 10px rgba(150, 100, 20, 0.3), 0 4px 14px rgba(0, 0, 0, 0.35); } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.62; } } .section-label { font-family: var(--font-display); font-size: 1.02rem; margin-bottom: 12px; color: var(--gold); letter-spacing: 0.02em; display: flex; align-items: baseline; gap: 4px; } .section-label .muted { font-family: var(--font-body); font-size: 0.82rem; font-weight: 700; } /* Recessed felt trays: a darker inset well with a stitched edge, so cards look like they're laid into the mat rather than floating on it. */ .shop-row-wrap, .deck-wrap { background: radial-gradient(ellipse 80% 120% 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; padding: 16px 18px 18px; box-shadow: var(--tray-inset), inset 0 0 0 1px rgba(246, 201, 78, 0.06); } /* Trumpets banked for the next battle, shown as a pill under the deck. */ .trumpet-indicator { margin: 12px auto 0; width: fit-content; font-family: var(--font-display); font-size: 1.05rem; padding: 5px 14px; border-radius: 999px; border: 1px solid rgba(246, 201, 78, 0.3); background: rgba(0, 0, 0, 0.28); color: var(--gold); } /* Bigfoot's shop-deck peek (Unicorn pack): a button and the revealed card. */ .peek-indicator { display: flex; flex-direction: column; align-items: center; gap: 8px; margin: 12px auto 0; width: fit-content; } /* Unicorn ailment cards: a cold violet frame (panel + text colours are set with the other card-kind overrides above). */ .card-ailment { background: linear-gradient(165deg, #b9a6e6, #9a83d9 55%, #7d64c6); } /* Ailment badges pinned to a battle unit. */ .ailment-badges { position: absolute; top: -8px; left: -8px; display: flex; gap: 3px; z-index: 5; } .ailment-badge { font-size: 0.8rem; font-weight: 800; line-height: 1; padding: 3px 5px; border-radius: 999px; border: 1px solid rgba(0, 0, 0, 0.35); background: rgba(20, 16, 40, 0.82); color: #fff; } .ailment-spooked { box-shadow: 0 0 6px rgba(150, 130, 255, 0.7); } .ailment-exposed { box-shadow: 0 0 6px rgba(255, 120, 90, 0.7); } /* Coins shown as big golden discs above the buy row. */ .shop-coins { display: flex; gap: 6px; align-items: center; justify-content: center; min-height: 40px; margin-bottom: 4px; } .coin-disc { font-size: 1.9rem; line-height: 1; filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.4)); animation: coin-pop 0.25s ease; transition: filter 200ms ease, opacity 200ms ease, transform 200ms ease; } /* A spent coin stays in place but greys out and fades. */ .coin-disc.is-spent { filter: grayscale(1) brightness(0.7); opacity: 0.4; transform: scale(0.88); animation: none; } @keyframes coin-pop { from { transform: scale(0.4); opacity: 0; } } .coin-empty { font-style: italic; font-size: 0.9rem; } /* "Your turn" flourish across the shop. */ .turn-banner { position: fixed; inset: 0; display: grid; place-items: center; pointer-events: none; z-index: 50; } /* Light text on a golden rounded-rect plate, so the flourish reads as a solid badge rather than bare text floating over the board. */ .turn-banner-pill { font-family: var(--font-display); font-size: clamp(2rem, 7vw, 4.5rem); color: #fff8e6; background: linear-gradient(180deg, var(--gold), var(--gold-deep)); padding: 0.28em 0.8em; border-radius: 22px; border: 3px solid rgba(255, 255, 255, 0.55); text-shadow: 0 2px 5px rgba(120, 78, 8, 0.55); box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.55), inset 0 -6px 14px rgba(150, 100, 20, 0.35), 0 14px 34px rgba(0, 0, 0, 0.5); animation: turn-flash 1.4s ease forwards; } @keyframes turn-flash { 0% { opacity: 0; transform: scale(0.6); } 20% { opacity: 1; transform: scale(1); } 75% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1.15); } } /* Pass button nudge when there's nothing left to buy. */ .btn-pass-hint { border-color: var(--gold); color: var(--gold); box-shadow: 0 0 0 1px var(--gold), 0 0 14px rgba(246, 201, 78, 0.5); animation: pass-pulse 1.4s ease-in-out infinite; } @keyframes pass-pulse { 50% { box-shadow: 0 0 0 1px var(--gold), 0 0 22px rgba(246, 201, 78, 0.8); } } /* Trigger explainer under the hover magnifier. */ .trigger-gloss { margin-top: 8px; max-width: 200px; background: rgba(0, 0, 0, 0.82); border: 1px solid rgba(246, 201, 78, 0.35); border-radius: 10px; padding: 8px 10px; font-size: 0.78rem; line-height: 1.35; color: var(--parchment, #eee); } .trigger-gloss-line + .trigger-gloss-line { margin-top: 4px; } .trigger-gloss strong { color: var(--gold); } /* Set-aside Avocado tokens (Golden pack): a slim tray with a toggle pill. */ .avocado-zone { background: rgba(0, 0, 0, 0.24); border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 18px; padding: 12px 18px 14px; box-shadow: var(--tray-inset), inset 0 0 0 1px rgba(246, 201, 78, 0.06); } .avocado-token { font-family: var(--font-display); font-size: 1.05rem; padding: 6px 14px; border-radius: 999px; border: 1px solid rgba(246, 201, 78, 0.3); background: rgba(0, 0, 0, 0.28); color: var(--gold); cursor: pointer; transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease; } .avocado-token:hover:not(:disabled) { transform: translateY(-1px); } .avocado-token.is-active { border-color: var(--gold); box-shadow: 0 0 10px rgba(246, 201, 78, 0.45); } .avocado-token:disabled { opacity: 0.6; cursor: default; } .shop-row, .deck-row { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; /* Positioning context for the exit "ghost" cards, which are pinned to where the departing card sat while they shrink away (see .card-ghost). */ position: relative; } /* A thin wrapper around each animated card. The FLIP animator moves and flips this element (not the .card inside), so the card's own hover/selected lift transforms stay independent. backface-visibility hides the card mid-flip so a dealt-in card reads as turning face-up rather than showing mirrored text. */ .card-cell { display: flex; flex: 0 0 auto; backface-visibility: hidden; } /* An exiting card: pinned (inline styles) where it last sat, shrinking away as its former neighbors glide into the gap. */ .card-ghost { z-index: 3; animation: card-ghost-out 240ms cubic-bezier(0.4, 0, 0.7, 0.4) forwards; } @keyframes card-ghost-out { to { opacity: 0; transform: scale(0.62) translateY(-6px); } } .deck-empty { text-align: center; padding: 22px 0; } .actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; } /* ---------- modal ---------- */ .modal-backdrop { position: fixed; inset: 0; background: rgba(10, 20, 14, 0.72); backdrop-filter: blur(3px); display: grid; place-items: center; z-index: 50; animation: result-in 200ms ease; } .modal { background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(255, 214, 138, 0.1), transparent 70%), linear-gradient(180deg, var(--felt-600), var(--felt-800)); border: 1px solid rgba(253, 243, 220, 0.2); border-radius: 20px; padding: 28px; text-align: center; display: flex; flex-direction: column; gap: 20px; max-width: 92vw; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 24px 60px -12px rgba(0, 0, 0, 0.7); } .modal-cards { display: flex; gap: 20px; justify-content: center; } /* ---------- arrange ---------- */ .arrange { display: flex; flex-direction: column; gap: 20px; } /* 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; flex-direction: column; align-items: center; gap: 12px; background: 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; padding: 16px 18px 20px; min-height: 224px; box-shadow: var(--tray-inset); } /* 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 { font-family: var(--font-display); color: var(--gold); font-size: 0.95rem; letter-spacing: 0.04em; opacity: 0.85; padding-bottom: 2px; } /* Each row is centered in the tray so the card sits under the "first to fight" marker; the reorder controls hang absolutely to its left (see below) and so don't push the card off-center. `.arrange-card` is the box the FLIP animator measures — it stays in its natural slot while a drag lifts its inner content. */ .arrange-card { position: relative; display: flex; justify-content: center; } /* The lifted card floats above its neighbors while being dragged. The z-index sits on the inner wrapper (which carries the follow transform) so it wins over sibling rows without disturbing the measured `.arrange-card` box. */ .arrange-card.is-dragging { z-index: 5; } /* Inner wrapper that actually moves during a drag. Transforming this rather than `.arrange-card` keeps the FLIP animator's measurements honest. */ .arrange-drag { position: relative; display: flex; align-items: center; } /* When released, glide from under the finger down into the resting slot. */ .arrange-drag.is-dropping { transition: transform 0.18s ease-out; } /* Reorder controls: the grip handle on top, the up/down arrows below. Hung to the right of the card so the card itself stays centered under the marker. */ .arrange-controls { position: absolute; left: 100%; top: 50%; transform: translateY(-50%); margin-left: 12px; display: flex; flex-direction: column; align-items: center; gap: 8px; } /* Grab this to drag-reorder. `touch-action: none` keeps the browser from scrolling the page while a touch drag is in progress; the pointer handlers in ArrangePhase do the reordering. */ .arrange-handle { touch-action: none; cursor: grab; user-select: none; -webkit-user-select: none; display: flex; align-items: center; justify-content: center; min-width: 40px; min-height: 40px; font-size: 1.5rem; line-height: 1; color: var(--gold); opacity: 0.75; border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 10px; background: rgba(0, 0, 0, 0.2); } .arrange-handle:active { cursor: grabbing; opacity: 1; } .arrange-arrows { display: flex; flex-direction: column; gap: 6px; } /* ---------- battle ---------- */ .battle { /* Playback-speed multiplier, set inline from the speed picker. Every battle 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; display: flex; flex-direction: column; gap: 18px; } /* Playback-speed toggle: one button that cycles through the speed options on each press. A fixed min-width keeps the controls row from reflowing as the label swaps between 0.5× / 1× / 1.5× / 2× / 3×. */ .battle-speed-toggle { margin-left: 4px; min-width: 3.6em; font-variant-numeric: tabular-nums; } .battle-header { display: flex; justify-content: center; align-items: center; gap: 16px; flex-wrap: wrap; } .battle-header h2 { font-size: 1.5rem; color: var(--gold); text-shadow: 0 2px 3px rgba(0, 0, 0, 0.4); } .battle-controls { display: flex; align-items: center; gap: 6px; background: rgba(0, 0, 0, 0.22); border-radius: 12px; padding: 5px 8px; box-shadow: var(--tray-inset); } .battle-step { font-family: var(--font-display); font-size: 0.85rem; color: rgba(253, 243, 220, 0.7); min-width: 3.5em; text-align: center; } .battle-names { display: flex; justify-content: center; gap: 18px; font-weight: 800; font-size: 1.05rem; } .battle-vs { font-family: var(--font-display); color: var(--gold); } /* The battlefield is the lit heart of the table — a spotlit arena inset into the felt, with a faint centre line where the two sides meet. */ .battlefield { position: relative; display: flex; flex-direction: column; align-items: stretch; justify-content: center; gap: 6px; background: 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; /* 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: var(--tray-inset), inset 0 0 60px rgba(0, 0, 0, 0.25); } .battle-center { position: relative; font-size: 1.7rem; flex-shrink: 0; display: grid; place-items: center; min-height: 2.4rem; } /* A soft seam across the middle of the arena, where the two sides clash. */ .battle-center::before { content: ''; position: absolute; left: 8%; right: 8%; height: 2px; background: linear-gradient(90deg, transparent, rgba(246, 201, 78, 0.25), transparent); } .battle-center-bolt { opacity: 0.5; 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; flex-direction: column; align-items: center; gap: 10px; min-width: 0; } /* Centres the deck stack under (you) / over (opponent) the pet. */ .battle-deck { display: flex; justify-content: center; } /* --- deck stacks --- */ .stackpile { position: relative; flex-shrink: 0; } /* 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; border-radius: var(--card-radius); border: 2px solid var(--cocoa); background: repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.06) 0 6px, transparent 6px 12px), repeating-linear-gradient(-45deg, rgba(0, 0, 0, 0.12) 0 6px, transparent 6px 12px), linear-gradient(160deg, #c25f31, #a2451f); box-shadow: inset 0 0 0 3px rgba(255, 230, 200, 0.18), inset 0 2px 4px rgba(255, 255, 255, 0.15), 3px 3px 0 rgba(0, 0, 0, 0.22), 6px 6px 0 rgba(0, 0, 0, 0.12), 0 8px 12px rgba(0, 0, 0, 0.35); display: grid; place-items: center; } .card-back-count { font-family: var(--font-display); font-size: 1.3rem; color: var(--cream); background: rgba(0, 0, 0, 0.4); border-radius: 50%; width: 40px; height: 40px; display: grid; place-items: center; box-shadow: inset 0 0 0 2px rgba(255, 230, 200, 0.25); } .stack-empty { width: 84px; height: 116px; } @keyframes summon-pop { 0% { opacity: 0; transform: translateY(18px) scale(0.6); } 30% { opacity: 1; transform: translateY(-14px) scale(1.05); } 80% { opacity: 1; transform: translateY(-10px) scale(1); } 100% { opacity: 0; transform: translateY(0) scale(0.7); } } .summon-pop { position: absolute; top: -30px; left: 50%; margin-left: -42px; z-index: 6; animation: summon-pop calc(720ms / var(--battle-speed)) ease forwards; pointer-events: none; } /* 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: translateY(-70px) scale(0.5); } 25% { opacity: 1; transform: translateY(-30px) scale(1.05); } 70% { opacity: 1; transform: translateY(4px) scale(1); } 100% { opacity: 0; transform: translateY(14px) scale(0.72); } } @keyframes summon-to-top { 0% { opacity: 0; transform: translateY(70px) scale(0.5); } 25% { opacity: 1; transform: translateY(30px) scale(1.05); } 70% { opacity: 1; transform: translateY(-4px) scale(1); } 100% { opacity: 0; transform: translateY(-14px) scale(0.72); } } /* 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-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. */ @keyframes spawn-fly { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.4) rotate(-10deg); } 20% { opacity: 1; transform: translate(-50%, -85%) scale(1.35) rotate(4deg); } 100% { opacity: 0; transform: translate(-50%, -165%) scale(1) rotate(0); } } .spawn-flyer { position: fixed; z-index: 60; font-size: 2rem; pointer-events: none; animation: spawn-fly 760ms ease-out forwards; filter: drop-shadow(0 3px 4px rgba(0, 0, 0, 0.5)); } .spawn-flyer-img { display: block; width: 1.5em; height: 1.5em; object-fit: contain; } /* A bought card in flight from the shop slot to its landing spot in your hand. The element is positioned over its destination slot (fixed) and inverted back to where it was clicked (--dx/--dy/--s), then plays that offset out — a FLIP from shop to hand. Its deck slot stays empty (.is-arriving) until it lands. */ @keyframes buy-fly { from { transform: translate(var(--dx), var(--dy)) scale(var(--s)); } to { transform: translate(0, 0) scale(1); } } .buy-flyer { position: fixed; z-index: 60; transform-origin: top left; animation: buy-fly 380ms cubic-bezier(0.5, 0.02, 0.3, 1) forwards; pointer-events: none; filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.5)); } .card-cell.is-arriving { opacity: 0; } /* --- the pet in play --- */ .battle-unit-zone { position: relative; min-width: 100px; min-height: 150px; display: grid; place-items: center; } /* 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; right: calc(100% + 6px); top: 50%; transform: translateY(-50%); display: flex; gap: 6px; z-index: 5; } /* 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; left: calc(100% - 6px); top: 50%; transform: translateY(-50%); display: flex; flex-direction: row; align-items: center; z-index: 4; } .food-fan-card { position: relative; } .food-fan-card:not(:first-child) { /* 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 fade out over their release step rather than vanishing instantly. The step lasts 500ms (EVENT_MS.release), so the 400ms exit finishes before unmount. */ @keyframes card-leave { to { opacity: 0; transform: scale(0.55) translateY(-8px); } } .setaside-card.is-leaving, .food-fan-card.is-leaving { animation: card-leave calc(300ms / var(--battle-speed)) cubic-bezier(0.4, 0, 0.7, 0.4) forwards; pointer-events: none; } /* 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(-30px, 70px) rotate(-12deg) scale(0.6); } to { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } } @keyframes food-in-top { from { opacity: 0; transform: translate(-30px, -70px) rotate(12deg) scale(0.6); } to { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } } .food-fan-card.food-in-bottom { animation: food-in-bottom calc(320ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1) backwards; } .food-fan-card.food-in-top { animation: food-in-top calc(320ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1) backwards; } /* 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: translateX(120px) scale(1.2); } 55% { opacity: 1; } to { opacity: 1; transform: translateX(0) scale(1); } } .setaside-card.setaside-in { animation: setaside-in calc(440ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1) backwards; } .battle-unit { position: relative; flex-shrink: 0; } .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; } /* Battle cards are short, so give the ability text more of the card by shrinking the decorative art; useFitText then keeps the text at a larger size before it has to shrink to fit. */ .battle-unit .card-art { font-size: 1.95rem; } /* A pet flips face-up off its deck: it starts over the stack (outer edge) still 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: translateY(108px) perspective(760px) rotateX(-92deg) scale(0.92); } 45% { opacity: 1; } to { opacity: 1; transform: translateY(0) perspective(760px) rotateX(0deg) scale(1); } } @keyframes unit-reveal-top { from { opacity: 0; transform: translateY(-108px) perspective(760px) rotateX(92deg) scale(0.92); } 45% { opacity: 1; } to { opacity: 1; transform: translateY(0) perspective(760px) rotateX(0deg) scale(1); } } .battle-unit.unit-reveal-bottom { transform-origin: center bottom; animation: unit-reveal-bottom calc(380ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1); } .battle-unit.unit-reveal-top { transform-origin: center top; animation: unit-reveal-top calc(380ms / var(--battle-speed)) cubic-bezier(0.2, 0.8, 0.3, 1); } /* --- rock dice --- */ /* 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; 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; } .droll { position: relative; width: 34px; height: 34px; border-radius: 7px; box-sizing: border-box; border: 2px solid var(--cocoa); background: linear-gradient(145deg, var(--cream), var(--cream-dark)); box-shadow: 0 3px 5px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.5); } .droll.is-rolling { animation: droll-shake calc(150ms / var(--battle-speed)) linear infinite; animation-delay: calc(var(--i) * 45ms / var(--battle-speed)); } .droll.is-settled { animation: droll-settle calc(240ms / var(--battle-speed)) cubic-bezier(0.2, 0.85, 0.3, 1) both; animation-delay: calc(var(--i) * 55ms / var(--battle-speed)); } @keyframes droll-shake { 0% { transform: rotate(-9deg) translateY(0) scale(1.06); } 25% { transform: rotate(7deg) translateY(-2px) scale(1.09); } 50% { transform: rotate(-6deg) translateY(1px) scale(1.05); } 75% { transform: rotate(8deg) translateY(-1px) scale(1.08); } 100% { transform: rotate(-9deg) translateY(0) scale(1.06); } } @keyframes droll-settle { 0% { transform: scale(1.22) rotate(-5deg); } 60% { transform: scale(0.95) rotate(1deg); } 100% { transform: scale(1) rotate(0); } } /* A rock icon: a lumpy gray boulder. */ .droll-rock { position: absolute; width: 14px; height: 14px; background: radial-gradient(circle at 35% 28%, #838a90, #34373c); clip-path: polygon(22% 6%, 60% 0%, 90% 24%, 100% 58%, 82% 96%, 42% 100%, 8% 82%, 0% 40%); } .droll-n-1 .droll-rock { top: 50%; left: 50%; transform: translate(-50%, -50%); } .droll-n-2 .droll-rock { width: 12px; height: 12px; } .droll-n-2 .droll-rock:nth-child(1) { top: 30%; left: 30%; transform: translate(-50%, -50%); } .droll-n-2 .droll-rock:nth-child(2) { top: 70%; left: 70%; transform: translate(-50%, -50%); } @keyframes fx-pop { 0% { opacity: 0; transform: translate(-50%, 6px) scale(0.6); } 30% { opacity: 1; transform: translate(-50%, -14px) scale(1.2); } 100% { opacity: 0; transform: translate(-50%, -34px) scale(1); } } .fx-pop { position: absolute; top: 0; left: 50%; font-family: var(--font-display); font-size: 1rem; color: #7be495; text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5); animation: fx-pop calc(760ms / var(--battle-speed)) ease forwards; pointer-events: none; z-index: 5; white-space: nowrap; } .mill-pop .card { filter: grayscale(1); } /* You (bottom) lunge up into the seam; the opponent (top) lunges down. A quick crouch of anticipation, an explosive lunge, then an impact recoil with a scale punch and a little shake before settling — reads as a real collision rather than a gentle bump. */ @keyframes clash-bottom { 0% { transform: translateY(0) scale(1) rotate(0); } 12% { transform: translateY(10px) scale(0.95); } 40% { transform: translateY(-34px) scale(1.09) rotate(2.5deg); } 52% { transform: translateY(-21px) scale(0.98) rotate(-2.5deg); } 67% { transform: translateY(8px) rotate(1.5deg); } 83% { transform: translateY(-3px) rotate(-0.5deg); } 100% { transform: translateY(0) scale(1) rotate(0); } } @keyframes clash-top { 0% { transform: translateY(0) scale(1) rotate(0); } 12% { transform: translateY(-10px) scale(0.95); } 40% { transform: translateY(34px) scale(1.09) rotate(-2.5deg); } 52% { transform: translateY(21px) scale(0.98) rotate(2.5deg); } 67% { transform: translateY(-8px) rotate(-1.5deg); } 83% { transform: translateY(3px) rotate(0.5deg); } 100% { transform: translateY(0) scale(1) rotate(0); } } .battle-unit.clash-bottom { animation: clash-bottom calc(460ms / var(--battle-speed)) cubic-bezier(0.36, 0.7, 0.3, 1); } .battle-unit.clash-top { animation: clash-top calc(460ms / var(--battle-speed)) cubic-bezier(0.36, 0.7, 0.3, 1); } /* When the two pets meet, the centre seam cracks: the bolt flares and a bright shockwave ring bursts outward from the collision point. */ @keyframes clash-bolt { 0% { transform: scale(0.5) rotate(-12deg); opacity: 0.5; filter: drop-shadow(0 0 6px rgba(246, 201, 78, 0.5)); } 30% { transform: scale(1.7) rotate(6deg); opacity: 1; filter: drop-shadow(0 0 18px rgba(255, 236, 150, 0.95)); } 100% { transform: scale(1) rotate(0); opacity: 0.5; filter: drop-shadow(0 0 8px rgba(246, 201, 78, 0.5)); } } @keyframes clash-shock { 0% { transform: translate(-50%, -50%) scale(0.2); opacity: 0.85; } 100% { transform: translate(-50%, -50%) scale(2.6); opacity: 0; } } .battle-center-clash .battle-center-bolt { animation: clash-bolt calc(420ms / var(--battle-speed)) cubic-bezier(0.3, 0.8, 0.3, 1); } .battle-center-clash::after { content: ''; position: absolute; left: 50%; top: 50%; width: 46px; height: 46px; border-radius: 50%; border: 3px solid rgba(255, 232, 150, 0.9); box-shadow: 0 0 14px rgba(255, 224, 130, 0.7); animation: clash-shock calc(420ms / var(--battle-speed)) ease-out forwards; pointer-events: none; } @keyframes dying { to { opacity: 0; transform: translateY(30px) rotate(12deg) scale(0.85); } } .battle-unit.unit-dying { animation: dying calc(480ms / var(--battle-speed)) ease calc(360ms / var(--battle-speed)) forwards; } @keyframes damage-pop { 0% { opacity: 0; transform: translate(-50%, 0) scale(0.6); } 25% { opacity: 1; transform: translate(-50%, -16px) scale(1.15); } 100% { opacity: 0; transform: translate(-50%, -44px) scale(1); } } .damage-pop { position: absolute; top: 0; left: 50%; font-family: var(--font-display); font-size: 1.4rem; color: #ff6b52; text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5); animation: damage-pop calc(700ms / var(--battle-speed)) ease calc(160ms / var(--battle-speed)) forwards; opacity: 0; pointer-events: none; z-index: 5; } /* The end-of-battle result dialog: a centered modal so it's always fully on screen (even on a phone where the arena fills the viewport). */ .battle-result-modal { gap: 14px; min-width: min(320px, 86vw); } @keyframes result-in { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } } .battle-result-title { font-family: var(--font-display); font-size: 2.6rem; text-shadow: 0 3px 0 rgba(0, 0, 0, 0.3), 0 6px 14px rgba(0, 0, 0, 0.4); } .battle-result-modal.is-win .battle-result-title { color: var(--gold); } .battle-result-modal.is-loss .battle-result-title { color: #ff8f7a; } .battle-result-modal.is-draw .battle-result-title { color: #6fd6c2; } .battle-result-sub { font-size: 1.1rem; font-weight: 700; } /* The toolbar's "advance the round" button stands out from the ghost controls. */ .battle-next { margin-left: 2px; } /* ---------- game over ---------- */ .gameover { min-height: 60vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; text-align: center; } .gameover-emoji { font-size: 4.4rem; filter: drop-shadow(0 8px 10px rgba(0, 0, 0, 0.45)); animation: float 3s ease-in-out infinite; } .gameover-title { font-size: 3.2rem; color: var(--gold); text-shadow: 0 2px 0 var(--gold-deep), 0 5px 0 rgba(90, 58, 32, 0.5), 0 8px 16px rgba(0, 0, 0, 0.4); } .gameover-scores { display: flex; flex-direction: column; gap: 8px; background: rgba(0, 0, 0, 0.24); border-radius: 16px; padding: 20px 28px; min-width: min(360px, 90vw); box-shadow: var(--tray-inset); } .score-line { display: flex; justify-content: space-between; gap: 24px; font-size: 1.1rem; font-weight: 700; padding: 4px 0; } .score-line.is-you .score-name { color: var(--gold); font-weight: 900; } /* ---------- toasts & banners ---------- */ .toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: linear-gradient(180deg, #e85c49, var(--red)); color: #fff; font-weight: 700; border-radius: 12px; padding: 11px 22px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 8px 18px rgba(0, 0, 0, 0.45); animation: result-in 200ms ease; z-index: 100; } .banner { position: fixed; top: 66px; left: 50%; transform: translateX(-50%); border-radius: 10px; padding: 7px 18px; font-size: 0.9rem; font-weight: 700; z-index: 90; box-shadow: 0 6px 14px rgba(0, 0, 0, 0.4); } .banner-warn { background: rgba(217, 74, 56, 0.92); } /* ============================================================ Phones, portrait — fit the whole game on one screen. A modern smartphone held upright (≤ 600px wide) locks the app to the viewport height so the page itself never scrolls: the topbar is a slim fixed rail and the play area fills the rest. Cards, trays, and type all shrink so a round of shopping or a battle reads at a glance. Card rows scroll sideways when a full six-pet deck outgrows the width, so the vertical layout stays put. The one screen allowed to grow past the viewport is Arrange — you can field a lot of pets, so it scrolls. ============================================================ */ @media (max-width: 600px) { /* --- app shell: lock to the viewport; only the play area scrolls --- */ html, body, #root { height: 100%; min-height: 0; } body { overflow: hidden; } .table { height: 100vh; /* fallback for browsers without dvh */ height: 100dvh; min-height: 0; overflow: hidden; } .topbar { flex-shrink: 0; } .table-body { flex: 1; min-height: 0; overflow: hidden; /* Stretch the play field to the body's full height (overriding the base flex-start) so its own overflow-y can scroll — otherwise the column is only as tall as its content and anything past the fold (e.g. the end-of-battle "Next round" button) gets clipped instead of scrolled. */ align-items: stretch; } /* The play field fills the space under the rail and scrolls internally when a phase runs long — the end-of-battle result, or an Arrange line with lots of pets. Everything else is sized to fit, so the scrollbar rarely appears. */ .table-main { flex: 1; min-height: 0; max-width: none; padding: 10px 10px 14px; overflow-y: auto; -webkit-overflow-scrolling: touch; } /* --- topbar: a slim rail --- */ .topbar { gap: 5px 8px; padding: 5px 9px; } /* Drop the wordmark entirely on a phone — the die, the player tags, and the ⋯ menu are all that's needed, and losing it helps the rail stay one row. */ .topbar-brand { display: none; } .topbar-die { width: 23px; height: 23px; } /* Let the tags pack tight; the ⋯ menu is pushed to the right by margin-auto. */ .topbar-players { flex: 0 1 auto; gap: 6px; } .topbar-menu-btn { padding: 4px 10px; } .topbar-round { font-size: 0.82rem; } .topbar-player { padding: 3px 9px; font-size: 0.8rem; gap: 5px; } .topbar-code { font-size: 0.8rem; letter-spacing: 0.18em; padding: 3px 7px 3px calc(7px + 0.18em); } .chip { font-size: 0.76rem; } /* --- cards: printed small --- */ .card { width: 80px; height: 126px; padding: 4px; gap: 3px; } .card-lg { width: 80px; height: 132px; } .card-sm { width: 60px; height: 82px; } .card-power { width: 23px; height: 23px; font-size: 0.72rem; } .card-lg .card-power { width: 25px; height: 25px; font-size: 0.78rem; } .card-name { font-size: 0.62rem; } .card-lg .card-name { font-size: 0.66rem; } .card-effect { font-size: 0.55rem; } .card-lg .card-effect { font-size: 0.57rem; } .card-art { font-size: 1.7rem; } .card-lg .card-art { font-size: 1.85rem; } /* In-tray cards give more of their height to the rule panel so the ability text has room to breathe (the magnified view keeps the roomy 58% scene). */ .shop-row .card-scene, .deck-row .card-scene { flex-basis: 49%; } /* A lift on tap would poke out of a sideways-scrolling strip and trip its scrollbar; keep the feedback small on touch. */ .card.is-clickable:hover { transform: translateY(-3px); } /* The trays scroll sideways, which clips vertical overflow — so the selected card's lift, gold outline, and check badge would get cut off at the top. Give the rows headroom and tuck the badge just inside the card corner. */ .card.is-selected { transform: translateY(-3px); } .card-check { top: -3px; right: -3px; width: 22px; height: 22px; } /* --- shop: compact trays, one horizontal row each --- */ .shop { gap: 10px; } .shop-status { font-size: 0.95rem; min-height: 1.3em; } .shop-coins { min-height: 26px; margin-bottom: 0; gap: 4px; } .coin-disc { font-size: 1.35rem; } .section-label { font-size: 0.82rem; margin-bottom: 6px; } .section-label .muted { font-size: 0.72rem; } .shop-row-wrap, .deck-wrap { padding: 5px 6px 7px; border-radius: 14px; } /* A full six-pet deck (or the shop row on a very narrow phone) scrolls sideways; each tray keeps its height so nothing else shifts. A row that isn't wide enough to scroll stays centered. */ .shop-row, .deck-row { flex-wrap: nowrap; justify-content: flex-start; gap: 6px; overflow-x: auto; -webkit-overflow-scrolling: touch; /* Headroom for the selected card's lift + outline + check badge, which the sideways scroll would otherwise clip. */ padding: 12px 6px 6px; scrollbar-width: thin; } .shop-row { justify-content: center; } .hint { font-size: 0.76rem; } .actions { gap: 8px; } .actions .btn { padding: 9px 13px; font-size: 0.86rem; } /* Tap-to-magnify overlay: the card shown big and centered with its actions. Overrides the shrunk card sizes/fonts — reading the card is the point. */ .card-focus { display: flex; flex-direction: column; align-items: center; gap: 16px; max-width: 92vw; } .card-focus .card { width: min(66vw, 230px); height: min(58vh, 312px); } /* The magnified card is big, so restore full-size art, power badge, and text (the phone .card-lg overrides had shrunk them for the tiny in-tray card). */ .card-focus .card-art { font-size: 4rem; } .card-focus .card-power { width: 44px; height: 44px; font-size: 1.25rem; } .card-focus .card-damage { font-size: 1rem; } .card-focus .card-name { font-size: 1.05rem; } .card-focus .card-effect { font-size: 0.72rem; } .card-focus-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; } /* --- avocado / trumpet / mana trays --- */ .avocado-zone { padding: 8px 10px 10px; } .trumpet-indicator { margin-top: 8px; font-size: 0.9rem; padding: 4px 12px; } /* --- arrange: the one screen that may scroll past the viewport --- */ .arrange { gap: 10px; } .arrange-col { min-height: 0; padding: 12px 12px 14px; gap: 10px; } .arrange-marker { font-size: 0.85rem; } .arrange-controls { margin-left: 8px; } .arrange-handle { min-width: 34px; min-height: 34px; font-size: 1.25rem; } /* --- battle: fit both sides and the clash between the rails --- */ .battle { gap: 8px; } .battle-header { gap: 8px; } .battle-header h2 { font-size: 1.1rem; } .battle-controls { flex-wrap: wrap; justify-content: center; gap: 5px; padding: 4px 6px; } .battle-speed-toggle { margin-left: 2px; } .battle-names { font-size: 0.9rem; gap: 10px; } .battlefield { padding: 8px 8px; min-height: 0; gap: 3px; border-radius: 16px; } .battle-center { font-size: 1.2rem; min-height: 1.5rem; } .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 the full card). The scene grows to fill the space the panel gives up. */ .battle-unit .card-scene { flex: 1 1 auto; } .battle-unit .card-panel { flex: 0 0 auto; } .battle-unit .card-effect, .battle-unit .card-suit, .battle-unit .card-die, .battle-unit .card-panel-slot { display: none; } .battle-unit .card-panel-head { grid-template-columns: 1fr; border-bottom: none; padding-bottom: 0; } .battle-unit .card-art { font-size: 2.1rem; } .card-back { width: 56px; height: 78px; } .card-back-count { width: 28px; height: 28px; font-size: 0.95rem; } /* Apples fan tighter so a big stack doesn't shove the pet off-centre. */ .food-fan-card:not(:first-child) { margin-left: -50px; } .battle-result-title { font-size: 1.7rem; } .battle-result-modal .btn-big { font-size: 1.05rem; padding: 11px 22px; } /* --- modals & game over --- */ .modal { padding: 18px 16px; } .gameover { gap: 14px; min-height: 0; padding: 16px 0; } .gameover-emoji { font-size: 3rem; } .gameover-title { font-size: 2.2rem; } .gameover-scores { padding: 14px 18px; } } /* --- debug panel (server DEBUG mode) --- */ .debug-panel { position: fixed; top: 66px; right: 0; z-index: 120; display: flex; align-items: flex-start; } .debug-toggle { background: #7a1f1f; color: var(--cream); border: 2px solid var(--cocoa); border-right: none; /* The button is flipped 180° (below) to read text upward, which also flips these corners — so round the right pair here to land rounded corners on the inner (screen-facing) edge after the rotation. */ border-radius: 0 8px 8px 0; font-weight: 800; font-size: 0.8rem; padding: 8px 10px; cursor: pointer; white-space: nowrap; writing-mode: vertical-rl; transform: rotate(180deg); } .debug-panel.is-open .debug-toggle { writing-mode: horizontal-tb; transform: none; border-radius: 8px 0 0 0; } .debug-body { width: min(340px, 80vw); max-height: calc(100vh - 80px); overflow-y: auto; background: rgba(18, 53, 31, 0.97); border: 2px solid var(--cocoa); border-radius: 8px 0 0 8px; padding: 12px; box-shadow: -6px 6px 18px rgba(0, 0, 0, 0.45); } .debug-head { color: var(--gold); font-weight: 800; margin-bottom: 8px; } .debug-tier-label { color: var(--cream); font-size: 0.75rem; font-weight: 700; opacity: 0.8; margin: 8px 0 4px; } .debug-grid { display: flex; flex-wrap: wrap; gap: 6px; } /* --- hover magnifier --- */ .card-magnify { position: fixed; z-index: 200; pointer-events: none; transform: scale(1.5); transform-origin: top left; filter: drop-shadow(0 12px 24px rgba(0, 0, 0, 0.55)); animation: magnify-in 90ms ease-out; } /* Right-anchored variant: scale outward from the top-right so the preview grows leftward and stays on-screen when hovered near the right edge. */ .card-magnify-flip { transform-origin: top right; } /* The magnifier exists to be read, so let the preview card grow to fit its full effect text instead of clipping like a fixed-height card. */ .card-magnify .card { height: auto; min-height: 180px; padding-bottom: 10px; } /* Keep the illustration a healthy size while the panel grows to fit the full, unclipped ability text. */ .card-magnify .card-scene { flex: 0 0 auto; height: 104px; } .card-magnify .card-effect { flex: 0 0 auto; overflow: visible; align-items: center; margin-bottom: 6px; } @keyframes magnify-in { from { opacity: 0; transform: scale(1.1); } to { opacity: 1; transform: scale(1.5); } } /* --- deck peek (battle) --- */ .stackpile.peekable { cursor: help; } /* Portaled to and anchored to the hovered stack's viewport rect (see BattlePhase) so it floats over the whole window instead of being clipped by the battlefield's overflow. left/right/bottom come from inline styles. */ .deck-peek { position: fixed; z-index: 60; width: max-content; max-width: 60vw; background: rgba(18, 53, 31, 0.98); border: 1px solid var(--cocoa); border-radius: 12px; padding: 10px 12px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 10px 24px rgba(0, 0, 0, 0.55); pointer-events: none; } .deck-peek-label { color: var(--gold); font-size: 0.72rem; font-weight: 800; margin-bottom: 6px; } .deck-peek-cards { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; max-width: min(60vw, 560px); } /* 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; } /* Compact echo of the arrangement phase's "⚔️ first" indicator. */ .deck-peek-first { display: flex; align-items: center; align-self: stretch; font-family: var(--font-display); font-size: 0.68rem; color: var(--gold); white-space: nowrap; padding: 0 2px; } /* Deck peek shown as a centered modal (shop-phase rival deck): cap the width so a wide lineup wraps instead of running off-screen. */ .deck-peek-modal { padding: 20px; gap: 12px; max-width: min(92vw, 720px); } .deck-peek-modal .deck-peek-cards { max-width: 100%; justify-content: flex-start; } .deck-peek-modal .deck-peek-label { font-size: 0.85rem; }