| from __future__ import annotations |
|
|
| from html import escape |
|
|
| from token_holdem.cards import RED_SUITS, SUIT_SYMBOLS, cards_label |
| from token_holdem.engine import GameState, PlayerState |
| from token_holdem.leaderboard import LeaderboardRow |
|
|
|
|
| SEAT_COORDS: dict[int, tuple[int, int]] = { |
| 0: (50, 82), |
| 1: (23, 78), |
| 2: (8, 54), |
| 3: (18, 24), |
| 4: (50, 12), |
| 5: (82, 24), |
| 6: (92, 54), |
| 7: (77, 78), |
| } |
|
|
| DEALER_BUTTON_COORDS: dict[int, tuple[int, int]] = { |
| 0: (50, 70), |
| 1: (30, 69), |
| 2: (20, 54), |
| 3: (28, 32), |
| 4: (50, 24), |
| 5: (72, 32), |
| 6: (80, 54), |
| 7: (70, 69), |
| } |
|
|
| SEAT_COUNT = 8 |
| BOARD_CARD_SLOTS = 5 |
|
|
| AVATAR_PERSONAS: dict[str, dict[str, str]] = { |
| "Nemotron Nano": { |
| "display": "Nemotron", |
| "role": "Tinkerer automaton", |
| "initials": "NE", |
| "accent": "#d9a35f", |
| "talk": "Counting tells by candlelight.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-nemotron-automaton-halfbody-768.png", |
| }, |
| "Qwen": { |
| "display": "Qwen", |
| "role": "Scholar oracle", |
| "initials": "QW", |
| "accent": "#74b8b2", |
| "talk": "Pot odds first, pride second.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-qwen-oracle-halfbody-768.png", |
| }, |
| "Gemma": { |
| "display": "Gemma", |
| "role": "Hearth strategist", |
| "initials": "GE", |
| "accent": "#b7cf7a", |
| "talk": "Small folds keep stacks warm.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-gemma-hearth-halfbody-768.png", |
| }, |
| "Cohere North Mini": { |
| "display": "North", |
| "role": "Cartographer monk", |
| "initials": "NO", |
| "accent": "#c982bc", |
| "talk": "A side quest enters the pot.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-north-cartographer-halfbody-768.png", |
| }, |
| "Mistral": { |
| "display": "Mistral", |
| "role": "Storm duelist", |
| "initials": "MI", |
| "accent": "#8dc5ff", |
| "talk": "Fast wind, faster raise.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-mistral-duelist-halfbody-768.png", |
| }, |
| "OpenAI Open Model 20B": { |
| "display": "Open Model", |
| "role": "Dealer savant", |
| "initials": "OM", |
| "accent": "#f2c45d", |
| "talk": "Expected value takes the stage.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-open-model-savant-halfbody-768.png", |
| }, |
| "Llama Scout": { |
| "display": "Llama Scout", |
| "role": "Pattern scout", |
| "initials": "LS", |
| "accent": "#d49464", |
| "talk": "The felt remembers every bet.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-llama-scout-halfbody-768.png", |
| }, |
| "Professor Odds": { |
| "display": "Professor Odds", |
| "role": "Probability professor", |
| "initials": "PO", |
| "accent": "#d9a35f", |
| "talk": "The math is in my favor.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-professor-odds-neutral-512.png", |
| }, |
| "Maverick": { |
| "display": "Maverick", |
| "role": "Frontier gambler", |
| "initials": "MV", |
| "accent": "#d49464", |
| "talk": "Smells like weakness.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-maverick-frontier-neutral-512.png", |
| }, |
| "Sheriff": { |
| "display": "Sheriff", |
| "role": "Lawkeeper", |
| "initials": "SH", |
| "accent": "#c4934e", |
| "talk": "Keep the table honest.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-sheriff-lawkeeper-neutral-512.png", |
| }, |
| "Gobbo": { |
| "display": "Gobbo", |
| "role": "Chip trickster", |
| "initials": "GB", |
| "accent": "#82bf63", |
| "talk": "Shiny chips... mine?", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-gobbo-trickster-neutral-512.png", |
| }, |
| } |
|
|
| HUMAN_PERSONA = { |
| "display": "You", |
| "role": "Tavern challenger", |
| "initials": "YU", |
| "accent": "#ffd37a", |
| "talk": "Your cards, your candle.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-human-wanderer-halfbody-768.png", |
| } |
|
|
|
|
| def table_html(game: GameState | None, reveal_all: bool = False, chats: list[str] | None = None) -> str: |
| if game is None: |
| return """ |
| <div class="token-table-shell tavern-scene"> |
| <div class="token-table token-table-empty"> |
| <div class="tavern-backdrop tavern-background" aria-hidden="true"> |
| <span class="wall-stone wall-stone-a"></span> |
| <span class="wall-stone wall-stone-b"></span> |
| <span class="bar-shelf"></span> |
| <span class="bartender-shadow"></span> |
| <span class="candle candle-left"></span> |
| <span class="candle candle-right"></span> |
| </div> |
| <div class="tavern-ambient-lights" aria-hidden="true"><span></span><span></span><span></span></div> |
| <div class="table-felt poker-table-surface" aria-hidden="true"></div> |
| <div class="table-chip-pile table-chip-pile-empty" aria-hidden="true"><span></span><span></span><span></span></div> |
| <div class="center-hud"> |
| <div class="hud-kicker">Waiting for players</div> |
| <div class="pot">Pot 0</div> |
| <div class="street">Quick Seat to begin</div> |
| <div class="community card-row" aria-label="Community cards"> |
| <span class="card card-slot"></span><span class="card card-slot"></span><span class="card card-slot"></span><span class="card card-slot"></span><span class="card card-slot"></span> |
| </div> |
| </div> |
| </div> |
| </div> |
| """ |
|
|
| current_id = game.current_player().id if not game.result else "" |
| current_actor = "Complete" if game.result else game.current_player().name |
| button_slot = _slot_for_player_index(game, game.dealer_index) |
| dealer_x, dealer_y = DEALER_BUTTON_COORDS[button_slot] |
| talk_by_name = _table_talk_by_name(chats) |
| seats = [] |
| human_seat = "" |
| speech_bubbles = [] |
| for slot, player in enumerate(_seat_slots(game)): |
| x, y = SEAT_COORDS[slot] |
| slot_class = " human-seat" if slot == 0 else " llm-seat" |
| if player is None: |
| seat_html = _empty_seat_html(slot, x, y, slot_class) |
| if slot == 0: |
| human_seat = seat_html |
| else: |
| seats.append(seat_html) |
| continue |
| hidden = not reveal_all and not player.is_human and not game.result |
| result_winner = bool(game.result and player.id in game.result.winners) |
| active = " active-seat" if player.id == current_id else "" |
| winner_class = " winner-seat" if result_winner else "" |
| folded = " folded-seat" if player.folded else "" |
| all_in = " all-in-seat" if player.all_in else "" |
| cards = _hole_cards_html(player, hidden=hidden) |
| status = _player_status(player, active=player.id == current_id, result_winner=result_winner) |
| bet = f'<span class="seat-bet">Bet {player.bet}</span>' if player.bet else '<span class="seat-bet muted">Bet 0</span>' |
| persona = _persona_for(player) |
| talk = talk_by_name.get(player.name, persona["talk"]) |
| avatar_html = _avatar_img_html(persona) |
| if talk and player.name in talk_by_name: |
| speech_bubbles.append( |
| f'<div class="speech-bubble seat-{slot}-bubble" style="--bubble-x: {x}%; --bubble-y: {max(8, y - 12)}%; --avatar-accent: {persona["accent"]};"><strong>{escape(player.name)}</strong><span>{escape(talk)}</span></div>' |
| ) |
| talk_html = f'<div class="seat-talk">{escape(talk)}</div>' if talk else "" |
| seat_html = f""" |
| <section class="seat seat-{slot}{slot_class}{active}{winner_class}{folded}{all_in}" style="--seat-x: {x}%; --seat-y: {y}%; --avatar-accent: {persona['accent']};" aria-label="Seat {slot}: {escape(player.name)}"> |
| <div class="avatar-layer" title="{escape(persona['display'])}" aria-hidden="true"> |
| <div class="avatar-slot">{avatar_html}<span>{escape(persona['initials'])}</span></div> |
| </div> |
| <div class="seat-plaque"> |
| <div class="seat-copy"> |
| <div class="seat-name">{escape(player.name)}</div> |
| </div> |
| <div class="seat-meta"><span class="seat-stack">Stack {player.stack}</span>{bet}<span class="seat-status">{status}</span></div> |
| </div> |
| <div class="speech-bubble-layer">{talk_html}</div> |
| <div class="seat-cards">{cards}</div> |
| </section> |
| """ |
| if slot == 0: |
| human_seat = seat_html |
| else: |
| seats.append(seat_html) |
| winner = f'<div class="result-banner">{escape(game.result.summary)}</div>' if game.result else "" |
| return f""" |
| <div class="token-table-shell tavern-scene"> |
| <div class="table-status-bar"> |
| <span>Hand {escape(game.hand_id or "-")}</span> |
| <span>Orbit {escape(game.orbit_id or "-")}</span> |
| <span>Blinds {game.small_blind}/{game.big_blind}</span> |
| </div> |
| <div class="token-table" role="img" aria-label="Token Hold'em 8-seat table"> |
| <div class="tavern-backdrop tavern-background" aria-hidden="true"> |
| <span class="wall-stone wall-stone-a"></span> |
| <span class="wall-stone wall-stone-b"></span> |
| <span class="bar-shelf"></span> |
| <span class="bartender-shadow"></span> |
| <span class="candle candle-left"></span> |
| <span class="candle candle-right"></span> |
| <span class="floor-planks"></span> |
| </div> |
| <div class="tavern-ambient-lights" aria-hidden="true"><span></span><span></span><span></span></div> |
| <div class="table-felt poker-table-surface" aria-hidden="true"></div> |
| <div class="table-chip-pile" aria-hidden="true"><span></span><span></span><span></span><b>{game.pot()}</b></div> |
| <div class="dealer-button" style="--dealer-x: {dealer_x}%; --dealer-y: {dealer_y}%;" aria-label="Dealer button at seat {button_slot}">D</div> |
| <section class="center-hud" aria-label="Current hand state"> |
| <div class="hud-kicker">Current actor</div> |
| <div class="current-actor">{escape(current_actor)}</div> |
| <div class="pot">Pot {game.pot()}</div> |
| <div class="street">{escape(game.street.value.title())} · Bet {game.current_bet}</div> |
| <div class="community card-row" aria-label="Community cards">{_community_cards_html(game.community_cards)}</div> |
| {winner} |
| </section> |
| <div class="seat-layer"> |
| {human_seat} |
| <div class="seat-ring">{''.join(seats)}</div> |
| </div> |
| <div class="speech-bubble-layer table-speech-layer">{''.join(speech_bubbles[-3:])}</div> |
| </div> |
| </div> |
| """ |
|
|
|
|
| def _slot_for_player_index(game: GameState, player_index: int) -> int: |
| if not game.players: |
| return 0 |
| player = game.players[player_index] |
| if player.is_human: |
| return 0 |
| human_present = any(candidate.is_human for candidate in game.players) |
| ai_players = [candidate for candidate in game.players if not candidate.is_human] |
| ai_slot = ai_players.index(player) + 1 |
| return min(ai_slot, SEAT_COUNT - 1) if human_present else min(player_index + 1, SEAT_COUNT - 1) |
|
|
|
|
| def _seat_slots(game: GameState) -> list[PlayerState | None]: |
| slots: list[PlayerState | None] = [None] * SEAT_COUNT |
| human = next((player for player in game.players if player.is_human), None) |
| ai_players = [player for player in game.players if not player.is_human] |
| slots[0] = human |
| for slot, player in enumerate(ai_players[: SEAT_COUNT - 1], start=1): |
| slots[slot] = player |
| return slots |
|
|
|
|
| def _empty_seat_html(slot: int, x: int, y: int, slot_class: str) -> str: |
| label = "Human seat" if slot == 0 else "LLM seat" |
| return f""" |
| <section class="seat seat-{slot}{slot_class} empty-seat" style="--seat-x: {x}%; --seat-y: {y}%;" aria-label="Seat {slot}: {label} open"> |
| <div class="avatar-slot" aria-hidden="true"><span>{slot}</span></div> |
| <div class="seat-copy"> |
| <div class="seat-label"><span class="seat-index">Seat {slot}</span></div> |
| <div class="seat-name">{label}</div> |
| <div class="seat-stack">Waiting</div> |
| </div> |
| <div class="seat-meta"><span class="seat-bet muted">No bet</span><span class="seat-status">Open</span></div> |
| <div class="seat-cards">{_empty_hole_cards_html()}</div> |
| </section> |
| """ |
|
|
|
|
| def _persona_for(player: PlayerState) -> dict[str, str]: |
| if player.is_human: |
| persona = HUMAN_PERSONA.copy() |
| initials = "".join(part[0] for part in player.name.split()[:2]).upper() or "YU" |
| persona["initials"] = initials[:2] |
| return persona |
| return AVATAR_PERSONAS.get( |
| player.name, |
| { |
| "display": "Tavern Regular", |
| "role": "Unknown model", |
| "initials": player.name[:2].upper(), |
| "accent": "#d9a35f", |
| "talk": "Waiting for a readable tell.", |
| "avatar": "/gradio_api/file=assets/token-holdem/avatar-sheriff-lawkeeper-neutral-512.png", |
| }, |
| ) |
|
|
|
|
| def _avatar_img_html(persona: dict[str, str]) -> str: |
| avatar = persona.get("avatar", "") |
| if not avatar: |
| return "" |
| return f'<img src="{escape(avatar)}" alt="" loading="lazy">' |
|
|
|
|
| def _table_talk_by_name(chats: list[str] | None) -> dict[str, str]: |
| if not chats: |
| return {} |
| talks: dict[str, str] = {} |
| for chat in chats[-16:]: |
| if ": " not in chat: |
| continue |
| name, line = chat.split(": ", 1) |
| talks[name] = line[:96] |
| return talks |
|
|
|
|
| def _last_action_by_name(game: GameState) -> dict[str, str]: |
| actions: dict[str, str] = {} |
| names = sorted((player.name for player in game.players), key=len, reverse=True) |
| for line in game.action_log: |
| for name in names: |
| if line.startswith(f"{name} "): |
| actions[name] = line[len(name) + 1 :][:42] |
| break |
| return actions |
|
|
|
|
| def _player_status(player: PlayerState, *, active: bool, result_winner: bool) -> str: |
| if result_winner: |
| return "Winner" |
| if player.folded: |
| return "Folded" |
| if active: |
| return "Acting" |
| if player.all_in or player.bet: |
| return "Ready" |
| return "Watching" |
|
|
|
|
| def _hole_cards_html(player: PlayerState, *, hidden: bool) -> str: |
| if not player.hole_cards: |
| return _empty_hole_cards_html() |
| return "".join(_card_html(card, hidden=hidden, small=not player.is_human) for card in player.hole_cards[:2]) |
|
|
|
|
| def _empty_hole_cards_html() -> str: |
| return '<span class="mini-card card-slot"></span><span class="mini-card card-slot"></span>' |
|
|
|
|
| def _community_cards_html(cards: list[str]) -> str: |
| visible_cards = [_card_html(card) for card in cards[:BOARD_CARD_SLOTS]] |
| empty_cards = ['<span class="card card-slot"></span>' for _ in range(BOARD_CARD_SLOTS - len(visible_cards))] |
| return "".join([*visible_cards, *empty_cards]) |
|
|
|
|
| def _card_html(card: str, *, hidden: bool = False, small: bool = False) -> str: |
| class_name = "mini-card" if small else "card" |
| if hidden: |
| return f'<span class="{class_name} hidden-card" aria-label="Hidden card">??</span>' |
| suit = card[1] |
| color = " red-card" if suit in RED_SUITS else "" |
| return f'<span class="{class_name}{color}">{escape(card[0] + SUIT_SYMBOLS[suit])}</span>' |
|
|
|
|
| def log_markdown(game: GameState | None, chats: list[str] | None = None) -> str: |
| if game is None: |
| return '<div class="tavern-log empty-log">No hand yet. The table is waiting by the hearth.</div>' |
| lines = ['<div class="tavern-log">', f'<div class="log-board">Board: <code>{escape(cards_label(game.community_cards))}</code></div>', '<ul>'] |
| lines.extend(f"<li>{escape(line)}</li>" for line in game.action_log[-18:]) |
| lines.append("</ul>") |
| if chats: |
| lines.append('<div class="log-talk-title">Table Talk</div><ul class="log-talk">') |
| lines.extend(f"<li>{escape(chat)}</li>" for chat in chats[-10:]) |
| lines.append("</ul>") |
| lines.append("</div>") |
| return "\n".join(lines) |
|
|
|
|
| def leaderboard_markdown(rows: list[LeaderboardRow]) -> str: |
| if not rows: |
| return '<div class="tavern-ledger empty-ledger">No hands recorded yet. The ledger is clean parchment.</div>' |
| body = ['<div class="tavern-ledger"><table><thead><tr><th>Rank</th><th>Player</th><th>Title</th><th>Bankroll</th><th>W-L-P</th><th>Hands</th><th>Biggest Pot</th><th>Net</th><th>Avg/Hand</th></tr></thead><tbody>'] |
| for idx, row in enumerate(rows, 1): |
| body.append( |
| "<tr>" |
| f"<td>{idx}</td><td>{escape(row.name)}</td><td>{escape(row.title)}</td><td>{row.bankroll}</td>" |
| f"<td>{row.wins}-{row.losses}-{row.pushes}</td><td>{row.hands_played}</td><td>{row.biggest_pot_won}</td>" |
| f"<td>{row.net_profit}</td><td>{row.average_profit:.1f}</td>" |
| "</tr>" |
| ) |
| body.append("</tbody></table></div>") |
| return "\n".join(body) |
|
|
|
|
| def hall_markdown(records) -> str: |
| if not records: |
| return '<div class="hall-wall empty-hall">No legends carved into the tavern wall yet.</div>' |
| items = ['<div class="hall-wall">'] |
| for record in records: |
| title = escape(record["key"].replace("_", " ").title()) |
| label = escape(record["label"]) |
| items.append(f'<article class="hall-plaque"><span>{title}</span><strong>{label}</strong></article>') |
| items.append("</div>") |
| return "\n".join(items) |
|
|
|
|
| CSS = """ |
| :root { |
| color-scheme: dark; |
| --ink: #f8e8c8; |
| --ember: #ffd37a; |
| --amber: #dca861; |
| --brass: #c4934e; |
| --walnut: #5b331c; |
| --dark-walnut: #211018; |
| --stone: #50494a; |
| --felt: #2f724f; |
| --felt-dark: #15351f; |
| --burgundy: #5a2230; |
| --danger: #bd3f35; |
| --confirm: #65b96f; |
| --raise: #d88736; |
| --all-in: #8965d6; |
| --z-bg: 0; |
| --z-stage: 10; |
| --z-table: 20; |
| --z-avatars: 30; |
| --z-speech: 40; |
| --z-hud: 50; |
| --z-actions: 60; |
| --z-modal: 100; |
| } |
| .gradio-container { |
| color: var(--ink) !important; |
| background: |
| linear-gradient(180deg, rgba(10, 7, 9, .58), rgba(10, 7, 9, .62)), |
| url("/gradio_api/file=assets/token-holdem/tavern-bg-main-1672x941.png") center top / cover fixed, |
| radial-gradient(circle at 18% 8%, rgba(255, 189, 94, .13), transparent 18rem), |
| radial-gradient(circle at 88% 18%, rgba(255, 211, 122, .09), transparent 16rem), |
| repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 2px, transparent 2px 34px), |
| repeating-linear-gradient(90deg, rgba(255,255,255,.018) 0 2px, transparent 2px 54px), |
| linear-gradient(180deg, #181217 0%, #100d12 38%, #17100f 100%) !important; |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace !important; |
| min-height: 100dvh; |
| } |
| .gradio-container > .main, .gradio-container .wrap, .gradio-container .contain { max-width: none !important; } |
| .gradio-container [role="tablist"], |
| .gradio-container [role="tabpanel"], |
| .gradio-container .tabs, |
| .gradio-container .tab-nav, |
| .gradio-container .tabitem { |
| border-color: transparent !important; |
| box-shadow: none !important; |
| } |
| .gradio-container [role="tabpanel"] { |
| background: transparent !important; |
| } |
| .app-title { |
| width: min(100%, 1440px); |
| margin: 0 auto 8px; |
| text-align: center; |
| padding: 10px 18px; |
| border: 2px solid var(--brass); |
| border-radius: 8px; |
| background: |
| linear-gradient(90deg, rgba(255,211,122,.06), transparent 14%, transparent 86%, rgba(255,211,122,.06)), |
| repeating-linear-gradient(0deg, rgba(255,255,255,.035) 0 2px, transparent 2px 11px), |
| linear-gradient(180deg, #3a1d18, #1b1114); |
| box-shadow: 0 0 0 3px rgba(40,20,14,.85), 0 12px 28px rgba(0,0,0,.48), 0 0 24px rgba(255,171,74,.12) inset; |
| } |
| .app-title h1 { margin: 0; color: var(--ember); font-size: clamp(28px, 4vw, 46px); letter-spacing: 0; text-shadow: 3px 3px #5a2415; } |
| .app-title p { margin: 4px 0 0; color: #f5dec0; font-weight: 800; } |
| .tavern-app-shell { |
| position: relative; |
| isolation: isolate; |
| min-height: calc(100dvh - 132px); |
| width: min(100%, 1540px); |
| margin: 0 auto; |
| padding: clamp(10px, 1.4vw, 18px); |
| overflow: hidden; |
| border: 0; |
| border-radius: 0; |
| background: transparent; |
| box-shadow: none; |
| } |
| .tavern-app-shell > .styler, |
| .tavern-app-shell > .gr-group > .styler { |
| position: relative; |
| z-index: 2; |
| min-height: calc(100dvh - 170px); |
| display: grid !important; |
| grid-template-columns: minmax(220px, 250px) minmax(720px, 1fr) minmax(230px, 260px); |
| grid-template-rows: minmax(0, 1fr) auto auto; |
| grid-template-areas: |
| "left stage right" |
| "actions actions actions" |
| "status status status"; |
| gap: clamp(10px, 1.2vw, 16px); |
| align-items: stretch; |
| } |
| .tavern-scene-chrome { |
| position: absolute !important; |
| inset: 0; |
| z-index: 0; |
| padding: 0 !important; |
| margin: 0 !important; |
| pointer-events: none; |
| } |
| .tavern-scene-chrome .tavern-background, |
| .tavern-scene-chrome .tavern-ambient-lights { |
| position: absolute; |
| inset: 0; |
| pointer-events: none; |
| z-index: var(--z-bg); |
| } |
| .tavern-scene-chrome .tavern-background { |
| background: |
| linear-gradient(180deg, rgba(12,8,8,.08), rgba(12,8,8,.16)), |
| url("/gradio_api/file=assets/token-holdem/tavern-bg-main-1672x941.png") center center / cover no-repeat, |
| linear-gradient(180deg, rgba(93,86,82,.35), rgba(24,14,16,.72) 47%, rgba(23,12,8,.92)), |
| repeating-linear-gradient(0deg, rgba(255,255,255,.035) 0 2px, transparent 2px 44px), |
| repeating-linear-gradient(90deg, rgba(0,0,0,.16) 0 3px, transparent 3px 86px); |
| } |
| .tavern-stonework { position: absolute; inset: 0 0 42%; opacity: .82; } |
| .tavern-bar-backdrop { |
| position: absolute; |
| left: 21%; |
| right: 21%; |
| top: 7%; |
| height: clamp(70px, 10vw, 118px); |
| border: 4px solid rgba(47,25,16,.86); |
| border-left-width: 24px; |
| border-right-width: 24px; |
| background: |
| radial-gradient(circle at 46% 66%, rgba(255,191,92,.28) 0 20px, transparent 21px), |
| radial-gradient(circle at 14% 52%, rgba(116,145,94,.48) 0 12px, transparent 13px), |
| radial-gradient(circle at 82% 54%, rgba(140,76,86,.5) 0 12px, transparent 13px), |
| linear-gradient(180deg, #59351f, #23120f); |
| box-shadow: 0 10px 0 rgba(17,9,7,.84), 0 32px 60px rgba(0,0,0,.35); |
| opacity: .12; |
| } |
| .tavern-floor { |
| position: absolute; |
| left: -8%; |
| right: -8%; |
| bottom: -9%; |
| height: 43%; |
| background: repeating-linear-gradient(90deg, rgba(104,58,30,.16) 0 48px, rgba(55,29,20,.20) 48px 54px); |
| transform: perspective(680px) rotateX(58deg); |
| transform-origin: bottom; |
| } |
| .tavern-scene-chrome .tavern-ambient-lights { |
| z-index: 1; |
| background: |
| radial-gradient(circle at 14% 23%, rgba(255,190,90,.24), transparent 13rem), |
| radial-gradient(circle at 86% 22%, rgba(255,202,118,.20), transparent 14rem), |
| radial-gradient(circle at 50% 47%, rgba(255,226,154,.13), transparent 23rem), |
| radial-gradient(ellipse at 50% 100%, rgba(10,5,6,.72), transparent 48%); |
| animation: ambient-glow 3.8s ease-in-out infinite alternate; |
| } |
| .tavern-left-rail, .tavern-right-rail, .poker-stage, .action-bar, .bottom-status-panel { |
| position: relative; |
| z-index: var(--z-hud); |
| min-width: 0; |
| } |
| .tavern-left-rail { grid-area: left; } |
| .tavern-right-rail { grid-area: right; } |
| .poker-stage { |
| grid-area: stage; |
| z-index: calc(var(--z-hud) + 1); |
| container-type: inline-size; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| min-height: 0; |
| } |
| .action-bar { grid-area: actions; z-index: var(--z-actions); } |
| .bottom-status-panel { grid-area: status; } |
| .rail-plaque { |
| padding: 14px; |
| border: 2px solid rgba(196,147,78,.75); |
| border-radius: 8px; |
| background: linear-gradient(180deg, rgba(68,38,23,.92), rgba(25,15,17,.94)); |
| box-shadow: 0 10px 24px rgba(0,0,0,.32), 0 0 0 2px rgba(255,211,122,.05) inset; |
| } |
| .rail-plaque h2 { margin: 3px 0 5px; color: var(--ember); font-size: 22px; letter-spacing: 0; } |
| .rail-plaque p { margin: 0; color: #f5dec0; font-size: 12px; line-height: 1.35; } |
| .rail-kicker { color: #b7f7c1; font-weight: 900; font-size: 10px; text-transform: uppercase; } |
| .tavern-panel, .action-panel { |
| max-width: 1120px; |
| margin: 10px auto; |
| padding: 12px; |
| border: 1px solid rgba(196,147,78,.72); |
| border-radius: 8px; |
| background: linear-gradient(180deg, rgba(42,24,32,.94), rgba(23,16,22,.94)); |
| box-shadow: 0 8px 22px rgba(0,0,0,.32), 0 0 0 2px rgba(255,211,122,.04) inset; |
| } |
| .tavern-app-shell .tavern-panel, |
| .tavern-app-shell .setup-panel, |
| .tavern-app-shell .log-panel, |
| .tavern-app-shell .review-panel, |
| .tavern-app-shell .status-scroll, |
| .tavern-app-shell .action-bar { |
| max-width: none; |
| width: 100%; |
| margin: 0 !important; |
| } |
| .tavern-app-shell .setup-panel { display: grid; gap: 8px; } |
| .tavern-app-shell .setup-panel .block, |
| .tavern-app-shell .setup-panel .form, |
| .tavern-app-shell .setup-panel .wrap, |
| .tavern-app-shell .setup-panel .styler, |
| .tavern-app-shell .action-bar .block, |
| .tavern-app-shell .action-bar .form, |
| .tavern-app-shell .action-bar .wrap, |
| .tavern-app-shell .action-bar .styler, |
| .tavern-app-shell .action-row, |
| .tavern-app-shell .action-row .styler { |
| border-color: rgba(196,147,78,.35) !important; |
| background: transparent !important; |
| box-shadow: none !important; |
| } |
| .tavern-app-shell label, |
| .tavern-app-shell .setup-panel span, |
| .tavern-app-shell .setup-panel .label-wrap { |
| color: #f5dec0 !important; |
| font-family: ui-monospace, monospace !important; |
| font-weight: 800 !important; |
| } |
| .tavern-app-shell input, |
| .tavern-app-shell textarea { |
| border: 1px solid rgba(255,211,122,.42) !important; |
| border-radius: 6px !important; |
| background: rgba(11, 8, 11, .82) !important; |
| color: #fff1c4 !important; |
| font-family: ui-monospace, monospace !important; |
| box-shadow: inset 0 0 0 1px rgba(255,211,122,.06) !important; |
| } |
| .tavern-app-shell input:focus, |
| .tavern-app-shell textarea:focus { |
| border-color: var(--ember) !important; |
| outline: 2px solid rgba(255,211,122,.28) !important; |
| outline-offset: 1px !important; |
| } |
| .status-scroll { |
| max-width: 1120px; |
| margin: 10px auto !important; |
| padding: 10px 12px; |
| border: 1px solid rgba(255,211,122,.65); |
| border-radius: 8px; |
| color: #fff1c4; |
| background: rgba(55,35,27,.86); |
| font-weight: 900; |
| } |
| .token-table-shell { width: min(100%, 1120px); margin: 10px auto 18px; } |
| .poker-stage .token-table-shell { width: 100%; margin: 0; } |
| .table-status-bar { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin: 0 auto 6px; color: #f3d7a2; font: 800 11px ui-monospace, monospace; opacity: .92; } |
| .table-status-bar span { min-height: 26px; display: inline-flex; align-items: center; padding: 2px 9px; border: 1px solid rgba(196,147,78,.48); border-radius: 6px; background: rgba(18,12,16,.70); box-shadow: 0 2px 0 rgba(0,0,0,.46); } |
| .token-table { |
| position: relative; |
| width: 100%; |
| max-width: 100%; |
| height: clamp(500px, 62.5cqw, 700px); |
| overflow: visible; |
| aspect-ratio: auto; |
| min-height: 0; |
| border: 0; |
| border-radius: 26px; |
| background: transparent; |
| box-shadow: none; |
| } |
| .tavern-scene { position: relative; } |
| .tavern-backdrop { position: absolute; inset: 0; z-index: 0; overflow: hidden; background: transparent; } |
| .tavern-backdrop::before { |
| content: ""; |
| position: absolute; |
| inset: 0 0 36%; |
| background: |
| repeating-linear-gradient(0deg, rgba(255,255,255,.035) 0 2px, transparent 2px 42px), |
| repeating-linear-gradient(90deg, rgba(0,0,0,.14) 0 3px, transparent 3px 86px); |
| opacity: .08; |
| } |
| .tavern-backdrop::after { |
| content: ""; |
| position: absolute; |
| left: 11%; |
| right: 11%; |
| top: 13%; |
| height: 58px; |
| border: 3px solid #3a2118; |
| border-left-width: 18px; |
| border-right-width: 18px; |
| background: |
| radial-gradient(circle at 14% 55%, #96623a 0 7px, transparent 8px), |
| radial-gradient(circle at 24% 55%, #62805e 0 8px, transparent 9px), |
| radial-gradient(circle at 70% 55%, #8f4e5b 0 7px, transparent 8px), |
| linear-gradient(180deg, #4b2a18, #261410); |
| box-shadow: 0 7px 0 #1a0f0d, 0 22px 34px rgba(0,0,0,.34); |
| opacity: .08; |
| } |
| .wall-stone { position: absolute; width: 120px; height: 38px; border: 2px solid rgba(0,0,0,.08); background: rgba(118,108,102,.04); } |
| .wall-stone-a { top: 7%; left: 6%; } |
| .wall-stone-b { top: 23%; right: 8%; width: 160px; } |
| .bartender-shadow { position: absolute; left: 50%; top: 17%; width: 52px; height: 72px; transform: translateX(-50%); border-radius: 18px 18px 8px 8px; background: linear-gradient(180deg, rgba(226,165,92,.20), rgba(42,24,20,.32)); box-shadow: 0 0 24px rgba(255,189,94,.10); opacity: .2; } |
| .floor-planks { position: absolute; left: -4%; right: -4%; bottom: 0; height: 36%; background: repeating-linear-gradient(90deg, rgba(92,51,28,.12) 0 42px, rgba(56,31,23,.14) 42px 47px); transform: perspective(500px) rotateX(58deg); transform-origin: bottom; opacity: .2; } |
| .candle { position: absolute; top: 25%; width: 13px; height: 40px; border-radius: 4px 4px 2px 2px; background: linear-gradient(180deg, #fff1c4, #b8763d); box-shadow: 0 0 22px 9px rgba(255,183,74,.22); } |
| .candle::before { content: ""; position: absolute; left: 3px; top: -13px; width: 7px; height: 15px; border-radius: 50% 50% 45% 45%; background: #ffd37a; box-shadow: 0 0 18px 8px rgba(255,171,74,.45); animation: candle-flicker 1.9s ease-in-out infinite alternate; } |
| .candle-left { left: 5%; } |
| .candle-right { right: 5%; animation-delay: .6s; } |
| .table-felt { |
| position: absolute; |
| inset: 13% 4% 7%; |
| z-index: var(--z-table); |
| border: clamp(18px, 3.1cqw, 34px) solid var(--walnut); |
| border-radius: 50%; |
| background: |
| linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px), |
| linear-gradient(0deg, rgba(0,0,0,.085) 1px, transparent 1px), |
| repeating-linear-gradient(45deg, rgba(255,255,255,.035) 0 3px, transparent 3px 9px), |
| repeating-linear-gradient(-45deg, rgba(0,0,0,.055) 0 4px, transparent 4px 12px), |
| radial-gradient(ellipse at 50% 45%, rgba(255,255,255,.10), transparent 28%), |
| radial-gradient(ellipse at center, #2c6a49 0%, #24563a 48%, #14351f 76%); |
| background-size: 10px 10px, 10px 10px, auto, auto, auto, auto; |
| image-rendering: pixelated; |
| box-shadow: |
| 0 0 0 4px #2a130c, |
| 0 0 0 9px #a46e39, |
| 0 0 0 14px #2f1710, |
| 0 0 42px rgba(0,0,0,.78) inset, |
| 0 26px 46px rgba(0,0,0,.62); |
| } |
| .table-felt::before { |
| content: ""; |
| position: absolute; |
| inset: -26px; |
| border-radius: 50%; |
| background: |
| repeating-linear-gradient(90deg, rgba(255,211,122,.10) 0 7px, rgba(42,19,10,.16) 7px 14px), |
| linear-gradient(180deg, rgba(117,65,32,.80), rgba(45,22,13,.92)); |
| z-index: -1; |
| } |
| .table-felt::after { content: ""; position: absolute; inset: 14%; border: 2px dashed rgba(255,211,122,.20); border-radius: 50%; box-shadow: 0 0 0 1px rgba(0,0,0,.12); } |
| .table-chip-pile { |
| position: absolute; |
| left: 50%; |
| top: 39%; |
| z-index: calc(var(--z-table) + 8); |
| transform: translate(-50%, -50%); |
| width: 92px; |
| height: 42px; |
| pointer-events: none; |
| } |
| .table-chip-pile span { |
| position: absolute; |
| bottom: 7px; |
| width: 22px; |
| height: 26px; |
| border-radius: 6px 6px 4px 4px; |
| background: repeating-linear-gradient(0deg, #bd4a2e 0 4px, #ffd37a 4px 6px, #58301f 6px 8px); |
| box-shadow: 2px 3px 0 rgba(0,0,0,.54), 0 0 0 2px rgba(255,241,196,.08) inset; |
| } |
| .table-chip-pile span:nth-child(1) { left: 18px; height: 32px; background: repeating-linear-gradient(0deg, #bd4a2e 0 4px, #ffd37a 4px 6px, #58301f 6px 8px); } |
| .table-chip-pile span:nth-child(2) { left: 38px; height: 38px; background: repeating-linear-gradient(0deg, #4f9565 0 4px, #fff1c4 4px 6px, #1f432d 6px 8px); } |
| .table-chip-pile span:nth-child(3) { left: 58px; height: 30px; background: repeating-linear-gradient(0deg, #6e5a9d 0 4px, #fff1c4 4px 6px, #2c2346 6px 8px); } |
| .table-chip-pile b { |
| position: absolute; |
| left: 50%; |
| bottom: -15px; |
| transform: translateX(-50%); |
| padding: 2px 8px; |
| border: 1px solid rgba(255,211,122,.5); |
| border-radius: 5px; |
| color: #ffe29a; |
| background: rgba(19,12,13,.82); |
| font: 900 11px ui-monospace, monospace; |
| } |
| .table-chip-pile-empty { opacity: .45; top: 42%; } |
| .token-table-empty { display: grid; place-items: center; } |
| .center-hud { |
| position: absolute; |
| left: 50%; |
| top: 50%; |
| transform: translate(-50%, -50%); |
| z-index: calc(var(--z-table) + 12); |
| width: min(48%, 500px); |
| min-width: 360px; |
| padding: 10px 12px 12px; |
| text-align: center; |
| border: 2px solid var(--brass); |
| border-radius: 8px; |
| background: rgba(20, 14, 16, .66); |
| box-shadow: 0 10px 20px rgba(0,0,0,.42), 0 0 0 3px rgba(255,211,122,.05) inset; |
| } |
| .hud-kicker { color: #d1b37c; font: 700 11px ui-monospace, monospace; text-transform: uppercase; } |
| .current-actor { margin-top: 2px; color: #fff1c4; font: 900 18px ui-monospace, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| .pot { color: #ffe29a; font: 900 24px ui-monospace, monospace; text-shadow: 0 0 12px rgba(255,211,122,.3); } |
| .street { color: #b7f7c1; font: 800 13px ui-monospace, monospace; margin-bottom: 8px; } |
| .result-banner { margin-top: 8px; padding: 7px 9px; border-radius: 6px; color: #120b17; background: linear-gradient(180deg, #ffe29a, #dca861); font: 900 12px ui-monospace, monospace; box-shadow: 0 3px 0 #5b331c; } |
| .card-row { position: relative; z-index: calc(var(--z-table) + 14); display: flex; justify-content: center; align-items: center; gap: clamp(5px, .8cqw, 10px); min-height: clamp(52px, 8cqw, 78px); } |
| .card, .mini-card { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; background: linear-gradient(180deg, #fff9e8, #ead7b4); color: #1b1520; border: 3px solid #3b2a24; border-radius: 4px; font-weight: 900; font-family: ui-monospace, monospace; box-shadow: 4px 4px 0 #000, 0 0 0 2px rgba(255,255,255,.18) inset; image-rendering: pixelated; } |
| .card { width: clamp(40px, 5.2cqw, 58px); height: clamp(56px, 7.3cqw, 78px); font-size: clamp(15px, 2cqw, 20px); } |
| .mini-card { width: 28px; height: 38px; font-size: 12px; } |
| .red-card { color: #a72828; } |
| .hidden-card { background: |
| radial-gradient(circle at 50% 50%, rgba(255,211,122,.22), transparent 13px), |
| repeating-linear-gradient(45deg, #5b331c 0 4px, #20122f 4px 8px, #33214d 8px 12px); |
| color: var(--ember); |
| } |
| .card-slot { background: rgba(255,241,212,.13); border-style: dashed; box-shadow: none; color: transparent; } |
| .seat-layer { position: absolute; inset: 0; z-index: var(--z-avatars); pointer-events: none; } |
| .seat-ring { position: absolute; inset: 0; z-index: var(--z-avatars); pointer-events: none; } |
| .seat { |
| position: absolute; |
| z-index: var(--z-avatars); |
| left: var(--seat-x); |
| top: var(--seat-y); |
| transform: translate(-50%, -50%); |
| width: clamp(132px, 14cqw, 206px); |
| min-height: 184px; |
| display: block; |
| padding: 0; |
| text-align: center; |
| border: 0; |
| border-radius: 0; |
| background: transparent; |
| box-shadow: none; |
| transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease, opacity 180ms ease, filter 180ms ease; |
| pointer-events: auto; |
| } |
| .seat-2, .seat-6 { width: clamp(124px, 12.2cqw, 178px); } |
| .human-seat { width: clamp(190px, 20cqw, 286px); min-height: 216px; border-color: rgba(255,211,122,.8); } |
| .seat-layer > .human-seat { z-index: calc(var(--z-avatars) + 2); } |
| .avatar-layer { position: relative; z-index: 2; min-width: 0; height: clamp(112px, 13cqw, 184px); pointer-events: none; } |
| .human-seat .avatar-layer { height: clamp(138px, 16cqw, 222px); } |
| .avatar-slot { |
| width: 100%; |
| height: 100%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| overflow: visible; |
| border-radius: 0; |
| border: 0; |
| background: transparent; |
| box-shadow: none; |
| color: #130e12; |
| font: 900 13px ui-monospace, monospace; |
| text-shadow: 0 1px rgba(255,255,255,.28); |
| } |
| .avatar-slot img { |
| width: 118%; |
| height: 118%; |
| object-fit: contain; |
| display: block; |
| image-rendering: auto; |
| filter: drop-shadow(0 12px 9px rgba(0,0,0,.62)) drop-shadow(0 0 12px color-mix(in srgb, var(--avatar-accent, #dca861), transparent 72%)); |
| } |
| .avatar-slot img + span { display: none; } |
| .seat-plaque, .hud-layer, .seat-copy, .seat-meta { min-width: 0; } |
| .seat-plaque { |
| position: relative; |
| z-index: 3; |
| margin: -20px auto 0; |
| width: min(100%, 178px); |
| padding: 7px 8px 8px; |
| border: 2px solid rgba(196,147,78,.78); |
| border-radius: 6px; |
| background: linear-gradient(180deg, rgba(51,31,24,.94), rgba(18,12,16,.96)); |
| box-shadow: 0 4px 0 rgba(0,0,0,.72), 0 0 0 2px rgba(255,211,122,.05) inset; |
| } |
| .human-seat .seat-plaque { width: min(100%, 210px); } |
| .seat-label { min-height: 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 3px; overflow: hidden; } |
| .seat-index { display: none; } |
| .persona-role { display: none; } |
| .seat-name { color: var(--ember); font: 900 13px ui-monospace, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| .seat-persona { color: #f5dec0; font: 800 10px ui-monospace, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| .llm-seat .seat-persona { display: none; } |
| .seat-stack { color: #b7f7c1; font: 900 11px ui-monospace, monospace; white-space: nowrap; } |
| .seat-meta { display: grid; grid-template-columns: 1fr; gap: 3px; align-items: center; margin-top: 4px; } |
| .seat-bet, .seat-status, .seat-badge, .seat-last-action { display: inline-flex; align-items: center; min-height: 18px; padding: 1px 5px; border-radius: 5px; font: 900 10px ui-monospace, monospace; } |
| .seat-bet { justify-content: center; color: #f5dec0; background: #37231b; } |
| .seat-status { color: #121014; background: #b7f7c1; } |
| .seat-status { justify-content: center; } |
| .seat-badge { color: #120b17; background: var(--ember); border: 1px solid var(--ember); } |
| .seat-last-action { display: none; } |
| .seat > .speech-bubble-layer { grid-column: 1 / 3; } |
| .seat-talk { display: none; } |
| .seat-cards { display: flex; justify-content: center; gap: 5px; min-height: 34px; margin-top: 6px; } |
| .active-seat .seat-plaque { border-color: var(--ember); box-shadow: 0 0 0 3px rgba(255,211,122,.20), 0 0 22px rgba(255,211,122,.42), 0 4px 0 rgba(0,0,0,.72); } |
| .active-seat .seat-status { background: var(--ember); } |
| .active-seat .avatar-slot { animation: active-seat-glow 1.8s ease-in-out infinite alternate; } |
| .winner-seat .seat-plaque { border-color: var(--ember); box-shadow: 0 0 0 3px rgba(255,211,122,.24), 0 0 30px rgba(255,211,122,.56), 0 4px 0 rgba(0,0,0,.72); } |
| .winner-seat .seat-status { background: var(--ember); } |
| .folded-seat { opacity: .58; filter: grayscale(.55) saturate(.65); } |
| .all-in-seat .seat-plaque { border-color: #b7f7c1; } |
| .empty-seat { opacity: .62; } |
| .muted { opacity: .74; } |
| .dealer-button { position: absolute; left: var(--dealer-x); top: var(--dealer-y); transform: translate(-50%, -50%); z-index: calc(var(--z-table) + 5); width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; border: 3px solid #4b2a18; color: #1b1520; background: radial-gradient(circle at 36% 28%, #fff1c4, #ffe29a 44%, #c4934e 78%); font: 900 16px ui-monospace, monospace; box-shadow: 2px 3px 0 #000, 0 0 12px rgba(255,211,122,.42); transition: left 220ms ease, top 220ms ease; animation: dealer-pulse 1.8s ease-in-out infinite; } |
| .table-speech-layer { position: absolute; inset: 0; z-index: var(--z-speech); pointer-events: none; } |
| .speech-bubble { |
| position: absolute; |
| left: var(--bubble-x); |
| top: var(--bubble-y); |
| transform: translate(-50%, -100%); |
| width: clamp(142px, 16cqw, 210px); |
| max-height: 76px; |
| overflow: hidden; |
| padding: 8px 10px; |
| border: 2px solid var(--avatar-accent, var(--brass)); |
| border-radius: 8px; |
| color: #fff1c4; |
| background: rgba(22, 14, 17, .96); |
| box-shadow: 0 10px 20px rgba(0,0,0,.42), 0 0 18px color-mix(in srgb, var(--avatar-accent, #dca861), transparent 70%); |
| animation: speech-float 420ms ease-out both; |
| } |
| .speech-bubble strong { display: block; color: var(--ember); font-size: 10px; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| .speech-bubble span { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; color: #f5dec0; font-size: 11px; line-height: 1.25; overflow: hidden; } |
| .tavern-log { max-height: 360px; overflow: auto; color: #f5dec0; } |
| .tavern-log ul { margin: 8px 0 0 18px; padding: 0; } |
| .tavern-log li { margin: 3px 0; } |
| .log-board, .log-talk-title { color: var(--ember); font-weight: 900; } |
| .log-talk { border-top: 1px solid rgba(196,147,78,.36); padding-top: 8px !important; } |
| .tavern-ledger { overflow-x: auto; } |
| .tavern-ledger table { width: 100%; min-width: 820px; border-collapse: collapse; color: #f8e8c8; } |
| .tavern-ledger th, .tavern-ledger td { padding: 8px 10px; border-bottom: 1px solid rgba(196,147,78,.25); text-align: right; white-space: nowrap; } |
| .tavern-ledger th:nth-child(2), .tavern-ledger td:nth-child(2), .tavern-ledger th:nth-child(3), .tavern-ledger td:nth-child(3) { text-align: left; } |
| .tavern-ledger th { color: var(--ember); background: rgba(91,51,28,.45); } |
| .runtime-report { overflow-x: auto; color: #f8e8c8; } |
| .runtime-report h3 { margin: 0 0 10px; color: var(--ember); } |
| .runtime-report table { width: 100%; min-width: 920px; border-collapse: collapse; } |
| .runtime-report th, .runtime-report td { padding: 7px 9px; border-bottom: 1px solid rgba(196,147,78,.24); text-align: left; vertical-align: top; } |
| .runtime-report th { color: var(--ember); background: rgba(91,51,28,.45); } |
| .runtime-report code { color: #b7f7c1; white-space: nowrap; } |
| .hall-wall { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; } |
| .hall-plaque { padding: 12px; border: 1px solid var(--brass); border-radius: 8px; background: linear-gradient(180deg, rgba(91,51,28,.75), rgba(42,24,32,.75)); box-shadow: 0 3px 0 #110b0f; } |
| .hall-plaque span { display: block; color: #d1b37c; font-size: 12px; text-transform: uppercase; } |
| .hall-plaque strong { display: block; margin-top: 4px; color: #fff1c4; } |
| button { min-height: 44px; font-family: ui-monospace, monospace !important; border-radius: 8px !important; } |
| button:hover:not(:disabled) { filter: brightness(1.08); transform: translateY(-1px); } |
| button:active:not(:disabled) { transform: translateY(1px); } |
| button:focus-visible { outline: 3px solid var(--ember) !important; outline-offset: 2px !important; } |
| .setup-panel button { |
| border: 1px solid rgba(255,211,122,.48) !important; |
| background: linear-gradient(180deg, rgba(91,51,28,.95), rgba(42,24,32,.96)) !important; |
| color: #fff1c4 !important; |
| font-weight: 900 !important; |
| opacity: 1 !important; |
| } |
| .setup-panel button:disabled { |
| color: rgba(255,241,196,.55) !important; |
| border-color: rgba(196,147,78,.28) !important; |
| background: linear-gradient(180deg, rgba(55,35,27,.76), rgba(28,20,24,.82)) !important; |
| } |
| .setup-panel button.primary, |
| .setup-panel button[class*="primary"] { |
| background: linear-gradient(180deg, #ff8b31, #b94a1d) !important; |
| border-color: #ffd37a !important; |
| color: #fff7d7 !important; |
| } |
| .action-panel { position: sticky; bottom: 8px; z-index: 20; } |
| .action-bar { |
| padding: 10px; |
| border: 2px solid rgba(196,147,78,.72); |
| border-radius: 8px; |
| background: linear-gradient(180deg, rgba(46,28,22,.96), rgba(17,11,14,.98)); |
| box-shadow: 0 10px 22px rgba(0,0,0,.36), 0 0 0 2px rgba(255,211,122,.05) inset; |
| } |
| .action-status { |
| margin-bottom: 8px !important; |
| text-align: center; |
| font-size: 16px; |
| color: #ffe29a !important; |
| text-transform: none; |
| letter-spacing: 0; |
| } |
| .action-status p { margin: 0; color: #ffe29a !important; } |
| .action-bar-caption { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 8px; color: #d1b37c; font: 800 11px ui-monospace, monospace; text-transform: uppercase; } |
| .action-bar-caption strong { color: var(--ember); font-size: 13px; } |
| .action-row { display: grid !important; grid-template-columns: repeat(7, minmax(92px, 1fr)); gap: 8px; } |
| .action-row > *, |
| .action-row > .form, |
| .action-row > .block { |
| min-width: 0 !important; |
| border: 0 !important; |
| background: transparent !important; |
| box-shadow: none !important; |
| } |
| .poker-action button, button.poker-action { min-width: 0; font-weight: 900 !important; opacity: 1 !important; } |
| .fold-action button, button.fold-action { background: linear-gradient(180deg, #d75b4d, var(--danger)) !important; color: #fff3e4 !important; border-color: #ffc0a8 !important; } |
| .check-action button, button.check-action { background: linear-gradient(180deg, #55473e, #31272b) !important; color: #f5dec0 !important; border-color: #9a8060 !important; } |
| .call-action button, button.call-action { background: linear-gradient(180deg, #83d68b, var(--confirm)) !important; color: #10140f !important; border-color: #d7ffd6 !important; } |
| .raise-action button, button.raise-action { background: linear-gradient(180deg, #f0a84c, var(--raise)) !important; color: #171008 !important; border-color: #ffe2a0 !important; } |
| .all-in-action button, button.all-in-action { background: linear-gradient(180deg, #b59cff, var(--all-in)) !important; color: #fff7d7 !important; border-color: #ffe29a !important; } |
| @keyframes candle-flicker { |
| from { transform: translateY(0) scaleX(1); opacity: .78; } |
| to { transform: translateY(-1px) scaleX(.82); opacity: 1; } |
| } |
| @keyframes ambient-glow { |
| from { opacity: .78; } |
| to { opacity: 1; } |
| } |
| @keyframes speech-float { |
| from { opacity: 0; transform: translate(-50%, -86%) scale(.96); } |
| to { opacity: 1; transform: translate(-50%, -100%) scale(1); } |
| } |
| @keyframes dealer-pulse { |
| 0%, 100% { box-shadow: 2px 3px 0 #000, 0 0 10px rgba(255,211,122,.34); } |
| 50% { box-shadow: 2px 3px 0 #000, 0 0 20px rgba(255,211,122,.7); } |
| } |
| @keyframes active-seat-glow { |
| from { filter: brightness(1); } |
| to { filter: brightness(1.18); } |
| } |
| @media (prefers-reduced-motion: reduce) { |
| .seat, .dealer-button, button, .candle::before, .speech-bubble, .avatar-slot, .tavern-ambient-lights { transition: none; animation: none; } |
| } |
| @media (max-width: 1240px) { |
| .tavern-app-shell > .styler, |
| .tavern-app-shell > .gr-group > .styler { |
| grid-template-columns: minmax(190px, 23vw) minmax(460px, 1fr); |
| grid-template-areas: |
| "left stage" |
| "right stage" |
| "actions actions" |
| "status status"; |
| } |
| .tavern-right-rail .tavern-log { max-height: 220px; } |
| } |
| @media (max-width: 980px) { |
| .tavern-app-shell > .styler, |
| .tavern-app-shell > .gr-group > .styler { |
| min-height: auto; |
| grid-template-columns: 1fr; |
| grid-template-areas: |
| "left" |
| "stage" |
| "right" |
| "actions" |
| "status"; |
| } |
| .tavern-left-rail .setup-panel { grid-template-columns: repeat(2, minmax(0, 1fr)); } |
| .rail-plaque { display: none; } |
| .token-table { height: clamp(600px, 86cqw, 660px); min-height: 0; aspect-ratio: auto; } |
| .table-felt { inset: 3% 4% 21%; border-width: 12px; } |
| .center-hud { top: 38%; width: min(76%, 460px); min-width: 280px; } |
| .seat { width: 136px; min-height: 128px; padding: 6px; grid-template-columns: 36px minmax(0, 1fr); } |
| .seat-talk { display: none; } |
| .human-seat { width: min(78%, 320px); top: 82% !important; } |
| .avatar-slot { width: 36px; height: 36px; } |
| .mini-card { width: 24px; height: 34px; } |
| .action-row { grid-template-columns: repeat(4, minmax(112px, 1fr)); } |
| } |
| @media (max-width: 680px) { |
| .app-title { padding: 12px; } |
| .tavern-app-shell { padding: 7px; border-left: 0; border-right: 0; } |
| .tavern-app-shell > .styler, |
| .tavern-app-shell > .gr-group > .styler { gap: 8px; } |
| .tavern-left-rail .setup-panel { grid-template-columns: 1fr; } |
| .token-table-shell { margin-top: 6px; } |
| .table-status-bar { justify-content: flex-start; overflow-x: auto; padding-bottom: 3px; } |
| .token-table { height: min(760px, max(640px, calc(100dvh - 84px))); min-height: 0; border-width: 4px; } |
| .table-felt { inset: 6% 3% 32%; border-width: 10px; } |
| .center-hud { top: 27%; width: calc(100% - 28px); min-width: 0; padding: 10px; } |
| .seat-ring { position: absolute; left: 10px; right: 10px; top: 43%; bottom: 172px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; overflow: auto; align-content: start; } |
| .seat { position: relative; left: auto; top: auto; transform: none; width: auto; min-height: 104px; } |
| .human-seat { position: absolute; left: 50%; right: auto; top: auto !important; bottom: 10px; transform: translateX(-50%); width: calc(100% - 28px); min-height: 124px; } |
| .avatar-layer { height: 86px; } |
| .human-seat .avatar-layer { height: 104px; } |
| .avatar-slot img { width: 112%; height: 112%; } |
| .seat-plaque { margin-top: -12px; padding: 5px 6px 6px; } |
| .seat-0 { order: 8; } |
| .dealer-button { width: 30px; height: 30px; font-size: 14px; } |
| .table-speech-layer { display: none; } |
| .current-actor { font-size: 15px; } |
| .pot { font-size: 19px; } |
| .seat-name { font-size: 12px; } |
| .persona-role { max-width: 76px; } |
| .action-row { grid-template-columns: repeat(2, minmax(126px, 1fr)); } |
| .tavern-panel, .action-panel { padding: 9px; } |
| } |
| """ |
|
|