Files
2026-05-13 17:35:46 +08:00

736 lines
21 KiB
CSS

/* =========================================================================
Texas Hold X Replay — Pixel-art skin & responsive layout
-------------------------------------------------------------------------
Design goals:
1. Stage zone (table + seats + animations) is visually isolated from the
interaction zone (controls + event log). Each zone has independent
overflow rules so speech bubbles never get clipped.
2. Pixel-art aesthetic: hard edges, stepped shadows (no blur), 8-bit
palette, monospace typography.
3. Three responsive breakpoints (desktop 3-col → tablet 2-col → mobile
stacked) — see media queries at the bottom of this file.
========================================================================= */
:root {
color-scheme: dark;
/* Palette — keep limited and high-contrast for that 8-bit look. */
--ink: #f7efd2;
--ink-dim: #c8b98a;
--muted: #8e8466;
--panel: #221a17;
--panel-2: #2d231f;
--panel-3: #3a2c25;
--line: #5d4638;
--line-soft: #3b2c25;
/* Felt greens. */
--felt: #1d8a5f;
--felt-dark: #0c4a37;
--felt-light: #4ec089;
--felt-rail: #6c3a20;
--felt-rail-dark: #3a1d10;
/* Accents. */
--gold: #f0b64d;
--gold-dark: #b87a1f;
--red: #e24b4b;
--blue: #53a6de;
--green: #63cb73;
--purple: #b773d3;
--shadow: rgba(8, 6, 5, 0.55);
--pixel: 3px;
/* Seat sizing scales with the stage width via container query fallback
(clamp on viewport). */
--seat-size: clamp(96px, 13vw, 138px);
--avatar-size: 44px;
}
* { box-sizing: border-box; }
body {
min-height: 100vh;
margin: 0;
color: var(--ink);
/* Pixel-art monospace stack — keeps numerals crisp & blocky. */
font-family: "Courier New", "Lucida Console", "Press Start 2P", monospace;
font-size: 13px;
letter-spacing: 0.2px;
background:
/* Tiny noise made from offset diagonals for that CRT pixel-grid feel. */
linear-gradient(45deg, rgba(255,255,255,0.025) 25%, transparent 25%) 0 0 / 8px 8px,
linear-gradient(135deg, rgba(255,255,255,0.025) 25%, transparent 25%) 0 0 / 8px 8px,
radial-gradient(ellipse at 50% 0%, #2a1f1a 0%, #14100e 60%, #0d0a08 100%);
image-rendering: pixelated;
-webkit-font-smoothing: none;
-moz-osx-font-smoothing: grayscale;
}
button, input, select { font: inherit; }
/* ---------- Buttons (chunky pixel "press" feel) ---------- */
button, .file-btn {
min-height: 42px;
border: var(--pixel) solid #17100d;
color: var(--ink);
background: #3a2b24;
box-shadow:
inset -3px -3px 0 rgba(0,0,0,0.35),
inset 3px 3px 0 rgba(255,255,255,0.08),
0 4px 0 #120d0b;
cursor: pointer;
transition: transform 90ms steps(2), filter 90ms steps(2);
}
button:hover, .file-btn:hover { filter: brightness(1.12); }
button:active, .file-btn:active {
transform: translateY(3px);
box-shadow:
inset -3px -3px 0 rgba(0,0,0,0.35),
inset 3px 3px 0 rgba(255,255,255,0.08),
0 1px 0 #120d0b;
}
button:disabled { opacity: 0.4; cursor: not-allowed; }
input, select {
width: 100%;
min-height: 40px;
border: 2px solid var(--line);
border-radius: 0;
color: var(--ink);
background: #15110f;
padding: 9px 10px;
outline: none;
}
input:focus, select:focus { border-color: var(--gold); }
/* ---------- Shell / Topbar ---------- */
.app-shell {
width: min(1640px, 100%);
margin: 0 auto;
padding: 18px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 14px 16px;
border: var(--pixel) solid #49382e;
background:
linear-gradient(180deg, #322620 0%, #1f1715 100%);
box-shadow: 0 8px 0 #0d0908, 0 18px 34px var(--shadow);
}
.brand-lockup { display: flex; gap: 14px; align-items: center; min-width: 0; }
.brand-meta { min-width: 0; }
.chip-mark {
display: grid;
place-items: center;
width: 58px;
height: 58px;
flex: 0 0 auto;
border: 4px dashed #fff4bc;
border-radius: 50%;
color: #20130b;
background: radial-gradient(circle, #ffe28a 0 42%, #c73f3d 43% 100%);
font-weight: 900;
text-shadow: 1px 1px 0 rgba(255,255,255,0.45);
/* Slow 8-bit chip rotation when idle for a touch of life. */
animation: chipSpin 6s linear infinite;
}
h1, h2, p { margin: 0; }
h1 { font-size: clamp(18px, 2.3vw, 28px); }
h2 { margin-bottom: 12px; color: var(--gold); font-size: 15px; }
#subtitle { margin-top: 5px; color: var(--ink-dim); font-size: 12px; }
.status-strip {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
/* Generic pixel "tag" badge — shared by status, hand, pot, etc. */
.badge {
display: inline-block;
border: 2px solid #16100d;
padding: 7px 10px;
color: #1b120c;
font-weight: 700;
white-space: nowrap;
box-shadow: inset -2px -2px 0 rgba(0,0,0,0.18);
}
.badge-gold { background: var(--gold); }
.badge-blue { background: var(--blue); color: #0f1c2e; }
/* ---------- Layout Grid ---------- */
/* Desktop: stage in middle, controls left, events right. The stage column
is the dominant area (1.4fr) so the table breathes. */
.layout-grid {
display: grid;
grid-template-columns: minmax(260px, 320px) minmax(0, 1.4fr) minmax(280px, 340px);
grid-template-areas: "controls stage events";
gap: 16px;
margin-top: 18px;
align-items: start;
}
.control-panel { grid-area: controls; display: grid; gap: 14px; }
.stage-zone { grid-area: stage; min-width: 0; }
.event-panel { grid-area: events; display: grid; gap: 14px; }
/* ---------- Panel sections (the chunky bordered cards) ---------- */
.panel-section {
border: var(--pixel) solid #49382e;
background: linear-gradient(180deg, var(--panel-2), var(--panel));
padding: 14px;
box-shadow: 0 7px 0 #0e0a08, 0 14px 26px var(--shadow);
}
.panel-section label {
display: grid;
gap: 6px;
margin-bottom: 10px;
color: var(--ink-dim);
font-size: 12px;
}
.button-row, .transport-row, .auto-grid { display: grid; gap: 9px; }
.button-row { grid-template-columns: 1fr 1fr; }
.transport-row { grid-template-columns: repeat(4, minmax(42px, 1fr)); }
.auto-grid { grid-template-columns: 1fr 86px; align-items: end; }
.toggle-line { display: flex !important; flex-direction: row; align-items: center; min-height: 40px; }
.toggle-line input { width: auto; min-height: auto; accent-color: var(--gold); }
.primary-btn { color: #1b120c; background: var(--gold); }
.file-btn {
display: grid; place-items: center; text-align: center;
position: relative; overflow: hidden;
}
.file-btn input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.progress-wrap {
height: 12px; margin-top: 14px;
border: 2px solid #120d0b; background: #14100e;
}
#progressBar {
width: 0; height: 100%;
background: linear-gradient(90deg, var(--red), var(--gold), var(--green));
transition: width 220ms linear;
}
.stat-list { display: grid; gap: 8px; margin: 0; }
.stat-list div {
display: flex; justify-content: space-between; gap: 12px;
border-bottom: 1px solid rgba(255,255,255,0.08);
padding-bottom: 7px;
}
.stat-list dt { color: var(--ink-dim); }
.stat-list dd { margin: 0; text-align: right; }
/* ---------- Stage zone ---------- */
.stage-head {
display: flex; justify-content: space-between; align-items: center;
gap: 10px; margin-bottom: 12px;
}
.stage-head-left { display: flex; gap: 10px; align-items: center; min-width: 0; }
#streetLabel { font-size: clamp(16px, 2vw, 22px); color: var(--ink); }
/* The poker-table is the positioning context for seats. Its overflow MUST
stay visible so seats placed near the table edges (and their speech
bubbles) can extend slightly outside the green felt without being clipped.
The visual clipping is handled by .felt-shell instead. */
.poker-table {
position: relative;
/* Reserve vertical breathing room above/below the felt for seats that
visually sit on the rail and for speech bubbles. */
padding: 70px 12px 80px;
/* Explicit responsive height — required because seats and felt are
absolutely positioned and the inner content (community area) is
center-aligned. clamp() keeps it usable from 640px to ~820px. */
height: clamp(620px, 64vw, 820px);
overflow: visible;
}
/* Felt shell — actual visible green table. Absolutely positioned and
inset within poker-table so seats/bubbles can spill outside. */
.felt-shell {
position: absolute;
inset: 60px 0 70px;
border: 6px solid var(--felt-rail);
border-radius: 46% / 36%;
background: var(--felt-rail-dark);
box-shadow:
inset 0 0 0 10px var(--felt-rail),
inset 0 0 0 16px #2c1b12,
0 10px 0 #130c09,
0 24px 44px var(--shadow);
overflow: hidden;
pointer-events: none;
}
.felt-rail {
/* Decorative pixel "studs" running along the rail. */
position: absolute; inset: -2px;
background:
repeating-linear-gradient(90deg,
transparent 0 22px, rgba(255,220,160,0.18) 22px 24px),
repeating-linear-gradient(0deg,
transparent 0 22px, rgba(255,220,160,0.18) 22px 24px);
mix-blend-mode: screen;
opacity: 0.4;
}
.felt-surface {
position: absolute;
inset: 12px;
border-radius: 44% / 32%;
background:
radial-gradient(ellipse at 50% 50%, var(--felt-light) 0%, var(--felt) 38%, var(--felt-dark) 100%);
overflow: hidden;
}
.felt-grid {
position: absolute; inset: 0;
opacity: 0.18;
/* 1px-wide pixel grid for the felt — gives a chess-board-like 8-bit feel. */
background:
linear-gradient(90deg, transparent calc(100% - 2px), rgba(255,255,255,0.4) 0) 0 0 / 28px 28px,
linear-gradient(180deg, transparent calc(100% - 2px), rgba(255,255,255,0.28) 0) 0 0 / 28px 28px;
image-rendering: pixelated;
}
.felt-glow {
position: absolute; inset: 0;
background: radial-gradient(ellipse at 50% 45%, rgba(255,255,255,0.18), transparent 55%);
pointer-events: none;
}
.felt-mark {
position: absolute;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
font-size: clamp(48px, 7vw, 96px);
font-weight: 900;
color: rgba(0,0,0,0.18);
letter-spacing: 6px;
text-shadow: 2px 2px 0 rgba(255,255,255,0.06);
user-select: none;
}
/* ---------- Community area (board + message) ---------- */
.community-area {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: min(60%, 560px);
display: grid;
justify-items: center;
gap: 14px;
z-index: 3;
pointer-events: none;
}
.card-row {
display: flex;
justify-content: center;
gap: clamp(6px, 1vw, 12px);
min-height: 76px;
}
.card-row.board { min-height: 92px; }
/* ---------- Cards ---------- */
.card {
display: grid;
grid-template-rows: auto 1fr auto;
width: clamp(48px, 5.4vw, 70px);
height: clamp(66px, 7.4vw, 96px);
border: 3px solid #1c1411;
background: #fff7df;
color: #17100d;
padding: 4px 6px;
box-shadow:
inset -2px -2px 0 rgba(0,0,0,0.12),
inset 2px 2px 0 rgba(255,255,255,0.6),
0 5px 0 rgba(0,0,0,0.42);
transform-origin: center;
animation: cardDeal 520ms cubic-bezier(.2,.9,.2,1);
font-family: "Courier New", monospace;
}
.card.red { color: #b92732; }
.card.back {
background:
/* 8-bit checker pattern for card back. */
linear-gradient(45deg, #2d579a 25%, transparent 25%) 0 0 / 8px 8px,
linear-gradient(-45deg, #2d579a 25%, transparent 25%) 0 0 / 8px 8px,
linear-gradient(45deg, transparent 75%, #2d579a 75%) 0 0 / 8px 8px,
linear-gradient(-45deg, transparent 75%, #2d579a 75%) 0 0 / 8px 8px,
#173a72;
}
.card .rank { font-size: clamp(12px, 1.2vw, 16px); font-weight: 900; line-height: 1; }
.card .suit { display: grid; place-items: center; font-size: clamp(18px, 2vw, 28px); }
.card .rank.bottom { transform: rotate(180deg); justify-self: end; }
.table-message {
min-height: 32px;
max-width: min(540px, 82%);
border: 3px solid #14100d;
padding: 8px 12px;
color: #1b120c;
background: #ffe28a;
text-align: center;
box-shadow: 0 4px 0 rgba(0,0,0,0.38);
}
/* ---------- Seats ---------- */
.seat-layer {
position: absolute;
inset: 0;
z-index: 4;
pointer-events: none;
}
.seat {
--x: 50%;
--y: 50%;
position: absolute;
left: var(--x);
top: var(--y);
width: var(--seat-size);
transform: translate(-50%, -50%);
transition: transform 220ms steps(4), filter 220ms ease;
pointer-events: auto;
}
/* Active seat — slight lift + glow + sprite "hop" animation. */
.seat.active {
filter: drop-shadow(0 0 14px rgba(240,182,77,0.85));
z-index: 6;
}
.seat.active .avatar { animation: avatarHop 520ms ease; }
.seat.folded { opacity: 0.55; filter: grayscale(0.6); }
.seat.folded .avatar { transform: rotate(-8deg); }
/* Action-driven sprite reactions (added by JS as transient classes). */
.seat.act-fold .avatar { animation: avatarFold 600ms ease forwards; }
.seat.act-bet .avatar,
.seat.act-raise .avatar,
.seat.act-all_in .avatar { animation: avatarShake 480ms ease; }
.seat.act-call .avatar,
.seat.act-check .avatar { animation: avatarNod 480ms ease; }
.seat.act-award .avatar { animation: avatarCheer 900ms ease; }
/* Speech bubble — placed above the seat by default; below for top seats so
it does not punch out of the table viewport. */
.speech {
position: absolute;
left: 50%;
bottom: calc(100% + 8px);
min-width: 72px;
max-width: 160px;
transform: translateX(-50%);
border: 3px solid #160f0c;
padding: 6px 10px;
color: #1a110b;
background: #fff2b7;
text-align: center;
font-weight: 700;
font-size: 12px;
box-shadow: 0 4px 0 rgba(0,0,0,0.38);
animation: bubblePop 380ms ease;
z-index: 8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Bubble tail — pixel-art triangle made from a rotated solid square. */
.speech::after {
content: "";
position: absolute;
left: 50%; top: 100%;
transform: translate(-50%, -3px) rotate(45deg);
width: 12px; height: 12px;
background: #fff2b7;
border-right: 3px solid #160f0c;
border-bottom: 3px solid #160f0c;
}
/* For top-row seats, flip the bubble below so it does not get clipped. */
.seat.seat-top .speech {
bottom: auto;
top: calc(100% + 8px);
}
.seat.seat-top .speech::after {
top: auto; bottom: 100%;
transform: translate(-50%, 3px) rotate(225deg);
}
/* Color the bubble by action category. */
.speech.kind-fold { background: #d8d2bc; }
.speech.kind-call,
.speech.kind-check { background: #c2e6c8; }
.speech.kind-bet,
.speech.kind-raise,
.speech.kind-all_in { background: #ffc7a8; color: #5a1f0a; }
.speech.kind-award { background: #ffe28a; }
/* Player name+avatar+stack box. */
.player-box {
border: 3px solid #15100d;
background: linear-gradient(180deg, #423128, #261c18);
padding: 8px;
box-shadow:
inset -3px -3px 0 rgba(0,0,0,0.35),
inset 3px 3px 0 rgba(255,255,255,0.08),
0 6px 0 #120d0b;
position: relative;
}
.player-head {
display: grid;
grid-template-columns: var(--avatar-size) 1fr auto;
gap: 8px;
align-items: center;
min-width: 0;
}
/* Avatar — wraps an inline SVG pixel-art portrait generated by JS. The SVG
is an 8x8 grid of <rect> elements; the host element only provides the
square frame, border, and animation hook. */
.avatar {
width: var(--avatar-size);
height: var(--avatar-size);
flex: 0 0 auto;
position: relative;
border: 2px solid #15100d;
background: #211814;
image-rendering: pixelated;
display: grid;
place-items: stretch;
overflow: hidden;
box-shadow: inset -2px -2px 0 rgba(0,0,0,0.35);
}
.avatar svg {
width: 100%;
height: 100%;
display: block;
shape-rendering: crispEdges;
}
.player-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 900;
font-size: 12px;
color: var(--ink);
}
.dealer {
display: none;
flex: 0 0 auto;
border: 2px solid #130e0b;
padding: 2px 6px;
color: #17100d;
background: var(--gold);
font-weight: 900;
font-size: 11px;
}
.seat.dealer-seat .dealer { display: inline-block; }
.player-meta {
display: flex;
justify-content: space-between;
gap: 8px;
margin-top: 6px;
font-size: 11px;
}
.stack { color: var(--green); }
.bet { min-height: 14px; color: var(--gold); }
.hole-cards {
justify-content: flex-start;
min-height: 38px;
margin-top: 6px;
gap: 4px;
}
.hole-cards .card {
width: clamp(26px, 2.8vw, 36px);
height: clamp(36px, 4vw, 50px);
padding: 2px 3px;
border-width: 2px;
box-shadow:
inset -1px -1px 0 rgba(0,0,0,0.12),
inset 1px 1px 0 rgba(255,255,255,0.6),
0 3px 0 rgba(0,0,0,0.42);
}
.hole-cards .card .rank { font-size: 10px; }
.hole-cards .card .suit { font-size: 14px; }
/* Chip stack indicator drawn near the player's bet (gives action visual
weight even before the bubble). */
.chip-pile {
position: absolute;
left: 50%;
top: -14px;
transform: translateX(-50%);
display: flex;
gap: 2px;
pointer-events: none;
opacity: 0;
transition: opacity 200ms steps(2);
}
.chip-pile.visible { opacity: 1; }
.chip-pile .chip {
width: 12px; height: 12px;
border-radius: 50%;
border: 2px solid #160f0c;
background: var(--gold);
box-shadow: 0 2px 0 rgba(0,0,0,0.5);
}
.chip-pile .chip:nth-child(2) { background: var(--red); }
.chip-pile .chip:nth-child(3) { background: var(--blue); }
/* ---------- Event log ---------- */
.event-panel .panel-section { display: flex; flex-direction: column; }
.event-log {
display: grid;
gap: 8px;
/* Use viewport-relative max-height for desktop, with a fallback minimum
so the log never collapses to nothing. The actual scrollable height is
plenty for 20+ events while still aligning with the table's height. */
max-height: clamp(420px, 70vh, 760px);
overflow: auto;
margin: 0;
padding: 0 4px 0 26px;
scrollbar-width: thin;
scrollbar-color: var(--gold-dark) #1a1310;
}
.event-log::-webkit-scrollbar { width: 10px; }
.event-log::-webkit-scrollbar-track { background: #1a1310; }
.event-log::-webkit-scrollbar-thumb {
background: var(--gold-dark);
border: 2px solid #1a1310;
}
.event-log li {
border-left: 4px solid var(--line);
padding: 7px 8px;
color: var(--ink-dim);
background: rgba(0,0,0,0.18);
word-break: break-word;
white-space: normal;
line-height: 1.4;
}
.event-log li.current {
border-color: var(--gold);
color: var(--ink);
background: rgba(240,182,77,0.14);
box-shadow: inset 2px 0 0 var(--gold);
}
.event-log li.past { opacity: 0.85; }
/* ---------- Animations ---------- */
@keyframes cardDeal {
from { opacity: 0; transform: translateY(-18px) rotate(-6deg) scale(0.86); }
to { opacity: 1; transform: translateY(0) rotate(0) scale(1); }
}
@keyframes bubblePop {
from { opacity: 0; transform: translate(-50%, 8px) scale(0.8); }
to { opacity: 1; transform: translate(-50%, 0) scale(1); }
}
@keyframes chipSpin {
0%, 90%, 100% { transform: rotate(0); }
95% { transform: rotate(8deg); }
}
@keyframes avatarHop {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-6px); }
70% { transform: translateY(-2px); }
}
@keyframes avatarShake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-3px) rotate(-3deg); }
40% { transform: translateX(3px) rotate(3deg); }
60% { transform: translateX(-2px) rotate(-2deg); }
80% { transform: translateX(2px) rotate(2deg); }
}
@keyframes avatarNod {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(3px); }
}
@keyframes avatarFold {
0% { transform: rotate(0) translateY(0); }
100% { transform: rotate(-12deg) translateY(2px); }
}
@keyframes avatarCheer {
0%, 100% { transform: translateY(0) rotate(0); }
20% { transform: translateY(-8px) rotate(-6deg); }
50% { transform: translateY(-4px) rotate(6deg); }
80% { transform: translateY(-6px) rotate(-3deg); }
}
/* =========================================================================
Responsive breakpoints
- Tablet (<=1180px): drop to 2 columns. Stage on top spans full width;
controls + events sit side-by-side underneath.
- Mobile (<=760px): single column; stage first, then controls, then log.
========================================================================= */
@media (max-width: 1180px) {
.layout-grid {
grid-template-columns: minmax(260px, 1fr) minmax(260px, 1fr);
grid-template-areas:
"stage stage"
"controls events";
}
.event-log { max-height: 360px; }
:root { --seat-size: clamp(96px, 16vw, 130px); }
}
@media (max-width: 760px) {
.app-shell { padding: 10px; }
.topbar {
display: grid;
grid-template-columns: 1fr;
}
.status-strip { justify-content: flex-start; }
.layout-grid {
grid-template-columns: 1fr;
grid-template-areas:
"stage"
"controls"
"events";
}
.stage-head {
flex-wrap: wrap;
}
.poker-table {
padding: 56px 4px 64px;
height: 600px;
}
.felt-shell {
inset: 48px 0 56px;
border-radius: 28px;
}
.felt-surface { border-radius: 22px; }
.community-area { width: min(76%, 420px); }
.event-log { max-height: 240px; }
:root {
--seat-size: clamp(86px, 30vw, 120px);
--avatar-size: 36px;
}
}
/* Reduce-motion users get static sprites (no shake/hop). */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; }
}