diff --git a/internal/games/blackjack/blackjack.go b/internal/games/blackjack/blackjack.go index e5a3883..d1a44a3 100644 --- a/internal/games/blackjack/blackjack.go +++ b/internal/games/blackjack/blackjack.go @@ -15,6 +15,7 @@ package blackjack import ( + "encoding/json" "errors" "math" "math/rand/v2" @@ -29,15 +30,21 @@ var ( ErrNotYourTurn = errors.New("blackjack: it is not the player's turn to act") ErrUnknownMove = errors.New("blackjack: unknown move") ErrCantDouble = errors.New("blackjack: double is only allowed on the opening two cards") + ErrCantSplit = errors.New("blackjack: split is only allowed on two cards of the same rank") ErrDeckExhausted = errors.New("blackjack: the shoe is empty") ErrBadBet = errors.New("blackjack: bet must be positive") ) +// MaxHands is how many hands one deal can turn into: the opening hand, plus +// three splits. Four is the usual house limit and it is also the point past +// which the felt runs out of room. +const MaxHands = 4 + // Phase is whose turn it is. type Phase string const ( - PhasePlayer Phase = "player" // the player is acting + PhasePlayer Phase = "player" // the player is acting on the active hand PhaseDealer Phase = "dealer" // transient: the dealer is drawing out PhaseDone Phase = "done" // settled, Outcome and Payout are final ) @@ -72,37 +79,77 @@ type Rules struct { // DefaultRules match the blackjack gogobee has been dealing in Matrix for years: // six decks, 3:2 on a natural, dealer hits soft 17. The rake is the one new term -// — see Settle for exactly what it touches. +// — see settle for exactly what it touches. func DefaultRules() Rules { return Rules{Decks: 6, BlackjackPays: 1.5, DealerHitsSoft17: true, RakePct: 0.05} } -// State is one hand of heads-up blackjack: one player, one dealer. Splitting -// isn't in v1, so there's exactly one player hand. +// Hand is one hand the player is holding, with the chips that are on it. +// +// A deal starts with one. A split turns one into two, and the new hand carries a +// bet of its own — which is the whole reason split is not just a card trick: it +// is the only move in the game that takes more chips out of a player's stack +// *after* the cards are out. +type Hand struct { + Cards []cards.Card `json:"cards"` + Bet int64 `json:"bet"` + Doubled bool `json:"doubled"` + + // Split marks a hand that came out of a split. It exists for one rule: 21 on + // a split hand is twenty-one, not a natural, and is paid 1:1 like any other. + // Otherwise splitting aces would print money. + Split bool `json:"split"` + + // Done means this hand will not be acted on again: it stood, it busted, it + // doubled, or it is a split ace, which gets exactly one card and no say. + Done bool `json:"done"` + + Outcome Outcome `json:"outcome"` + Payout int64 `json:"payout"` // stake plus winnings, net of rake. Zero on a loss. + Rake int64 `json:"rake"` +} + +// Value totals the hand. See HandValue. +func (h Hand) Value() (int, bool) { return HandValue(h.Cards) } + +// Natural reports a blackjack: 21 on the opening two cards of a hand that was +// dealt, not split. +func (h Hand) Natural() bool { return !h.Split && IsBlackjack(h.Cards) } + +// State is one deal of heads-up blackjack: the player's hands against the +// dealer's one. type State struct { Rules Rules `json:"rules"` Deck cards.Deck `json:"deck"` // the shoe, top card first — never shown to the browser - Player []cards.Card `json:"player"` Dealer []cards.Card `json:"dealer"` - Bet int64 `json:"bet"` // chips at risk; doubles on a double-down - Doubled bool `json:"doubled"` + // Hands is always at least one, and Active indexes the one being played. The + // player works left to right: a hand is finished before the next is looked at, + // which is both how a real table does it and what keeps the felt legible. + Hands []Hand `json:"hands"` + Active int `json:"active"` Phase Phase `json:"phase"` - Outcome Outcome `json:"outcome"` + Outcome Outcome `json:"outcome"` // the deal as a whole; per-hand outcomes live on the hands - // Payout is what returns to the player's chip stack when the hand is done: - // stake plus winnings, net of rake. Zero on a loss. Rake is the house's cut, - // recorded so the ledger can account for every chip that left the table. + // Bet, Payout and Rake are the totals across every hand: what the player has + // staked, what comes back, and what the house kept. The ledger and the chip + // stack only ever deal in these. + Bet int64 `json:"bet"` Payout int64 `json:"payout"` Rake int64 `json:"rake"` } // Event is something the table can narrate or animate. The engine emits them // instead of drawing anything itself. +// +// Hand is which of the player's hands an event landed on — meaningless for the +// dealer's, and the reason it is here at all is that after a split the browser +// has to know which fan a card is flying to. type Event struct { - Kind string `json:"kind"` // "deal" | "player_card" | "dealer_card" | "reveal" | "settle" + Kind string `json:"kind"` // "deal" | "player_card" | "dealer_card" | "split" | "reveal" | "settle" Card *cards.Card `json:"card,omitempty"` + Hand int `json:"hand"` Text string `json:"text,omitempty"` } @@ -113,6 +160,7 @@ const ( Hit Move = "hit" Stand Move = "stand" Double Move = "double" + Split Move = "split" ) // HandValue totals a hand, counting each ace as 11 until that would bust, then @@ -161,36 +209,52 @@ func New(bet int64, r Rules, rng *rand.Rand) (State, []Event, error) { deck := cards.NewDeck(r.Decks) deck.Shuffle(rng) - s := State{Rules: r, Deck: deck, Bet: bet, Phase: PhasePlayer} + s := State{ + Rules: r, + Deck: deck, + Hands: []Hand{{Bet: bet}}, + Bet: bet, + Phase: PhasePlayer, + } evs := []Event{{Kind: "deal"}} for i := 0; i < 2; i++ { - if err := s.draw(&s.Player, "player_card", &evs); err != nil { + if err := s.hit(0, &evs); err != nil { return State{}, nil, err } - if err := s.draw(&s.Dealer, "dealer_card", &evs); err != nil { + if err := s.drawDealer(&evs); err != nil { return State{}, nil, err } } // A natural on either side ends it before the player ever acts. - if IsBlackjack(s.Player) || IsBlackjack(s.Dealer) { + if s.Hands[0].Natural() || IsBlackjack(s.Dealer) { s.settle(&evs) } return s, evs, nil } -// draw takes one card off the shoe onto the given hand and records the event. -// Pointer receiver: it mutates the deck and the hand together, and neither may -// end up applied to a stale copy of the state. -func (s *State) draw(hand *[]cards.Card, kind string, evs *[]Event) error { +// hit puts one card on a player hand. Pointer receiver: it mutates the deck and +// the hand together, and neither may end up applied to a stale copy of the state. +func (s *State) hit(i int, evs *[]Event) error { c, ok := s.Deck.Draw() if !ok { return ErrDeckExhausted } - *hand = append(*hand, c) + s.Hands[i].Cards = append(s.Hands[i].Cards, c) card := c - *evs = append(*evs, Event{Kind: kind, Card: &card}) + *evs = append(*evs, Event{Kind: "player_card", Card: &card, Hand: i}) + return nil +} + +func (s *State) drawDealer(evs *[]Event) error { + c, ok := s.Deck.Draw() + if !ok { + return ErrDeckExhausted + } + s.Dealer = append(s.Dealer, c) + card := c + *evs = append(*evs, Event{Kind: "dealer_card", Card: &card}) return nil } @@ -210,41 +274,122 @@ func ApplyMove(s State, m Move) (State, []Event, error) { if s.Phase != PhasePlayer { return s, nil, ErrNotYourTurn } - if m == Double && len(s.Player) != 2 { + switch m { + case Hit, Stand, Double, Split: + default: + return s, nil, ErrUnknownMove + } + if m == Double && !s.CanDouble() { // Doubling means doubling the stake for exactly one more card. Only ever // legal on the opening two — after that you're just describing a hit. return s, nil, ErrCantDouble } - if m != Hit && m != Stand && m != Double { - return s, nil, ErrUnknownMove + if m == Split && !s.CanSplit() { + return s, nil, ErrCantSplit } + i := s.Active evs := []Event{} - if m == Double { - s.Bet *= 2 - s.Doubled = true - } + switch m { + case Split: + // The second card moves to a hand of its own, carrying a bet the same size + // as the one it came from, and both hands are topped up to two cards. + h := &s.Hands[i] + moved := h.Cards[1] + h.Cards = h.Cards[:1] + h.Split = true - if m == Hit || m == Double { - if err := s.draw(&s.Player, "player_card", &evs); err != nil { + fresh := Hand{Cards: []cards.Card{moved}, Bet: h.Bet, Split: true} + s.Hands = append(s.Hands, Hand{}) + copy(s.Hands[i+2:], s.Hands[i+1:]) // the new hand sits immediately to the right + s.Hands[i+1] = fresh + s.Bet += fresh.Bet + + evs = append(evs, Event{Kind: "split", Hand: i}) + if err := s.hit(i, &evs); err != nil { return s, nil, err } - if v, _ := HandValue(s.Player); v > 21 { - s.settle(&evs) // bust; the dealer never has to play - return s, evs, nil + if err := s.hit(i+1, &evs); err != nil { + return s, nil, err } - if m == Hit { - return s, evs, nil // still the player's turn + + // Split aces get one card each and no say in it. Without this rule a pair + // of aces is the best hand in the game and everybody would split them + // forever; with it, splitting aces is a gamble like everything else. + if moved.Rank == cards.Ace { + s.Hands[i].Done = true + s.Hands[i+1].Done = true } + // A hand that has just been dealt a card can still be sitting on 21, and a + // 21 has nothing left to decide. + s.finishIfDone(i) + s.finishIfDone(i + 1) + + case Double: + h := &s.Hands[i] + s.Bet += h.Bet + h.Bet *= 2 + h.Doubled = true + // Announced before the card, because that is the order it happens in: the + // chips go down, and *then* you find out what you bought with them. + evs = append(evs, Event{Kind: "double", Hand: i}) + if err := s.hit(i, &evs); err != nil { + return s, nil, err + } + h.Done = true // one card, and that is the deal you made + + case Hit: + if err := s.hit(i, &evs); err != nil { + return s, nil, err + } + s.finishIfDone(i) + + case Stand: + s.Hands[i].Done = true } - // Stand, or a double that survived its card: the dealer draws out. - s.Phase = PhaseDealer - s.dealerPlay(&evs) + s.advance(&evs) return s, evs, nil } +// finishIfDone closes a hand that has nothing left to decide: it busted, or it +// is sitting on 21 and would only be hitting it to be polite. +func (s *State) finishIfDone(i int) { + if v, _ := s.Hands[i].Value(); v >= 21 { + s.Hands[i].Done = true + } +} + +// advance moves to the next hand still owed a decision. When there are none, the +// dealer plays — unless every hand busted, in which case there is nothing to beat +// and the dealer does not bother turning over. +func (s *State) advance(evs *[]Event) { + for i := s.Active; i < len(s.Hands); i++ { + if !s.Hands[i].Done { + s.Active = i + return + } + } + s.Active = len(s.Hands) - 1 + + if s.allBust() { + s.settle(evs) + return + } + s.Phase = PhaseDealer + s.dealerPlay(evs) +} + +func (s *State) allBust() bool { + for _, h := range s.Hands { + if v, _ := h.Value(); v <= 21 { + return false + } + } + return true +} + // dealerPlay draws the dealer out to the house rule, then settles. The dealer // has no choices to make — that's the game — so this needs no move. func (s *State) dealerPlay(evs *[]Event) { @@ -255,74 +400,104 @@ func (s *State) dealerPlay(evs *[]Event) { if v >= 17 && !hitSoft17 { break } - if err := s.draw(&s.Dealer, "dealer_card", evs); err != nil { + if err := s.drawDealer(evs); err != nil { break // shoe ran dry mid-draw; settle on what's on the table } } s.settle(evs) } -// settle decides the outcome and the payout, and is the only place chips are -// computed. +// settle decides every hand against the dealer and adds up what comes back. It +// is the only place chips are computed. // // The rake comes off winnings, never off the stake: a player who pushes gets // exactly their bet back, and a player who loses is never charged for the // privilege. The house only takes a cut of money the house was going to hand // over anyway. That's a rake, as opposed to a fee for showing up. +// +// Each hand is raked on its own winnings. Netting the hands against each other +// first would let a player who won one and lost one pay no rake at all, which is +// not a rake, it's a discount for splitting. func (s *State) settle(evs *[]Event) { - playerVal, _ := HandValue(s.Player) dealerVal, _ := HandValue(s.Dealer) - playerBJ := IsBlackjack(s.Player) dealerBJ := IsBlackjack(s.Dealer) - // profit is what the player wins on top of their stake. Negative means the - // stake is gone. - var profit int64 + s.Payout, s.Rake = 0, 0 + for i := range s.Hands { + h := &s.Hands[i] + playerVal, _ := h.Value() - switch { - case playerVal > 21: - s.Outcome = OutcomeBust - profit = -s.Bet - case playerBJ && dealerBJ: - s.Outcome = OutcomePush - case playerBJ: - s.Outcome = OutcomeBlackjack - profit = int64(math.Floor(float64(s.Bet) * s.Rules.BlackjackPays)) - case dealerBJ: - s.Outcome = OutcomeLose - profit = -s.Bet - case dealerVal > 21: - s.Outcome = OutcomeDealerBust - profit = s.Bet - case playerVal > dealerVal: - s.Outcome = OutcomeWin - profit = s.Bet - case playerVal == dealerVal: - s.Outcome = OutcomePush - default: - s.Outcome = OutcomeLose - profit = -s.Bet - } + // profit is what this hand wins on top of its stake. Negative means the + // stake is gone. + var profit int64 - if profit > 0 { - s.Rake = int64(math.Floor(float64(profit) * s.Rules.RakePct)) - if s.Rake < 0 { - s.Rake = 0 + switch { + case playerVal > 21: + h.Outcome = OutcomeBust + profit = -h.Bet + case h.Natural() && dealerBJ: + h.Outcome = OutcomePush + case h.Natural(): + h.Outcome = OutcomeBlackjack + profit = int64(math.Floor(float64(h.Bet) * s.Rules.BlackjackPays)) + case dealerBJ: + h.Outcome = OutcomeLose + profit = -h.Bet + case dealerVal > 21: + h.Outcome = OutcomeDealerBust + profit = h.Bet + case playerVal > dealerVal: + h.Outcome = OutcomeWin + profit = h.Bet + case playerVal == dealerVal: + h.Outcome = OutcomePush + default: + h.Outcome = OutcomeLose + profit = -h.Bet } - profit -= s.Rake - } - if profit < 0 { - s.Payout = 0 // stake is lost; nothing comes back - } else { - s.Payout = s.Bet + profit + + if profit > 0 { + h.Rake = int64(math.Floor(float64(profit) * s.Rules.RakePct)) + if h.Rake < 0 { + h.Rake = 0 + } + profit -= h.Rake + } + if profit < 0 { + h.Payout = 0 // stake is lost; nothing comes back + } else { + h.Payout = h.Bet + profit + } + + s.Payout += h.Payout + s.Rake += h.Rake } + s.Outcome = s.overall() s.Phase = PhaseDone *evs = append(*evs, Event{Kind: "settle", Text: string(s.Outcome)}) } -// Net is what the hand did to the player's chip stack: payout minus the stake -// they put up. Negative on a loss, zero on a push. +// overall is the deal's outcome as one word, which is what the ledger and the +// history line want. With one hand it is simply that hand's. With several there +// is no honest single word for "won one, lost one", so it reports what the deal +// did to the player's chips, which is the thing anybody actually means. +func (s State) overall() Outcome { + if len(s.Hands) == 1 { + return s.Hands[0].Outcome + } + switch net := s.Payout - s.Bet; { + case net > 0: + return OutcomeWin + case net < 0: + return OutcomeLose + default: + return OutcomePush + } +} + +// Net is what the deal did to the player's chip stack: everything paid out minus +// everything staked. Negative on a loss, zero on a push. func (s State) Net() int64 { if s.Phase != PhaseDone { return 0 @@ -330,17 +505,85 @@ func (s State) Net() int64 { return s.Payout - s.Bet } +// Hand returns the hand being played. There is always one. +func (s State) Hand() Hand { + if s.Active < 0 || s.Active >= len(s.Hands) { + return Hand{} + } + return s.Hands[s.Active] +} + // CanDouble reports whether Double is legal right now — the shell asks this to // decide whether to light the button up. func (s State) CanDouble() bool { - return s.Phase == PhasePlayer && len(s.Player) == 2 + h := s.Hand() + return s.Phase == PhasePlayer && !h.Done && len(h.Cards) == 2 } +// CanSplit reports whether Split is legal: two cards of the same rank, and room +// at the table for another hand. +// +// Same *rank*, not same value: a king and a queen are both worth ten and are not +// a pair, which is the stricter of the two house rules and the one that doesn't +// need explaining on the felt. +func (s State) CanSplit() bool { + h := s.Hand() + if s.Phase != PhasePlayer || h.Done || len(h.Cards) != 2 || len(s.Hands) >= MaxHands { + return false + } + return h.Cards[0].Rank == h.Cards[1].Rank +} + +// SplitCost is what splitting the active hand would take out of the player's +// stack: another bet the same size as the one already on it. The shell has to +// take these chips before the move, because a split the player cannot cover is +// not a legal move. +func (s State) SplitCost() int64 { return s.Hand().Bet } + +// DoubleCost is the same idea for a double: the stake again. +func (s State) DoubleCost() int64 { return s.Hand().Bet } + // clone deep-copies the slices so a derived state shares no backing array with // the one it came from. func (s State) clone() State { s.Deck = append(cards.Deck(nil), s.Deck...) - s.Player = append([]cards.Card(nil), s.Player...) s.Dealer = append([]cards.Card(nil), s.Dealer...) + hands := make([]Hand, len(s.Hands)) + copy(hands, s.Hands) + for i := range hands { + hands[i].Cards = append([]cards.Card(nil), hands[i].Cards...) + } + s.Hands = hands return s } + +// UnmarshalJSON reads a state, including one written before split existed. +// +// A live hand outlives a deploy: it is a blob in the database, and somebody is +// mid-hand when the new binary starts. Those blobs have a "player" array and no +// "hands", and without this they would come back as a state with no hands at all +// — which is not a decoding error, it's a player whose cards vanished. So the old +// shape is read as what it always was: one hand, holding the whole stake. +func (s *State) UnmarshalJSON(b []byte) error { + type state State // shed the method, or this recurses forever + var v struct { + state + Player []cards.Card `json:"player"` + } + if err := json.Unmarshal(b, &v); err != nil { + return err + } + *s = State(v.state) + if len(s.Hands) == 0 && len(v.Player) > 0 { + s.Hands = []Hand{{ + Cards: v.Player, + Bet: s.Bet, + Outcome: s.Outcome, + Payout: s.Payout, + Rake: s.Rake, + Done: s.Phase == PhaseDone, + }} + s.Active = 0 + } + return nil +} diff --git a/internal/games/blackjack/blackjack_test.go b/internal/games/blackjack/blackjack_test.go index d3ebe7d..2d4349f 100644 --- a/internal/games/blackjack/blackjack_test.go +++ b/internal/games/blackjack/blackjack_test.go @@ -60,7 +60,7 @@ func TestIsBlackjack_OnlyOnTwoCards(t *testing.T) { // deal so the payout math can be checked case by case. func settleWith(t *testing.T, r Rules, bet int64, player, dealer []cards.Card) State { t.Helper() - s := State{Rules: r, Bet: bet, Player: player, Dealer: dealer} + s := State{Rules: r, Bet: bet, Hands: []Hand{{Cards: player, Bet: bet}}, Dealer: dealer} evs := []Event{} s.settle(&evs) if s.Phase != PhaseDone { @@ -164,8 +164,8 @@ func TestNew_DealsFourCardsAndAskThePlayer(t *testing.T) { if err != nil { t.Fatal(err) } - if len(s.Player) != 2 || len(s.Dealer) != 2 { - t.Fatalf("dealt %d/%d cards, want 2/2", len(s.Player), len(s.Dealer)) + if len(s.Hands[0].Cards) != 2 || len(s.Dealer) != 2 { + t.Fatalf("dealt %d/%d cards, want 2/2", len(s.Hands[0].Cards), len(s.Dealer)) } if len(s.Deck) != 6*52-4 { t.Fatalf("shoe has %d cards left, want %d", len(s.Deck), 6*52-4) @@ -174,7 +174,7 @@ func TestNew_DealsFourCardsAndAskThePlayer(t *testing.T) { t.Fatal("no deal event") } // Unless somebody was dealt a natural, it's the player's move. - if !IsBlackjack(s.Player) && !IsBlackjack(s.Dealer) && s.Phase != PhasePlayer { + if !IsBlackjack(s.Hands[0].Cards) && !IsBlackjack(s.Dealer) && s.Phase != PhasePlayer { t.Fatalf("phase = %q, want %q", s.Phase, PhasePlayer) } } @@ -195,7 +195,7 @@ func TestNew_NaturalSettlesImmediately(t *testing.T) { if err != nil { t.Fatal(err) } - if IsBlackjack(s.Player) { + if IsBlackjack(s.Hands[0].Cards) { if s.Phase != PhaseDone { t.Fatalf("seed %d: player has a natural but phase = %q", seed, s.Phase) } @@ -225,7 +225,7 @@ func TestApplyMove_HitUntilBustSettles(t *testing.T) { if s.Phase != PhaseDone { t.Fatal("hitting a dozen times never ended the hand") } - if v, _ := HandValue(s.Player); v <= 21 { + if v, _ := HandValue(s.Hands[0].Cards); v <= 21 { t.Fatalf("player stopped at %d without busting — the loop should have gone over", v) } if s.Outcome != OutcomeBust || s.Payout != 0 { @@ -285,11 +285,11 @@ func TestApplyMove_DoubleTakesOneCardThenStands(t *testing.T) { if err != nil { t.Fatal(err) } - if !s.Doubled || s.Bet != 200 { - t.Fatalf("bet = %d doubled = %v, want 200/true", s.Bet, s.Doubled) + if !s.Hands[0].Doubled || s.Bet != 200 { + t.Fatalf("bet = %d doubled = %v, want 200/true", s.Bet, s.Hands[0].Doubled) } - if len(s.Player) != 3 { - t.Fatalf("player has %d cards after a double, want exactly 3", len(s.Player)) + if len(s.Hands[0].Cards) != 3 { + t.Fatalf("player has %d cards after a double, want exactly 3", len(s.Hands[0].Cards)) } if s.Phase != PhaseDone { t.Fatal("a double must end the player's turn") @@ -378,9 +378,9 @@ func TestNew_IsReproducibleFromItsSeed(t *testing.T) { if err != nil { t.Fatal(err) } - if cards.Hand(a.Player) != cards.Hand(b.Player) || cards.Hand(a.Dealer) != cards.Hand(b.Dealer) { + if cards.Hand(a.Hands[0].Cards) != cards.Hand(b.Hands[0].Cards) || cards.Hand(a.Dealer) != cards.Hand(b.Dealer) { t.Fatalf("same seed dealt different hands: %s/%s vs %s/%s", - cards.Hand(a.Player), cards.Hand(a.Dealer), cards.Hand(b.Player), cards.Hand(b.Dealer)) + cards.Hand(a.Hands[0].Cards), cards.Hand(a.Dealer), cards.Hand(b.Hands[0].Cards), cards.Hand(b.Dealer)) } } @@ -395,21 +395,21 @@ func TestApplyMove_DerivedStatesDoNotShareCards(t *testing.T) { if s.Phase == PhaseDone { t.Skip("dealt a natural") } - before := cards.Hand(s.Player) + before := cards.Hand(s.Hands[0].Cards) a, _, err := ApplyMove(s, Hit) if err != nil { t.Fatal(err) } - aHand := cards.Hand(a.Player) + aHand := cards.Hand(a.Hands[0].Cards) if _, _, err := ApplyMove(s, Hit); err != nil { // same start, applied again t.Fatal(err) } - if got := cards.Hand(a.Player); got != aHand { + if got := cards.Hand(a.Hands[0].Cards); got != aHand { t.Fatalf("the first hand changed under us: %q became %q", aHand, got) } - if got := cards.Hand(s.Player); got != before { + if got := cards.Hand(s.Hands[0].Cards); got != before { t.Fatalf("ApplyMove mutated the state it was given: %q became %q", before, got) } } diff --git a/internal/games/blackjack/split_test.go b/internal/games/blackjack/split_test.go new file mode 100644 index 0000000..d0fbb84 --- /dev/null +++ b/internal/games/blackjack/split_test.go @@ -0,0 +1,374 @@ +package blackjack + +import ( + "encoding/json" + "testing" + + "pete/internal/games/cards" +) + +// Split is the only move in blackjack that takes chips out of a player's stack +// after the cards are already out, so most of what can go wrong with it is money +// rather than cards. These tests are mostly about the money. + +// dealt builds a state mid-hand: the player holding `player`, the dealer showing +// `dealer`, and a shoe stacked with `shoe` so the next cards are known. It skips +// New() because a split needs a pair, and waiting for one out of a shuffled shoe +// is not a test, it's a slot machine. +func dealt(bet int64, player, dealer, shoe []cards.Card) State { + return State{ + Rules: DefaultRules(), + Deck: cards.Deck(shoe), + Dealer: dealer, + Hands: []Hand{{Cards: player, Bet: bet}}, + Bet: bet, + Phase: PhasePlayer, + } +} + +func TestSplitDealsTwoHandsAndTakesASecondBet(t *testing.T) { + s := dealt(100, + hand(8, 8), + hand(cards.King, 6), + hand(3, 9, 5, 5), // one card to each hand, then whatever the dealer needs + ) + if !s.CanSplit() { + t.Fatal("CanSplit says no to a pair of eights") + } + if s.SplitCost() != 100 { + t.Fatalf("SplitCost = %d, want the same bet again (100)", s.SplitCost()) + } + + s, evs, err := ApplyMove(s, Split) + if err != nil { + t.Fatal(err) + } + if len(s.Hands) != 2 { + t.Fatalf("split made %d hands, want 2", len(s.Hands)) + } + for i, h := range s.Hands { + if len(h.Cards) != 2 { + t.Errorf("hand %d holds %d cards after the split, want 2", i, len(h.Cards)) + } + if h.Cards[0].Rank != 8 { + t.Errorf("hand %d didn't keep an eight: %s", i, cards.Hand(h.Cards)) + } + if h.Bet != 100 { + t.Errorf("hand %d carries a bet of %d, want 100", i, h.Bet) + } + if !h.Split { + t.Errorf("hand %d isn't marked as split", i) + } + } + // The whole point, in one line: there is twice as much on the table as there + // was, and the shell has to have taken it. + if s.Bet != 200 { + t.Fatalf("total stake = %d after splitting a 100 hand, want 200", s.Bet) + } + if s.Active != 0 { + t.Fatalf("active hand = %d, want the left one first", s.Active) + } + + var split, cardsDealt int + for _, e := range evs { + switch e.Kind { + case "split": + split++ + case "player_card": + cardsDealt++ + } + } + if split != 1 || cardsDealt != 2 { + t.Fatalf("the split emitted %d split and %d card events, want 1 and 2", split, cardsDealt) + } +} + +// Split aces get one card each and no further say. Without that rule a pair of +// aces is the best hand in the game every single time. +func TestSplitAcesGetOneCardEachAndNoMore(t *testing.T) { + s := dealt(50, + hand(cards.Ace, cards.Ace), + hand(9, 7), // dealer sits on 16, has to draw + hand(cards.King, 9, 5), + ) + s, _, err := ApplyMove(s, Split) + if err != nil { + t.Fatal(err) + } + for i, h := range s.Hands { + if len(h.Cards) != 2 { + t.Errorf("split ace %d holds %d cards, want exactly 2", i, len(h.Cards)) + } + if !h.Done { + t.Errorf("split ace %d is still being asked for a decision", i) + } + } + // Both hands are finished the moment they're dealt, so the dealer plays and + // the deal settles without the player ever acting again. + if s.Phase != PhaseDone { + t.Fatalf("phase = %q after splitting aces, want the deal to have run to the end", s.Phase) + } +} + +// An ace and a ten on a split hand is twenty-one. It is not a blackjack, and the +// house does not pay 3:2 for it — which is the single most expensive rule in this +// file, because splitting aces makes 21s for a living. +func TestTwentyOneOnASplitHandIsNotANatural(t *testing.T) { + s := dealt(100, + hand(cards.Ace, cards.Ace), + hand(cards.King, 7), // dealer stands on 17 + hand(cards.King, cards.Queen), + ) + s, _, err := ApplyMove(s, Split) + if err != nil { + t.Fatal(err) + } + if s.Phase != PhaseDone { + t.Fatalf("phase = %q, want done", s.Phase) + } + for i, h := range s.Hands { + if v, _ := h.Value(); v != 21 { + t.Fatalf("hand %d is %d, want the 21 this test is about", i, v) + } + if h.Natural() { + t.Errorf("hand %d counts as a natural — a split 21 must not pay 3:2", i) + } + if h.Outcome != OutcomeWin { + t.Errorf("hand %d settled as %q, want a plain win", i, h.Outcome) + } + // 100 staked, 100 profit, 5% rake off the profit: 195 back, not 245. + if h.Payout != 195 { + t.Errorf("hand %d paid %d, want 195 (a 1:1 win less the rake), not a 3:2 payout", i, h.Payout) + } + } + if s.Payout != 390 { + t.Fatalf("the deal paid %d, want 390", s.Payout) + } +} + +// Same rank, not same value. A king and a queen are both worth ten and are not a +// pair. +func TestSplitNeedsAPairOfTheSameRank(t *testing.T) { + s := dealt(100, hand(cards.King, cards.Queen), hand(9, 9), hand(5)) + if s.CanSplit() { + t.Error("CanSplit says a king and a queen are a pair") + } + if _, _, err := ApplyMove(s, Split); err != ErrCantSplit { + t.Errorf("splitting K+Q gave %v, want ErrCantSplit", err) + } + + // And you cannot split a hand you have already hit. + s = dealt(100, hand(8, 8, 5), hand(9, 9), hand(5)) + if s.CanSplit() { + t.Error("CanSplit says yes to a three-card hand") + } +} + +func TestSplittingStopsAtFourHands(t *testing.T) { + // Every card an eight, so every hand splits again for as long as it's allowed. + shoe := make([]cards.Card, 0, 12) + for i := 0; i < 12; i++ { + shoe = append(shoe, cards.Card{Rank: 8, Suit: cards.Spades}) + } + s := dealt(100, hand(8, 8), hand(9, 7), shoe) + + for i := 0; i < MaxHands-1; i++ { + var err error + if s, _, err = ApplyMove(s, Split); err != nil { + t.Fatalf("split %d: %v", i+1, err) + } + } + if len(s.Hands) != MaxHands { + t.Fatalf("ended with %d hands, want %d", len(s.Hands), MaxHands) + } + if s.CanSplit() { + t.Fatalf("CanSplit says yes at %d hands", MaxHands) + } + if _, _, err := ApplyMove(s, Split); err != ErrCantSplit { + t.Errorf("the fifth split gave %v, want ErrCantSplit", err) + } + if s.Bet != 400 { + t.Errorf("four hands of 100 = %d staked, want 400", s.Bet) + } +} + +// Each hand is raked on its own winnings. Netting the hands against each other +// first would mean a player who wins one and loses one pays no rake at all, which +// is not a rake, it's a discount for splitting. +func TestEachSplitHandIsSettledAndRakedOnItsOwn(t *testing.T) { + s := dealt(100, + hand(8, 8), + hand(cards.King, 9), // dealer stands on 19 + // left hand gets a 2 (10, then it hits to 20 and stands); + // right hand gets a 3 (11) and will bust on a king. + hand(2, 3, cards.King, cards.King), + ) + s, _, err := ApplyMove(s, Split) + if err != nil { + t.Fatal(err) + } + if s, _, err = ApplyMove(s, Hit); err != nil { // left: 8+2+K = 20 + t.Fatal(err) + } + if v, _ := s.Hands[0].Value(); v != 20 { + t.Fatalf("left hand is %d, want 20", v) + } + if s, _, err = ApplyMove(s, Stand); err != nil { + t.Fatal(err) + } + if s.Active != 1 { + t.Fatalf("standing on the left hand left the active hand at %d, want 1", s.Active) + } + if s, _, err = ApplyMove(s, Hit); err != nil { // right: 8+3+K = 21... no: 8+3=11, +K = 21 + t.Fatal(err) + } + if s.Phase != PhaseDone { + t.Fatalf("phase = %q, want done", s.Phase) + } + + left, right := s.Hands[0], s.Hands[1] + if left.Outcome != OutcomeWin { // 20 beats 19 + t.Errorf("left hand settled %q, want a win", left.Outcome) + } + if right.Outcome != OutcomeWin { // 21 beats 19 + t.Errorf("right hand settled %q, want a win", right.Outcome) + } + // Two 100 hands, each winning 100, each raked 5 on its own profit. + if left.Rake != 5 || right.Rake != 5 || s.Rake != 10 { + t.Errorf("rake = %d/%d (total %d), want 5/5 (10) — each hand raked on its own winnings", left.Rake, right.Rake, s.Rake) + } + if s.Payout != 390 || s.Net() != 190 { + t.Errorf("payout = %d net = %d, want 390 and 190", s.Payout, s.Net()) + } +} + +// A hand that busts loses its own bet and nothing else. The other hand is a +// separate bet and settles on its own merits. +func TestBustingOneSplitHandDoesNotTouchTheOther(t *testing.T) { + s := dealt(100, + hand(9, 9), + hand(cards.King, 8), // dealer stands on 18 + hand(5, 2, cards.King, cards.King), + ) + s, _, err := ApplyMove(s, Split) // left: 9+5=14, right: 9+2=11 + if err != nil { + t.Fatal(err) + } + if s, _, err = ApplyMove(s, Hit); err != nil { // left: 14+K = 24, bust + t.Fatal(err) + } + if s.Hands[0].Outcome != "" && s.Hands[0].Outcome != OutcomeBust { + t.Fatalf("left hand outcome %q before settling", s.Hands[0].Outcome) + } + if !s.Hands[0].Done || s.Active != 1 { + t.Fatalf("a bust hand didn't hand over: done=%v active=%d", s.Hands[0].Done, s.Active) + } + if s, _, err = ApplyMove(s, Hit); err != nil { // right: 11+K = 21 + t.Fatal(err) + } + + if s.Hands[0].Outcome != OutcomeBust || s.Hands[0].Payout != 0 { + t.Errorf("left hand = %q paying %d, want a bust paying nothing", s.Hands[0].Outcome, s.Hands[0].Payout) + } + if s.Hands[1].Outcome != OutcomeWin || s.Hands[1].Payout != 195 { + t.Errorf("right hand = %q paying %d, want a win paying 195", s.Hands[1].Outcome, s.Hands[1].Payout) + } + // Staked 200, got 195 back: down 5 on the deal, and the word for that is lose. + if s.Payout != 195 || s.Net() != -5 || s.Outcome != OutcomeLose { + t.Errorf("deal settled %q paying %d (net %d), want lose/195/-5", s.Outcome, s.Payout, s.Net()) + } +} + +// If every hand busts there is nothing left to beat, and the dealer does not turn +// over — same as it always was with one hand. +func TestTheDealerDoesNotPlayWhenEveryHandIsBust(t *testing.T) { + s := dealt(100, + hand(9, 9), + hand(cards.King, 6), + hand(8, 8, cards.King, cards.King, 4), // both hands reach 17 then bust on a king + ) + s, _, err := ApplyMove(s, Split) + if err != nil { + t.Fatal(err) + } + if s, _, err = ApplyMove(s, Hit); err != nil { // left 9+8+K = 27 + t.Fatal(err) + } + if s, _, err = ApplyMove(s, Hit); err != nil { // right 9+8+K = 27 + t.Fatal(err) + } + if s.Phase != PhaseDone { + t.Fatalf("phase = %q, want done", s.Phase) + } + if len(s.Dealer) != 2 { + t.Errorf("the dealer drew to %d cards with every hand already bust", len(s.Dealer)) + } + if s.Payout != 0 { + t.Errorf("two bust hands paid %d, want nothing", s.Payout) + } +} + +// Doubling after a split doubles *that hand's* bet, not the whole table's. +func TestDoublingAfterASplitOnlyDoublesThatHand(t *testing.T) { + s := dealt(100, + hand(5, 5), + hand(cards.King, 7), + hand(6, 4, 9, cards.King), + ) + s, _, err := ApplyMove(s, Split) // left: 5+6=11, right: 5+4=9 + if err != nil { + t.Fatal(err) + } + if s.DoubleCost() != 100 { + t.Fatalf("DoubleCost = %d on a split hand of 100, want 100", s.DoubleCost()) + } + s, _, err = ApplyMove(s, Double) // left doubles: 11 + 9 = 20 + if err != nil { + t.Fatal(err) + } + if s.Hands[0].Bet != 200 || s.Hands[1].Bet != 100 { + t.Fatalf("bets are %d/%d after doubling the left hand, want 200/100", s.Hands[0].Bet, s.Hands[1].Bet) + } + if s.Bet != 300 { + t.Fatalf("total staked = %d, want 300 (100 + a doubled 100 + 100)", s.Bet) + } + if !s.Hands[0].Done || s.Active != 1 { + t.Fatal("a doubled hand takes one card and hands over") + } +} + +// A live hand outlives a deploy. The blobs written before split existed have a +// "player" array and no "hands", and a state that decodes to no hands at all is a +// player whose cards vanished mid-deal. +func TestALiveHandDealtBeforeSplitExistedStillLoads(t *testing.T) { + legacy := []byte(`{ + "rules": {"decks": 6, "blackjack_pays": 1.5, "dealer_hits_soft17": true, "rake_pct": 0.05}, + "deck": [], + "player": [{"r": 10, "s": 1}, {"r": 7, "s": 2}], + "dealer": [{"r": 9, "s": 0}, {"r": 5, "s": 3}], + "bet": 250, + "doubled": false, + "phase": "player" + }`) + + var s State + if err := json.Unmarshal(legacy, &s); err != nil { + t.Fatal(err) + } + if len(s.Hands) != 1 { + t.Fatalf("an old live hand decoded to %d hands, want 1 — the player's cards went missing", len(s.Hands)) + } + if len(s.Hands[0].Cards) != 2 { + t.Fatalf("the revived hand holds %d cards, want 2", len(s.Hands[0].Cards)) + } + if s.Hands[0].Bet != 250 || s.Bet != 250 { + t.Fatalf("the revived hand carries %d of the %d staked, want all of it", s.Hands[0].Bet, s.Bet) + } + if v, _ := s.Hands[0].Value(); v != 17 { + t.Fatalf("the revived hand is worth %d, want 17", v) + } + // And it can still be played to the end. + if _, _, err := ApplyMove(s, Stand); err != nil { + t.Fatalf("the revived hand could not be stood on: %v", err) + } +} diff --git a/internal/web/games_play.go b/internal/web/games_play.go index abeeb0e..82723bb 100644 --- a/internal/web/games_play.go +++ b/internal/web/games_play.go @@ -91,35 +91,62 @@ func viewCard(c cards.Card) cardView { // genuinely not in the payload. A field the browser is told to ignore is a field // somebody reads in devtools. type handView struct { - Phase string `json:"phase"` + Phase string `json:"phase"` + Bet int64 `json:"bet"` // everything staked this deal, across every hand + Hands []spotView `json:"hands"` + Active int `json:"active"` + Dealer []cardView `json:"dealer"` + Hole bool `json:"hole"` // true: the dealer has a face-down card + DTotal int `json:"dealer_total"` // what the *shown* dealer cards add to + + Outcome string `json:"outcome,omitempty"` + Payout int64 `json:"payout,omitempty"` + Rake int64 `json:"rake,omitempty"` + Net int64 `json:"net"` + Double bool `json:"can_double"` + Split bool `json:"can_split"` +} + +// spotView is one of the player's hands: its cards, its own chips, its own +// verdict. Before split there was only ever one and it was flattened into the +// hand itself; a split is the moment that stops being true. +type spotView struct { + Cards []cardView `json:"cards"` Bet int64 `json:"bet"` - Player []cardView `json:"player"` - Dealer []cardView `json:"dealer"` - Hole bool `json:"hole"` // true: the dealer has a face-down card Total int `json:"total"` Soft bool `json:"soft"` - DTotal int `json:"dealer_total"` // what the *shown* dealer cards add to + Doubled bool `json:"doubled"` + Done bool `json:"done"` Outcome string `json:"outcome,omitempty"` Payout int64 `json:"payout,omitempty"` - Rake int64 `json:"rake,omitempty"` - Net int64 `json:"net"` - Double bool `json:"can_double"` } func viewHand(st blackjack.State) handView { v := handView{ Phase: string(st.Phase), Bet: st.Bet, + Active: st.Active, Outcome: string(st.Outcome), Payout: st.Payout, Rake: st.Rake, Net: st.Net(), Double: st.CanDouble(), + Split: st.CanSplit(), } - for _, c := range st.Player { - v.Player = append(v.Player, viewCard(c)) + for _, h := range st.Hands { + s := spotView{ + Bet: h.Bet, + Doubled: h.Doubled, + Done: h.Done, + Outcome: string(h.Outcome), + Payout: h.Payout, + } + for _, c := range h.Cards { + s.Cards = append(s.Cards, viewCard(c)) + } + s.Total, s.Soft = h.Value() + v.Hands = append(v.Hands, s) } - v.Total, v.Soft = blackjack.HandValue(st.Player) dealer := st.Dealer if st.Phase == blackjack.PhasePlayer && len(dealer) > 1 { @@ -139,6 +166,7 @@ func viewHand(st blackjack.State) handView { type eventView struct { Kind string `json:"kind"` Card *cardView `json:"card,omitempty"` + Hand int `json:"hand"` // which of the player's hands the card landed on Text string `json:"text,omitempty"` } @@ -149,7 +177,7 @@ func viewEvents(evs []blackjack.Event, phase blackjack.Phase) []eventView { out := make([]eventView, 0, len(evs)) dealerCards := 0 for _, e := range evs { - v := eventView{Kind: e.Kind, Text: e.Text} + v := eventView{Kind: e.Kind, Text: e.Text, Hand: e.Hand} if e.Card != nil { c := viewCard(*e.Card) v.Card = &c @@ -492,31 +520,45 @@ func (s *Server) handleMove(w http.ResponseWriter, r *http.Request) { move := blackjack.Move(req.Move) - // A double doubles the stake, so the extra chips have to be taken before the - // move is applied — and if they aren't there, the move simply isn't legal. - // Take them first: if the engine then refuses the move, they go straight back. - doubled := false - if move == blackjack.Double { + // Double and split are the two moves that put more chips on the table *after* + // the cards are out, so the money has to move before the move does — and if the + // chips aren't there, the move simply isn't legal. Take them first: if the + // engine then refuses the move, they go straight back. + // + // Both cost the active hand's bet, not the whole stake. Once a hand can be + // split those are different numbers, and doubling the third hand of a split for + // the total of all three would be quite a thing to discover in production. + var staked int64 + switch move { + case blackjack.Double: if !st.CanDouble() { - writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "you can only double on the first two cards"}) + writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "you can only double on the first two cards of a hand"}) return } - if err := storage.Stake(user, st.Bet); err != nil { + staked = st.DoubleCost() + case blackjack.Split: + if !st.CanSplit() { + writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "you can only split two cards of the same rank, and only up to four hands"}) + return + } + staked = st.SplitCost() + } + if staked > 0 { + if err := storage.Stake(user, staked); err != nil { if errors.Is(err, storage.ErrInsufficientChips) { - writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "not enough chips to double"}) + writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "not enough chips for that"}) return } - slog.Error("games: stake double", "user", user, "err", err) + slog.Error("games: stake raise", "user", user, "move", move, "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } - doubled = true } next, evs, err := blackjack.ApplyMove(st, move) if err != nil { - if doubled { - _ = storage.Award(user, st.Bet) // the move didn't happen; neither did the raise + if staked > 0 { + _ = storage.Award(user, staked) // the move didn't happen; neither did the raise } writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "that move isn't legal here"}) return diff --git a/internal/web/games_play_test.go b/internal/web/games_play_test.go index 4b8418e..e7d81cd 100644 --- a/internal/web/games_play_test.go +++ b/internal/web/games_play_test.go @@ -99,8 +99,8 @@ func TestDealTakesTheStakeAndHidesTheHoleCard(t *testing.T) { if v.Hand == nil { t.Fatal("deal returned no hand") } - if len(v.Hand.Player) != 2 { - t.Fatalf("player was dealt %d cards, want 2", len(v.Hand.Player)) + if len(v.Hand.Hands[0].Cards) != 2 { + t.Fatalf("player was dealt %d cards, want 2", len(v.Hand.Hands[0].Cards)) } // A natural settles on the spot and legitimately shows both dealer cards. @@ -181,12 +181,14 @@ func TestOneHandAtATime(t *testing.T) { t.Fatalf("the live hand went missing: %v", err) } var st struct { - Player []struct{} `json:"player"` + Hands []struct { + Cards []struct{} `json:"cards"` + } `json:"hands"` } if err := json.Unmarshal(live.State, &st); err != nil { t.Fatal(err) } - if len(st.Player) != len(first.Hand.Player) { + if len(st.Hands) != 1 || len(st.Hands[0].Cards) != len(first.Hand.Hands[0].Cards) { t.Fatal("the refused deal replaced the hand in progress") } } diff --git a/internal/web/static/css/input.css b/internal/web/static/css/input.css index f06962e..2ee265a 100644 --- a/internal/web/static/css/input.css +++ b/internal/web/static/css/input.css @@ -767,6 +767,55 @@ html[data-phase="night"] { } .pete-spot[data-live="1"] .pete-spot-label { opacity: 0; } + /* The player's hands at blackjack. There is one of them, until you split. + Then there are up to four, side by side, each with its own chips on its own + spot — because after a split they are not one bet in two piles, they are two + bets that win and lose separately. */ + .bj-hands { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 0.75rem 1rem; + } + .bj-hand { + display: flex; + align-items: center; + gap: 0.85rem; + border-radius: 1.5rem; + padding: 0.4rem 0.7rem 0.4rem 0.4rem; + transition: opacity 0.25s ease, box-shadow 0.25s ease, background 0.25s ease; + } + /* Which hand you are actually playing. The rest are still on the table; they + are simply not your problem this second. */ + .bj-hand[data-live="0"] { opacity: 0.5; } + .bj-hand[data-live="1"] { + background: rgba(0,0,0,0.16); + box-shadow: 0 0 0 2px rgba(var(--glow, 242,181,61), 0.5); + } + /* Four hands do not fit at one hand's worth of card, and the fix is to tell the + row what a card *is* rather than to squash the box a card is in: everything + about a card is sized off these two vars. The spot shrinks with them, or the + chips end up bigger than the cards they're riding on. */ + .bj-hands[data-count="2"] { --card-h: 6.6rem; --card-w: 4.7rem; } + .bj-hands[data-count="3"], + .bj-hands[data-count="4"] { --card-h: 5rem; --card-w: 3.6rem; } + .bj-hands[data-count="2"] .pete-hand { min-height: 6.8rem; } + .bj-hands[data-count="3"] .pete-hand, + .bj-hands[data-count="4"] .pete-hand { min-height: 5.2rem; gap: 0.35rem; } + .bj-hands[data-count="2"] .pete-spot { height: 5.5rem; width: 5.5rem; } + .bj-hands[data-count="3"] .pete-spot, + .bj-hands[data-count="4"] .pete-spot { height: 4.25rem; width: 4.25rem; } + .bj-hands[data-count="3"] .pete-spot-label, + .bj-hands[data-count="4"] .pete-spot-label { display: none; } + + /* The card a split is about to take away. It lifts, and then it's gone — + otherwise a card simply teleports into a hand that didn't exist yet. */ + .bj-splitting { animation: bj-split-lift 0.32s ease forwards; } + @keyframes bj-split-lift { + from { transform: translateY(0) rotate(0); } + to { transform: translateY(-1.1rem) rotate(6deg); } + } + /* The stack in the spot. Each chip is offset up the pile by its index, so ten chips look like ten chips rather than one chip with a number on it. */ .pete-stack { diff --git a/internal/web/static/css/output.css b/internal/web/static/css/output.css index 941377f..5542e58 100644 --- a/internal/web/static/css/output.css +++ b/internal/web/static/css/output.css @@ -1 +1 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{font-family:Nunito,system-ui,sans-serif}html{scroll-behavior:smooth}body,html{overflow-x:hidden}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pete-channel-nav{scrollbar-width:none;-ms-overflow-style:none}.pete-channel-nav::-webkit-scrollbar{display:none}.pete-reader-backdrop{position:absolute;inset:0;background:rgba(20,14,6,.55);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}html[data-phase=night] .pete-reader-backdrop{background:rgba(6,8,20,.65)}.pete-reader-shell{position:relative;height:100%;width:100%;max-width:44rem;margin:0 auto;display:flex;flex-direction:column;padding:max(env(safe-area-inset-top),.75rem) 1rem .75rem;gap:.75rem}.pete-reader-bar{display:flex;align-items:center;justify-content:space-between;gap:.75rem;color:#fff}.pete-reader-progress{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:600;font-size:.85rem;letter-spacing:.02em;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-nav{display:flex;align-items:center;gap:.4rem}.pete-reader-btn{display:inline-flex;align-items:center;justify-content:center;min-width:2.25rem;height:2.25rem;padding:0 .7rem;border-radius:9999px;background:hsla(0,0%,100%,.14);color:#fff;font-weight:700;font-size:.95rem;border:2px solid hsla(0,0%,100%,.18);cursor:pointer;transition:background .15s ease,transform .15s ease,opacity .15s ease}.pete-reader-btn:hover{background:hsla(0,0%,100%,.28);transform:translateY(-1px)}.pete-reader-btn:disabled{opacity:.35;cursor:default;transform:none}.pete-reader-btn-open{background:var(--accent);border-color:transparent;color:#1c1305;text-decoration:none}.pete-reader-btn-open:hover{background:var(--accent);filter:brightness(1.08)}.pete-reader-btn[aria-expanded=true],.pete-reader-btn[aria-pressed=true]{background:var(--accent);border-color:transparent;color:#1c1305}.pete-reader-type{position:relative;display:inline-flex}.pete-reader-typemenu[hidden]{display:none}.pete-reader-typemenu{position:absolute;top:calc(100% + .5rem);right:0;z-index:10;width:15rem;padding:.75rem;border-radius:1rem;background:var(--card);color:var(--ink);border:2px solid rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(20,14,6,.28);display:flex;flex-direction:column;gap:.6rem}html[data-phase=night] .pete-reader-typemenu{border-color:hsla(0,0%,100%,.12)}.pete-reader-typerow{display:flex;align-items:center;justify-content:space-between;gap:.6rem}.pete-reader-typelabel{font-size:.8rem;font-weight:700;opacity:.7}.pete-reader-typebtns{display:inline-flex;gap:.25rem}.pete-reader-typebtns button{min-width:2rem;height:2rem;padding:0 .55rem;border-radius:.6rem;background:rgba(20,14,6,.06);color:var(--ink);font-weight:700;font-size:.82rem;border:2px solid transparent;cursor:pointer;transition:background .15s ease}html[data-phase=night] .pete-reader-typebtns button{background:hsla(0,0%,100%,.08)}.pete-reader-typebtns button:hover{background:rgba(20,14,6,.12)}.pete-reader-typebtns button.is-active{background:var(--accent);color:#1c1305;border-color:transparent}.pete-reader-voicerow[hidden]{display:none}.pete-reader-voice{font:inherit;font-size:.8rem;font-weight:600;padding:.25rem .4rem;border-radius:.55rem;border:1px solid rgba(20,14,6,.18);background:rgba(20,14,6,.06);color:inherit;max-width:11rem;cursor:pointer}html[data-phase=night] .pete-reader-voice{border-color:hsla(0,0%,100%,.14);background:hsla(0,0%,100%,.08)}.pete-reader-scroll{flex:1 1 auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border-radius:1.75rem}.pete-reader-scroll:focus{outline:none}.pete-reader-article{background:var(--card);color:var(--ink);border-radius:1.75rem;border:2px solid rgba(0,0,0,.06);box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.18);padding:clamp(1.25rem,4vw,2.5rem)}html[data-phase=night] .pete-reader-article{border-color:hsla(0,0%,100%,.08)}.pete-reader-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.75rem;margin-bottom:.85rem}.pete-reader-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:9999px;padding:.15rem .65rem;color:#fff;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.pete-reader-source{font-weight:700;opacity:.75}.pete-reader-time{opacity:.55}.pete-reader-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;line-height:1.15;letter-spacing:-.01em;font-size:clamp(1.6rem,4.5vw,2.4rem);margin-bottom:1rem}.pete-reader-hero{width:100%;border-radius:1.1rem;margin-bottom:1.25rem;background:rgba(0,0,0,.05)}.pete-reader-scroll{--reader-fs:1.08rem}.pete-reader-body{font-size:var(--reader-fs);line-height:1.75}.pete-reader-body p{margin-bottom:1.05em}.pete-reader-body p:last-child{margin-bottom:0}.pete-reader-scroll.is-size-s{--reader-fs:0.98rem}.pete-reader-scroll.is-size-m{--reader-fs:1.08rem}.pete-reader-scroll.is-size-l{--reader-fs:1.22rem}.pete-reader-scroll.is-size-xl{--reader-fs:1.4rem}.pete-reader-scroll.is-serif .pete-reader-body{font-family:Georgia,Iowan Old Style,Times New Roman,serif}.pete-reader-scroll.is-sepia .pete-reader-article{background:#f6ecd6;color:#4a3a28;border-color:rgba(74,58,40,.16)}.pete-reader-scroll.is-sepia .pete-reader-note{border-top-color:rgba(74,58,40,.18);opacity:.8}.pete-reader-scroll.is-sepia .pete-reader-hero{background:rgba(74,58,40,.08)}.pete-reader-body p.is-speaking{background:color-mix(in srgb,var(--accent) 32%,transparent);border-radius:.4rem;box-shadow:0 0 0 .35rem color-mix(in srgb,var(--accent) 32%,transparent)}.pete-reader-note{margin-top:1.25rem;padding-top:1rem;border-top:1px solid rgba(0,0,0,.1);font-size:.85rem;opacity:.7}html[data-phase=night] .pete-reader-note{border-color:hsla(0,0%,100%,.12)}.pete-reader-note a{color:var(--accent);font-weight:700}.pete-reader-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:.75rem;padding:clamp(2rem,8vw,4rem) 1rem;color:var(--ink)}.pete-reader-empty-emoji{font-size:2.5rem}.pete-reader-toast{position:absolute;left:50%;bottom:4.5rem;transform:translate(-50%,.5rem);z-index:20;padding:.5rem .9rem;border-radius:9999px;background:rgba(20,14,6,.9);color:#fff;font-weight:700;font-size:.85rem;box-shadow:0 8px 24px rgba(0,0,0,.3);opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease}.pete-reader-toast.is-shown{opacity:1;transform:translate(-50%)}.pete-reader-hint{text-align:center;color:hsla(0,0%,100%,.85);font-size:.75rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-hint kbd{font-family:ui-monospace,monospace;background:hsla(0,0%,100%,.16);border-radius:.3rem;padding:.05rem .3rem}@media (max-width:640px){.pete-reader-hint{display:none}.pete-reader-bar{flex-wrap:wrap}.pete-reader-nav{flex-wrap:wrap;justify-content:flex-end;row-gap:.4rem}}.pete-reader-related{width:100%;margin:.85rem auto 0}.pete-reader-related-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;font-size:.95rem;color:#fff;margin:0 0 .6rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-related-grid{display:grid;gap:.6rem}.pete-reader-related-card{display:flex;align-items:center;gap:.75rem;background:var(--card);color:var(--ink);border-radius:1rem;border:2px solid rgba(0,0,0,.06);padding:.6rem;text-decoration:none;transition:transform .15s ease,box-shadow .15s ease}.pete-reader-related-card:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(60,40,20,.16)}html[data-phase=night] .pete-reader-related-card{border-color:hsla(0,0%,100%,.08)}.pete-reader-related-thumb{width:4.5rem;height:3.25rem;flex-shrink:0;-o-object-fit:cover;object-fit:cover;border-radius:.6rem;background:rgba(0,0,0,.06)}.pete-reader-related-meta{min-width:0;display:flex;flex-direction:column;gap:.25rem}.pete-reader-related-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.4rem;font-size:.7rem}.pete-reader-related-source{font-weight:700;opacity:.7}.pete-reader-related-headline{font-weight:700;line-height:1.25;font-size:.92rem;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}[data-story-card][data-read="1"]{opacity:.5;transition:opacity .2s ease}[data-story-card][data-read="1"]:hover{opacity:1}[data-story-card][data-read="1"]:after{content:"✓";position:absolute;top:.6rem;right:.6rem;z-index:6;display:grid;place-items:center;height:1.5rem;width:1.5rem;border-radius:9999px;background:rgba(20,14,6,.72);color:#fff;font-size:.8rem;font-weight:700}.pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,#2f7d5b,#24614a 55%,#1c4d3c)}.pete-shoe{position:absolute;top:1.25rem;right:1.25rem;height:4.2rem;width:3rem;border-radius:.55rem;background:linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.18);box-shadow:0 4px 0 rgba(0,0,0,.18),inset 0 0 0 3px hsla(0,0%,100%,.12)}.pete-shoe:after{content:"";position:absolute;inset:.45rem .4rem auto .4rem;height:.5rem;border-radius:.2rem;background:rgba(0,0,0,.22)}.pete-hand{display:flex;flex-wrap:wrap;gap:.6rem;min-height:8.6rem}.pete-card{perspective:700px;height:var(--card-h,8.4rem);width:var(--card-w,6rem);animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards}.pete-card-inner{position:relative;height:100%;width:100%;transform-style:preserve-3d;transition:transform .45s cubic-bezier(.4,0,.2,1)}.pete-card[data-face=down] .pete-card-inner{transform:rotateY(180deg)}.pete-card-back,.pete-card-front{position:absolute;inset:0;display:grid;border-radius:.55rem;backface-visibility:hidden;-webkit-backface-visibility:hidden;box-shadow:0 3px 0 rgba(0,0,0,.18),0 6px 14px rgba(0,0,0,.22)}.pete-card-front{place-items:stretch;background:#fdfaf2;border:2px solid rgba(30,20,10,.12);color:#2b2118;font-family:Fredoka,Nunito,system-ui,sans-serif;line-height:1;overflow:hidden}.pete-card-front[data-red="1"]{color:#cc3d4a}.pete-card-back{transform:rotateY(180deg);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.15)}.pete-card-svg{width:100%;height:100%;display:block;fill:currentColor}.pete-card-idx{font-size:26px;font-weight:700;text-anchor:middle;fill:currentColor}.pete-card-panel{fill:currentColor;fill-opacity:.06;stroke:currentColor;stroke-opacity:.35;stroke-width:1.5}.pete-card-court{font-size:34px;font-weight:700;text-anchor:middle;fill:currentColor}@keyframes pete-deal{0%{opacity:0;transform:translate(var(--deal-x,14rem),var(--deal-y,-7rem)) scale(.72) rotate(9deg)}62%{opacity:1;transform:translateY(.35rem) scale(1.05) rotate(calc(var(--tilt, 0deg) - 2deg))}to{opacity:1;transform:translate(0) scale(1) rotate(var(--tilt,0deg))}}.pete-card:after{content:"";position:absolute;inset:auto 6% -.35rem 6%;height:.6rem;border-radius:999px;background:rgba(0,0,0,.35);filter:blur(5px);animation:pete-thud .42s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-thud{0%,55%{opacity:0;transform:scale(.4)}72%{opacity:.9;transform:scale(1.15)}to{opacity:.45;transform:scale(1)}}.pete-hand[data-won="1"] .pete-card{animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards,pete-win .6s ease .1s}@keyframes pete-win{0%,to{transform:rotate(var(--tilt,0deg)) translateY(0)}40%{transform:rotate(var(--tilt,0deg)) translateY(-.6rem)}}.pete-hand[data-won="-1"] .pete-card{filter:saturate(.45) brightness(.78);transition:filter .5s ease .2s}.pete-disc{position:relative;border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 62%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.22);box-shadow:0 3px 0 rgba(0,0,0,.28),0 6px 14px rgba(0,0,0,.2)}.pete-disc:before{content:"";position:absolute;inset:11%;border-radius:999px;border:2px dashed hsla(0,0%,100%,.55)}.pete-disc[data-chip="5"]{--chip:#5aa9e6}.pete-disc[data-chip="25"]{--chip:#4caf7d}.pete-disc[data-chip="100"]{--chip:#2b2118}.pete-disc[data-chip="500"]{--chip:#b079d6}.pete-chip{transition:transform .12s ease}.pete-chip>span{position:relative}.pete-chip:active{transform:translateY(1px) scale(.94)}.pete-spot{position:relative;display:grid;place-items:center;height:7rem;width:7rem;flex:none;border-radius:999px;border:2px dashed hsla(0,0%,100%,.35);background:radial-gradient(circle at 50% 40%,hsla(0,0%,100%,.1),transparent 70%);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08);transition:box-shadow .3s ease,border-color .3s ease}.pete-spot[data-live="1"]{border-color:rgba(var(--glow,242,181,61),.85);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08),0 0 22px rgba(var(--glow,242,181,61),.35)}.pete-spot-label{font-size:.65rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:hsla(0,0%,100%,.35)}.pete-spot[data-live="1"] .pete-spot-label{opacity:0}.pete-stack{position:absolute;inset:0;pointer-events:none}.pete-stack .pete-disc{position:absolute;left:50%;top:50%;height:2.6rem;width:2.6rem;margin:-1.3rem 0 0 -1.3rem;transform:translateY(calc(-.5rem*var(--i, 0))) rotate(var(--spin,0deg));box-shadow:0 2px 0 rgba(0,0,0,.35),0 4px 8px rgba(0,0,0,.25);animation:pete-chip-land .28s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes pete-chip-land{0%{transform:translateY(calc(-.5rem*var(--i, 0) - 1.1rem)) rotate(var(--spin,0deg)) scale(1.18)}}.pete-spot-total{position:absolute;bottom:-.6rem;left:50%;transform:translateX(-50%);white-space:nowrap;border-radius:999px;background:rgba(0,0,0,.45);padding:.1rem .6rem;font-size:.72rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-fly-layer{position:fixed;inset:0;z-index:60;pointer-events:none;overflow:hidden}.pete-fly{position:absolute;height:2.6rem;width:2.6rem;margin:-1.3rem 0 0 -1.3rem;will-change:transform}.pete-rack{position:absolute;top:1.25rem;right:5.75rem;display:flex;align-items:flex-end;gap:.4rem;padding:.5rem .6rem .45rem;border-radius:.75rem;background:rgba(0,0,0,.2);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.06)}.pete-rack span{display:block;width:1.7rem;border-radius:.35rem;background:linear-gradient(90deg,rgba(0,0,0,.28),transparent 35%,transparent 65%,rgba(0,0,0,.28)),repeating-linear-gradient(180deg,hsla(0,0%,100%,.18) 0 .06rem,var(--chip,#e07a5f) .06rem .3rem,rgba(0,0,0,.35) .3rem .36rem);border:1px solid rgba(0,0,0,.35);height:calc(.36rem*var(--stack, 3))}.pete-rack span[data-chip="5"]{--chip:#5aa9e6}.pete-rack span[data-chip="25"]{--chip:#4caf7d}.pete-rack span[data-chip="100"]{--chip:#2b2118}.pete-rack span[data-chip="500"]{--chip:#b079d6}@media (max-width:639px){.pete-rack:not([data-at=rail]){gap:.25rem;padding:.35rem .4rem .3rem}.pete-rack:not([data-at=rail]) span{width:1.15rem}.pete-rack:not([data-at]){right:1rem}}.pete-spark{position:absolute;height:.75rem;width:.45rem;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.35);will-change:transform,opacity}.pete-dealer-think{animation:pete-think .9s ease-in-out infinite}@keyframes pete-think{0%,to{opacity:.6}50%{opacity:1;transform:translateY(-1px)}}.pete-gallows{overflow:visible;fill:none;stroke-linecap:round;stroke-linejoin:round}.pete-gallows-frame path{stroke:rgba(0,0,0,.35);stroke-width:6}.pete-gallows-frame path:last-child{stroke:rgba(0,0,0,.3);stroke-width:3.5}.pete-gallows-body>*{stroke:#fff;stroke-width:5;opacity:0;filter:drop-shadow(0 2px 0 rgba(0,0,0,.25))}.pete-gallows-body>[data-on="1"]{opacity:1}.pete-part-draw{stroke-dasharray:260;animation:pete-part .45s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-part{0%{stroke-dashoffset:260}to{stroke-dashoffset:0}}.pete-shake{animation:pete-shake .42s cubic-bezier(.36,.07,.19,.97)}@keyframes pete-shake{10%,90%{transform:translateX(-1.5px)}20%,80%{transform:translateX(3px)}30%,50%,70%{transform:translateX(-5px)}40%,60%{transform:translateX(5px)}}.pete-board{display:flex;flex-wrap:wrap;gap:.35rem 1.1rem;min-height:5rem;align-items:center}.pete-word{display:flex;gap:.3rem}.pete-tile{display:grid;place-items:center;height:2.9rem;width:2.2rem;border-radius:.5rem;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.4rem;font-weight:700;color:#fff;background:rgba(0,0,0,.22);border-bottom:3px solid rgba(0,0,0,.28);text-transform:uppercase}.pete-tile[data-up="1"]{background:hsla(0,0%,100%,.95);color:#2b2118;border-bottom-color:rgba(0,0,0,.35)}.pete-tile[data-punct="1"]{background:none;border:0;width:auto;min-width:.7rem;color:hsla(0,0%,100%,.55)}.pete-tile-hit{animation:pete-tile-hit .34s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-tile-hit{0%{transform:rotateX(90deg) scale(1.1)}to{transform:rotateX(0) scale(1)}}.pete-missed{display:grid;place-items:center;height:1.5rem;min-width:1.5rem;padding:0 .3rem;border-radius:.375rem;background:rgba(0,0,0,.25);font-size:.75rem;font-weight:700;color:hsla(0,0%,100%,.5);text-decoration:line-through;text-transform:uppercase}.pete-meter{display:flex;align-items:baseline;gap:.5rem;border-radius:999px;background:rgba(0,0,0,.28);padding:.4rem .9rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:box-shadow .3s ease}.pete-meter-label{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:hsla(0,0%,100%,.45)}.pete-meter-value{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.5rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--accent)}.pete-meter[data-cold="1"] .pete-meter-value{color:hsla(0,0%,100%,.55)}.pete-meter[data-hit="1"]{box-shadow:inset 0 0 0 2px rgba(204,61,74,.9);animation:pete-meter-hit .4s ease}@keyframes pete-meter-hit{0%{transform:scale(1)}35%{transform:scale(.94)}to{transform:scale(1)}}.pete-keys{display:grid;gap:.35rem}.pete-key-row{display:flex;justify-content:center;gap:.35rem}.pete-key-row[data-digits="1"]{margin-top:.25rem;opacity:.75}.pete-key-row[data-digits="1"] .pete-key{height:2rem;min-width:1.8rem;font-size:.8rem}.pete-key{height:2.75rem;min-width:2.2rem;flex:0 1 2.4rem;border-radius:.6rem;background:color-mix(in srgb,var(--ink) 6%,transparent);border:2px solid color-mix(in srgb,var(--ink) 10%,transparent);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-weight:700;color:var(--ink);transition:transform .08s ease,background .15s ease,opacity .15s ease}.pete-key:hover:not(:disabled){background:color-mix(in srgb,var(--ink) 12%,transparent)}.pete-key:active:not(:disabled){transform:translateY(1px) scale(.96)}.pete-key:disabled{cursor:default}.pete-key[data-state=hit]{background:#4caf7d;border-color:#3d9367;color:#fff;opacity:1}.pete-key[data-state=miss]{background:color-mix(in srgb,var(--ink) 4%,transparent);color:color-mix(in srgb,var(--ink) 30%,transparent);text-decoration:line-through}.pete-key:disabled[data-state=""]{opacity:.4}.pete-tier{background:color-mix(in srgb,var(--ink) 4%,transparent);border-color:color-mix(in srgb,var(--ink) 10%,transparent)}.pete-tier:hover{background:color-mix(in srgb,var(--ink) 8%,transparent)}.pete-tier[data-on="1"]{background:color-mix(in srgb,var(--accent) 18%,transparent);border-color:var(--accent)}.pete-clock{position:relative;height:.6rem;border-radius:999px;background:rgba(0,0,0,.3);overflow:hidden;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.08)}.pete-clock-fill{height:100%;width:100%;border-radius:999px;background:var(--accent);transform-origin:left center;transform:scaleX(1)}.pete-clock[data-hot="1"] .pete-clock-fill{background:#cc3d4a}.pete-clock[data-hot="1"]{animation:pete-clock-pulse .7s ease-in-out infinite}@keyframes pete-clock-pulse{0%,to{box-shadow:inset 0 0 0 2px rgba(204,61,74,.35)}50%{box-shadow:inset 0 0 0 2px rgba(204,61,74,.95)}}.pete-answer{position:relative;display:flex;align-items:center;gap:.75rem;width:100%;border-radius:1rem;padding:.85rem 1rem;text-align:left;font-weight:600;color:#fff;background:rgba(0,0,0,.26);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:background .15s ease,transform .1s ease,box-shadow .2s ease}.pete-answer:hover:not(:disabled){background:rgba(0,0,0,.36);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.28)}.pete-answer:active:not(:disabled){transform:translateY(1px)}.pete-answer:disabled{cursor:default}.pete-answer-key{display:grid;place-items:center;height:1.75rem;width:1.75rem;flex:none;border-radius:.6rem;background:hsla(0,0%,100%,.12);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:hsla(0,0%,100%,.7)}.pete-answer[data-state=right]{background:rgba(46,160,103,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.5)}.pete-answer[data-state=wrong]{background:rgba(204,61,74,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.35);animation:pete-answer-no .45s ease}.pete-answer[data-state=missed]{box-shadow:inset 0 0 0 2px rgba(46,160,103,.95)}.pete-answer[data-state=dim]{opacity:.4}@keyframes pete-answer-no{0%,to{transform:translateX(0)}20%{transform:translateX(-6px)}45%{transform:translateX(5px)}70%{transform:translateX(-3px)}}.pete-ladder{display:flex;flex-wrap:wrap;gap:.3rem}.pete-rung{height:.5rem;width:1.1rem;border-radius:999px;background:hsla(0,0%,100%,.14);transition:background .3s ease,transform .3s ease}.pete-rung[data-on="1"]{background:var(--accent);transform:scaleY(1.35)}.pete-solitaire{--card-w:clamp(2.5rem,10.6vw,5.2rem);--card-h:calc(var(--card-w)*1.4);--fan-up:calc(var(--card-h)*0.29);--fan-down:calc(var(--card-h)*0.14)}.pete-slot{position:relative;display:grid;place-items:center;height:var(--card-h);width:var(--card-w);flex:none;border-radius:.55rem;border:2px dashed hsla(0,0%,100%,.25);background:rgba(0,0,0,.12);box-shadow:inset 0 2px 8px rgba(0,0,0,.18);transition:border-color .18s ease,background .18s ease,transform .12s ease}.pete-slot-glyph{font-size:calc(var(--card-w)*.42);line-height:1;color:hsla(0,0%,100%,.3)}.pete-slot-glyph[data-red="1"]{color:hsla(0,100%,83%,.35)}.pete-slot>.pete-card{position:absolute;inset:0;height:100%;width:100%}.pete-stock{border-style:solid;border-color:rgba(0,0,0,.15);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);cursor:pointer}.pete-stock:hover{filter:brightness(1.08)}.pete-stock:active{transform:translateY(1px) scale(.98)}.pete-stock[data-empty="1"]{background:rgba(0,0,0,.12);border-style:dashed;border-color:hsla(0,0%,100%,.25)}.pete-stock[data-dead="1"]{cursor:default;opacity:.4}.pete-stock[data-dead="1"]:hover{filter:none}.pete-stock[data-dead="1"]:active{transform:none}.pete-slot-count{position:absolute;bottom:-.5rem;left:50%;transform:translateX(-50%);border-radius:999px;background:rgba(0,0,0,.5);padding:.05rem .5rem;font-size:.7rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-slot-recycle{color:hsla(0,0%,100%,.45)}.pete-slot-recycle svg{height:1.4rem;width:1.4rem}.pete-waste{position:relative;display:flex;height:var(--card-h);min-width:var(--card-w)}.pete-waste .pete-card+.pete-card{margin-left:calc(var(--card-w)*-.72)}.pete-waste .pete-card{position:relative}.pete-tableau{display:grid;grid-template-columns:repeat(7,var(--card-w));justify-content:space-between;gap:.5rem;align-items:start;min-height:calc(var(--card-h)*2.6)}.pete-col{display:flex;flex-direction:column;align-items:center;min-height:var(--card-h)}.pete-col .pete-card{position:relative}.pete-col .pete-card[data-face=down]+.pete-card{margin-top:calc(var(--fan-down) - var(--card-h))}.pete-col .pete-card[data-face=up]+.pete-card{margin-top:calc(var(--fan-up) - var(--card-h))}.pete-card[data-live="1"]{cursor:pointer}.pete-card[data-live="1"]:hover .pete-card-front{filter:brightness(1.06)}.pete-card[data-held="1"]{z-index:5;transform:translateY(-.55rem);transition:transform .14s cubic-bezier(.34,1.56,.64,1)}.pete-card[data-held="1"] .pete-card-front{box-shadow:0 3px 0 rgba(0,0,0,.18),0 10px 22px rgba(0,0,0,.32),0 0 0 3px rgba(242,181,61,.9)}.pete-col[data-drop="1"] .pete-card:last-child .pete-card-front,.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{border-color:rgba(242,181,61,.9);box-shadow:0 0 0 3px rgba(242,181,61,.45)}.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{background:rgba(242,181,61,.12)}.pete-nope{animation:pete-shake .4s cubic-bezier(.36,.07,.19,.97)}.pete-home-flash{animation:pete-home .5s ease-out}@keyframes pete-home{0%{box-shadow:0 0 0 0 rgba(242,181,61,.9)}to{box-shadow:0 0 0 1.4rem rgba(242,181,61,0)}}.pete-rail{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;justify-content:center;gap:1rem 1.5rem}@media (min-width:1024px){.pete-rail{flex-direction:column;justify-content:flex-start;width:9.5rem;gap:1.5rem}}.pete-rack[data-at=rail]{position:static;justify-content:center}.pete-uno[data-c=red]{--play:#d64545}.pete-uno[data-c=blue]{--play:#3d7fd6}.pete-uno[data-c=yellow]{--play:#e0b02c}.pete-uno[data-c=green]{--play:#46a86b}.pete-uno:before{content:"";position:absolute;inset:0;pointer-events:none;background:radial-gradient(80% 55% at 50% 45%,var(--play,transparent),transparent 70%);opacity:.16;transition:opacity .5s ease,background .5s ease}.pete-uno-card{position:relative;height:var(--uno-h,6.4rem);width:var(--uno-w,4.3rem);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),0 6px 14px rgba(0,0,0,.18);flex:none}.pete-uno-face{position:relative;display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;overflow:hidden;background:var(--uno,#2b2118);color:#fff}.pete-uno-card[data-c=red]{--uno:#d64545}.pete-uno-card[data-c=blue]{--uno:#3d7fd6}.pete-uno-card[data-c=yellow]{--uno:#e0b02c}.pete-uno-card[data-c=green]{--uno:#46a86b}.pete-uno-card[data-c=wild]{--uno:#2b2118}.pete-uno-card[data-named=red]{box-shadow:0 0 0 3px #d64545,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=blue]{box-shadow:0 0 0 3px #3d7fd6,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=yellow]{box-shadow:0 0 0 3px #e0b02c,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=green]{box-shadow:0 0 0 3px #46a86b,0 3px 0 rgba(0,0,0,.22)}.pete-uno-oval{position:relative;z-index:1;display:grid;place-items:center;height:78%;width:62%;border-radius:50%;transform:rotate(-24deg);background:#fff;color:var(--uno,#2b2118);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.45rem;font-weight:700;line-height:1;text-shadow:0 1px 0 rgba(0,0,0,.1)}.pete-uno-card[data-c=wild] .pete-uno-oval{color:#2b2118;font-size:1.15rem}.pete-uno-corner{position:absolute;z-index:1;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.62rem;font-weight:700;line-height:1;color:#fff;text-shadow:0 1px 1px rgba(0,0,0,.35)}.pete-uno-corner[data-at=tl]{top:.22rem;left:.28rem}.pete-uno-corner[data-at=br]{bottom:.22rem;right:.28rem;transform:rotate(180deg)}.pete-uno-wheel{position:absolute;inset:0;background:conic-gradient(#d64545 0 25%,#e0b02c 0 50%,#46a86b 0 75%,#3d7fd6 0);opacity:.92}.pete-uno-card-back{padding:.28rem}.pete-uno-back{display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;background:radial-gradient(120% 80% at 50% 0,hsla(0,0%,100%,.16),transparent 60%),#2b2118}.pete-uno-back:after{content:"UNO";display:block;transform:rotate(-24deg);padding:.1rem .35rem;border-radius:999px;background:#e0b02c;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.6rem;font-weight:700;letter-spacing:.02em}.pete-uno-hand{display:flex;flex-wrap:wrap;justify-content:center;gap:.4rem;min-height:6.8rem;padding-top:.6rem}.pete-uno-hand .pete-uno-card{animation:pete-uno-in .34s cubic-bezier(.22,1,.36,1) backwards;animation-delay:calc(var(--i, 0)*45ms);transition:transform .16s ease,filter .16s ease}.pete-uno-hand .pete-uno-card[data-settled="1"]{animation:none}.pete-uno-hand .pete-uno-card[data-on="1"]{cursor:pointer;transform:translateY(-.5rem)}.pete-uno-hand .pete-uno-card[data-on="1"]:hover{transform:translateY(-1.1rem) scale(1.03)}.pete-uno-hand .pete-uno-card[data-on="0"]{filter:brightness(.62) saturate(.7)}@keyframes pete-uno-in{0%{transform:translateY(2rem) rotate(6deg);opacity:0}to{transform:translateY(-.5rem);opacity:1}}.pete-uno-seat{display:flex;flex-direction:column;align-items:center;gap:.3rem;position:relative;padding:.5rem .7rem;border-radius:1rem;transition:background .25s ease,transform .25s ease}.pete-uno-seat[data-turn="1"]{background:hsla(0,0%,100%,.12);transform:translateY(-2px)}.pete-uno-seat[data-uno="1"] .pete-uno-count{color:#ffd76e}.pete-uno-fan{display:flex;--uno-h:3.2rem;--uno-w:2.15rem}.pete-uno-fan .pete-uno-card{margin-left:-1.35rem;transform:rotate(calc((var(--i, 0) - (var(--n, 1) - 1)/2)*4deg));transform-origin:bottom center;box-shadow:0 2px 0 rgba(0,0,0,.22)}.pete-uno-fan .pete-uno-card:first-child{margin-left:0}.pete-uno-fan .pete-uno-back:after{font-size:.4rem;padding:.06rem .22rem}.pete-uno-name{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.9rem;font-weight:700;color:#fff;line-height:1}.pete-uno-count{font-size:.7rem;font-weight:700;color:hsla(0,0%,100%,.55);line-height:1}.pete-uno-deck{position:relative;--uno-h:6.4rem;--uno-w:4.3rem;height:var(--uno-h);width:var(--uno-w);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),.28rem .28rem 0 -.06rem hsla(0,0%,100%,.35),.5rem .5rem 0 -.12rem hsla(0,0%,100%,.18);transition:transform .15s ease}.pete-uno-deck:not(:disabled):hover{transform:translateY(-3px)}.pete-uno-deck:disabled{opacity:.75}.pete-uno-deck-count{position:absolute;bottom:-.55rem;left:50%;transform:translateX(-50%);padding:.1rem .5rem;border-radius:999px;background:rgba(0,0,0,.55);color:#fff;font-size:.65rem;font-weight:700;line-height:1.4}.pete-uno-shuffle{animation:pete-uno-shuffle .42s ease}@keyframes pete-uno-shuffle{0%,to{transform:none}30%{transform:translateY(-4px) rotate(-3deg)}65%{transform:translateY(2px) rotate(2deg)}}.pete-uno-discard{display:grid;place-items:center;height:6.4rem;width:4.3rem;border-radius:.55rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.14)}.pete-uno-land{animation:pete-uno-land .3s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-uno-land{0%{transform:scale(1.14) rotate(-4deg)}to{transform:none}}.pete-uno-colour{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.15rem .6rem;border-radius:999px;color:#fff;background:rgba(0,0,0,.3)}.pete-uno-colour[data-c=red]{background:#d64545}.pete-uno-colour[data-c=blue]{background:#3d7fd6}.pete-uno-colour[data-c=yellow]{background:#e0b02c;color:#2b2118}.pete-uno-colour[data-c=green]{background:#46a86b}.pete-uno-badge{position:absolute;top:-.4rem;left:50%;z-index:5;padding:.2rem .6rem;border-radius:999px;background:#fff;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;white-space:nowrap;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-badge .9s ease forwards}.pete-uno-badge[data-tone=bad]{background:#cc3d4a;color:#fff}.pete-uno-badge[data-tone=uno]{background:#e0b02c}@keyframes pete-uno-badge{0%{transform:translate(-50%,.4rem) scale(.7);opacity:0}25%{transform:translate(-50%,-.5rem) scale(1.06);opacity:1}75%{transform:translate(-50%,-.9rem) scale(1);opacity:1}to{transform:translate(-50%,-1.6rem) scale(.95);opacity:0}}.pete-uno-wild{position:absolute;inset:0;z-index:10;display:grid;place-items:center;background:rgba(10,20,15,.55);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pete-uno-wild-box{padding:1.25rem 1.5rem;border-radius:1.25rem;background:rgba(20,40,30,.92);box-shadow:0 12px 30px rgba(0,0,0,.35);text-align:center}.pete-uno-swatch{padding:.7rem 1.4rem;border-radius:.75rem;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1rem;font-weight:700;color:#fff;background:var(--sw,#666);box-shadow:0 3px 0 rgba(0,0,0,.3);transition:transform .12s ease,filter .12s ease}.pete-uno-swatch:hover{transform:translateY(-2px);filter:brightness(1.08)}.pete-uno-swatch:active{transform:translateY(1px)}.pete-uno-swatch[data-c=red]{--sw:#d64545}.pete-uno-swatch[data-c=blue]{--sw:#3d7fd6}.pete-uno-swatch[data-c=yellow]{--sw:#e0b02c;color:#2b2118}.pete-uno-swatch[data-c=green]{--sw:#46a86b}.pete-uno-call{margin-top:.85rem;padding:.9rem 2.6rem;border-radius:999px;background:#e0b02c;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.7rem;font-weight:700;letter-spacing:.02em;box-shadow:0 5px 0 rgba(0,0,0,.3),0 0 0 0 rgba(224,176,44,.55);animation:pete-uno-call .9s ease-in-out infinite}.pete-uno-call:hover{filter:brightness(1.07)}.pete-uno-call:active{transform:translateY(2px);box-shadow:0 3px 0 rgba(0,0,0,.3)}@keyframes pete-uno-call{0%,to{transform:scale(1);box-shadow:0 5px 0 rgba(0,0,0,.3),0 0 0 0 rgba(224,176,44,.5)}50%{transform:scale(1.05);box-shadow:0 5px 0 rgba(0,0,0,.3),0 0 0 .9rem rgba(224,176,44,0)}}.pete-uno-timer{margin-top:.8rem;height:.35rem;border-radius:999px;background:hsla(0,0%,100%,.16);overflow:hidden}.pete-uno-timer>span{display:block;height:100%;width:100%;background:#e0b02c;transform-origin:left center;animation:pete-uno-drain linear forwards}@keyframes pete-uno-drain{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.pete-uno-catch{position:absolute;top:-.55rem;left:50%;transform:translateX(-50%);z-index:6;padding:.22rem .7rem;border-radius:999px;background:#cc3d4a;color:#fff;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.72rem;font-weight:700;white-space:nowrap;box-shadow:0 3px 0 rgba(0,0,0,.25);animation:pete-uno-catch 1.1s ease-in-out infinite;transition:filter .12s ease}.pete-uno-catch:hover{filter:brightness(1.12)}.pete-uno-catch:disabled{opacity:.45;animation:none}@keyframes pete-uno-catch{0%,to{transform:translateX(-50%) scale(1)}50%{transform:translateX(-50%) scale(1.08)}}@media (prefers-reduced-motion:reduce){.pete-uno-call,.pete-uno-catch{animation:none}}.pete-uno-card .pete-uno-oval[data-size=mid]{font-size:1.05rem}.pete-uno-card .pete-uno-oval[data-size=words]{font-size:.5rem;line-height:1.1;letter-spacing:.02em;text-align:center;padding:0 .15rem}.pete-uno-card[data-glow="1"]{position:relative;z-index:0}.pete-uno-card[data-glow="1"]:before{content:"";position:absolute;inset:-.35rem;z-index:-1;border-radius:.9rem;pointer-events:none;background:conic-gradient(from 0deg,#d64545,#e0b02c,#46a86b,#3d7fd6,#d64545);filter:blur(7px);opacity:.75;animation:pete-uno-glow 3.2s linear infinite}.pete-uno-card[data-glow="1"] .pete-uno-face:after{content:"";position:absolute;inset:0;z-index:2;pointer-events:none;background:linear-gradient(115deg,transparent 35%,hsla(0,0%,100%,.55) 50%,transparent 65%);background-size:260% 100%;animation:pete-uno-shine 2.6s ease-in-out infinite}@keyframes pete-uno-glow{to{transform:rotate(1turn)}}@keyframes pete-uno-shine{0%{background-position:140% 0}55%{background-position:-40% 0}to{background-position:-40% 0}}.pete-uno-pending{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.18rem .7rem;border-radius:999px;color:#fff;background:#cc3d4a;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-bill 1.1s ease-in-out infinite}@keyframes pete-uno-bill{0%,to{transform:scale(1)}50%{transform:scale(1.06)}}.pete-uno-seat[data-out="1"]{opacity:.4;filter:grayscale(1)}.pete-uno-seat[data-out="1"] .pete-uno-count{color:#ffb3ba}.pete-seg{display:inline-flex;padding:.2rem;border-radius:999px;background:color-mix(in srgb,var(--ink) 6%,transparent)}.pete-seg-btn{padding:.3rem .9rem;border-radius:999px;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:color-mix(in srgb,var(--ink) 55%,transparent);transition:background .15s ease,color .15s ease}.pete-seg-btn:hover{color:var(--ink)}.pete-seg-btn[data-on="1"]{background:var(--accent);color:#fff;box-shadow:0 2px 0 rgba(0,0,0,.15)}@media (max-width:639px){.pete-uno-hand{gap:.3rem}.pete-uno-deck,.pete-uno-hand{--uno-h:5.2rem;--uno-w:3.5rem}.pete-uno-card .pete-uno-oval[data-size=words]{font-size:.42rem}.pete-uno-card .pete-uno-oval[data-size=mid]{font-size:.9rem}.pete-uno-discard{--uno-h:5.2rem;--uno-w:3.5rem;height:5.2rem;width:3.5rem}.pete-uno-hand .pete-uno-card{padding:.22rem}.pete-uno-oval{font-size:1.15rem}.pete-uno-seat{padding:.35rem .45rem}}@media (prefers-reduced-motion:reduce){.pete-card,.pete-card:after,.pete-dealer-think,.pete-stack .pete-disc{animation:none}.pete-card-inner{transition:none}.pete-hand[data-won="1"] .pete-card,.pete-home-flash,.pete-meter[data-hit="1"],.pete-nope,.pete-part-draw,.pete-shake,.pete-tile-hit{animation:none}.pete-card[data-held="1"]{transition:none}.pete-answer[data-state=wrong],.pete-clock[data-hot="1"]{animation:none}.pete-rung{transition:none}.pete-uno-card[data-glow="1"]:before{animation:none}.pete-uno-card[data-glow="1"] .pete-uno-face:after{display:none}.pete-uno-pending{animation:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.-right-6{right:-1.5rem}.-top-6{top:-1.5rem}.-z-10{z-index:-10}.-z-\[5\]{z-index:-5}.z-50{z-index:50}.order-none{order:0}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.-mt-1{margin-top:-.25rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.mt-8{margin-top:2rem}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-2,.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-3{-webkit-line-clamp:3}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-\[16\/10\]{aspect-ratio:16/10}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.max-h-\[55vh\]{max-height:55vh}.max-h-\[60vh\]{max-height:60vh}.min-h-\[1\.75rem\]{min-height:1.75rem}.min-h-\[16rem\]{min-height:16rem}.min-h-\[2\.75rem\]{min-height:2.75rem}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-9{width:2.25rem}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[28rem\]{min-width:28rem}.min-w-\[52rem\]{min-width:52rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[7rem\]{max-width:7rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.-translate-y-0{--tw-translate-y:-0px}.-translate-y-0,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-10{-moz-column-gap:2.5rem;column-gap:2.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-3{row-gap:.75rem}.gap-y-4{row-gap:1rem}.gap-y-8{row-gap:2rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.justify-self-center{justify-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-dashed{border-style:dashed}.border-transparent{border-color:transparent}.bg-\[\#cc3d4a\]{--tw-bg-opacity:1;background-color:rgb(204 61 74/var(--tw-bg-opacity,1))}.bg-\[color\:var\(--accent\)\]{background-color:var(--accent)}.bg-\[color\:var\(--bg\)\]{background-color:var(--bg)}.bg-\[color\:var\(--bg-grad\)\]{background-color:var(--bg-grad)}.bg-\[color\:var\(--card\)\]{background-color:var(--card)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/25{background-color:rgba(0,0,0,.25)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/15{background-color:rgba(16,185,129,.15)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/15{background-color:rgba(239,68,68,.15)}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/95{background-color:hsla(0,0%,100%,.95)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-10{padding-bottom:2.5rem}.pb-20{padding-bottom:5rem}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pr-24{padding-right:6rem}.pr-32{padding-right:8rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.pt-\[10vh\]{padding-top:10vh}.pt-\[12vh\]{padding-top:12vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-\[-4px\]{vertical-align:-4px}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12rem\]{font-size:12rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#1c1305\]{--tw-text-opacity:1;color:rgb(28 19 5/var(--tw-text-opacity,1))}.text-\[\#20180c\]{--tw-text-opacity:1;color:rgb(32 24 12/var(--tw-text-opacity,1))}.text-\[\#2b2118\]{--tw-text-opacity:1;color:rgb(43 33 24/var(--tw-text-opacity,1))}.text-\[color\:var\(--accent\)\]{color:var(--accent)}.text-\[color\:var\(--ink\)\]{color:var(--ink)}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/40{color:hsla(0,0%,100%,.4)}.text-white\/50{color:hsla(0,0%,100%,.5)}.text-white\/55{color:hsla(0,0%,100%,.55)}.text-white\/60{color:hsla(0,0%,100%,.6)}.text-white\/70{color:hsla(0,0%,100%,.7)}.text-white\/85{color:hsla(0,0%,100%,.85)}.text-white\/90{color:hsla(0,0%,100%,.9)}.underline{text-decoration-line:underline}.decoration-4{text-decoration-thickness:4px}.underline-offset-2{text-underline-offset:2px}.underline-offset-4{text-underline-offset:4px}.accent-\[color\:var\(--accent\)\]{accent-color:var(--accent)}.opacity-20{opacity:.2}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale,.sepia{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sepia{--tw-sepia:sepia(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-1000{transition-duration:1s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.font-display{font-family:Fredoka,Nunito,system-ui,sans-serif;letter-spacing:-.01em}.shadow-pete{box-shadow:0 4px 0 rgba(60,40,20,.1),0 8px 24px rgba(60,40,20,.08)}.shadow-pete-lg{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.bg-theme-gaming{background-color:#4caf7d}.bg-theme-tech{background-color:#5aa9e6}.bg-theme-politics{background-color:#e07a5f}.bg-theme-eu{background-color:#039}.bg-theme-music{background-color:#b079d6}.bg-theme-anime{background-color:#ec5e8a}.bg-theme-foss{background-color:#d97706}.bg-theme-kids{background-color:#14b8a6}.bg-theme-finance{background-color:#10b981}.bg-theme-lego{background-color:#d01012}.bg-theme-adventure{background-color:#6d4bd8}.text-theme-gaming{color:#2d8a5a}.text-theme-tech{color:#2f7fb8}.text-theme-politics{color:#b8523a}.text-theme-eu{color:#039}.text-theme-music{color:#8a4fb8}.text-theme-anime{color:#c33a6a}.text-theme-foss{color:#b8530a}.text-theme-kids{color:#0f766e}.text-theme-finance{color:#059669}.text-theme-lego{color:#b00d0e}.text-theme-adventure{color:#5836b8}.decoration-theme-gaming{text-decoration-color:#4caf7d}.decoration-theme-tech{text-decoration-color:#5aa9e6}.decoration-theme-politics{text-decoration-color:#e07a5f}.decoration-theme-eu{text-decoration-color:#039}.decoration-theme-music{text-decoration-color:#b079d6}.decoration-theme-anime{text-decoration-color:#ec5e8a}.decoration-theme-foss{text-decoration-color:#d97706}.decoration-theme-kids{text-decoration-color:#14b8a6}.decoration-theme-finance{text-decoration-color:#10b981}.decoration-theme-lego{text-decoration-color:#d01012}.decoration-theme-adventure{text-decoration-color:#6d4bd8}.border-theme-gaming{border-color:#4caf7d}.border-theme-tech{border-color:#5aa9e6}.border-theme-politics{border-color:#e07a5f}.border-theme-eu{border-color:#039}.border-theme-music{border-color:#b079d6}.border-theme-anime{border-color:#ec5e8a}.border-theme-foss{border-color:#d97706}.border-theme-kids{border-color:#14b8a6}.border-theme-finance{border-color:#10b981}.border-theme-lego{border-color:#d01012}.border-theme-adventure{border-color:#6d4bd8}.glow-theme-gaming{box-shadow:0 0 0 2px rgba(76,175,125,.35),0 0 24px 4px rgba(76,175,125,.45)}.glow-theme-gaming,.glow-theme-tech{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-tech{box-shadow:0 0 0 2px rgba(90,169,230,.35),0 0 24px 4px rgba(90,169,230,.45)}.glow-theme-politics{box-shadow:0 0 0 2px rgba(224,122,95,.35),0 0 24px 4px rgba(224,122,95,.45)}.glow-theme-eu,.glow-theme-politics{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-eu{box-shadow:0 0 0 2px rgba(0,51,153,.35),0 0 24px 4px rgba(0,51,153,.45)}.glow-theme-music{box-shadow:0 0 0 2px rgba(176,121,214,.35),0 0 24px 4px rgba(176,121,214,.45)}.glow-theme-anime,.glow-theme-music{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-anime{box-shadow:0 0 0 2px rgba(236,94,138,.35),0 0 24px 4px rgba(236,94,138,.45)}.glow-theme-foss{box-shadow:0 0 0 2px rgba(217,119,6,.35),0 0 24px 4px rgba(217,119,6,.45)}.glow-theme-foss,.glow-theme-kids{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-kids{box-shadow:0 0 0 2px rgba(20,184,166,.35),0 0 24px 4px rgba(20,184,166,.45)}.glow-theme-finance{box-shadow:0 0 0 2px rgba(16,185,129,.35),0 0 24px 4px rgba(16,185,129,.45)}.glow-theme-finance,.glow-theme-lego{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-lego{box-shadow:0 0 0 2px rgba(208,16,18,.35),0 0 24px 4px rgba(208,16,18,.45)}.glow-theme-adventure{box-shadow:0 0 0 2px rgba(109,75,216,.35),0 0 24px 4px rgba(109,75,216,.45);animation:pete-glow-pulse 2.4s ease-in-out infinite}@keyframes pete-glow-pulse{0%,to{filter:brightness(1)}50%{filter:brightness(1.08)}}.line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.paywall-stamp{position:absolute;inset:0;pointer-events:none;display:flex;align-items:center;justify-content:center;z-index:5;background:rgba(255,250,240,.18)}.paywall-stamp:before{content:"PAYWALLED";font-family:var(--font-display,ui-sans-serif),system-ui,sans-serif;font-weight:900;font-size:clamp(1rem,3.2vw,1.75rem);letter-spacing:.12em;color:rgba(180,30,30,.85);border:.28rem double rgba(180,30,30,.85);padding:.3rem .9rem;transform:rotate(-15deg);text-shadow:0 1px 0 hsla(0,0%,100%,.4);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.15);background:rgba(255,240,230,.55);border-radius:.25rem;filter:contrast(1.05)}:root,html[data-phase=day]{--bg:#fff7e4;--bg-grad:#ffeec2;--card:#fff;--ink:#3a2e1f;--accent:#f2a541}html[data-phase=dawn]{--bg:#ffe7d6;--bg-grad:#ffc9c9;--card:#fff4ea;--ink:#4a2e2a;--accent:#ff8a65}html[data-phase=dusk]{--bg:#ffd6a8;--bg-grad:#f7a07e;--card:#fff1de;--ink:#3d2417;--accent:#e6553a}html[data-phase=night]{--bg:#1a1f3a;--bg-grad:#2a3358;--card:#2d365a;--ink:#f1ecd8;--accent:#f9d976}html[data-room=casinopolis]{--bg:#16211c;--card:#23342b;--ink:#f6ecd8;--accent:#f2b53d;--felt-a:#2f7d5b;--felt-b:#24614a;--felt-c:#1c4d3c;--glow:242,181,61}html[data-room=casino-night]{--bg:#140f2e;--card:#241a4d;--ink:#f2ecff;--accent:#ffcc2f;--felt-a:#4a2fa8;--felt-b:#351f80;--felt-c:#241659;--glow:126,86,255}html[data-room] .cs-room{background:radial-gradient(80% 55% at 50% -8%,rgba(var(--glow),.18),transparent 62%),radial-gradient(120% 90% at 50% 120%,rgba(0,0,0,.45),transparent 55%)}html[data-room=casino-night] .cs-room:before{content:"";position:absolute;inset:0 -50% auto -50%;height:.5rem;background:repeating-linear-gradient(90deg,rgba(255,204,47,.55) 0 .5rem,transparent .5rem 2.25rem);filter:blur(1px);animation:cs-bulbs 2.4s linear infinite}@keyframes cs-bulbs{0%{transform:translateX(0)}to{transform:translateX(2.75rem)}}html[data-room] .shadow-pete{box-shadow:0 4px 0 rgba(0,0,0,.3),0 10px 26px rgba(0,0,0,.3)}html[data-room] .shadow-pete-lg{box-shadow:0 6px 0 rgba(0,0,0,.34),0 20px 40px rgba(0,0,0,.38)}html[data-room] .cs-comb svg{filter:drop-shadow(0 3px 0 rgba(0,0,0,.35))}html[data-room] .pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,var(--felt-a) 0,var(--felt-b) 55%,var(--felt-c) 100%)}@media (prefers-reduced-motion:reduce){html[data-room=casino-night] .cs-room:before{animation:none}}.cs-stack{display:block;width:2.75rem;height:calc(.45rem*var(--stack, 1) + .55rem);border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 60%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.25);box-shadow:0 3px 0 rgba(0,0,0,.3),inset 0 -.4rem 0 rgba(0,0,0,.16),inset 0 .35rem 0 hsla(0,0%,100%,.14)}.cs-stack[data-chip="5"]{--chip:#5aa9e6}.cs-stack[data-chip="25"]{--chip:#4caf7d}.cs-stack[data-chip="100"]{--chip:#2b2118}.cs-stack[data-chip="500"]{--chip:#b079d6}.pete-poker{--seat-w:7.5rem}.pete-poker-seats{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;gap:.75rem}.pete-seat{position:relative;display:flex;flex-direction:column;align-items:center;gap:.4rem;width:var(--seat-w);transition:opacity .3s ease,filter .3s ease}.pete-seat[data-state=folded]{opacity:.38;filter:grayscale(.7)}.pete-seat[data-state=out]{opacity:.2}.pete-seat[data-turn="1"] .pete-seat-plate{box-shadow:0 0 0 2px var(--accent),0 0 1.6rem -.2rem var(--accent);animation:pete-seat-wait 1.6s ease-in-out infinite}@keyframes pete-seat-wait{0%,to{transform:translateY(0)}50%{transform:translateY(-2px)}}.pete-seat-cards{display:flex;justify-content:center;gap:.15rem;min-height:4.4rem;--card-h:4.4rem;--card-w:3.15rem}.pete-seat-cards[data-mucked="1"]{opacity:0;transform:translateY(-.6rem) scale(.9);transition:all .35s ease}.pete-seat-plate{position:relative;display:flex;flex-direction:column;align-items:center;width:100%;padding:.3rem .4rem;border-radius:.85rem;background:rgba(0,0,0,.32);border:1px solid hsla(0,0%,100%,.12);transition:box-shadow .25s ease}.pete-seat-name{font-family:var(--font-display,inherit);font-weight:700;font-size:.8rem;line-height:1.1;color:hsla(0,0%,100%,.92);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pete-seat-stack{font-weight:700;font-size:.78rem;font-variant-numeric:tabular-nums;color:hsla(0,0%,100%,.58)}.pete-seat[data-state=allin] .pete-seat-stack:after{content:" all in";color:var(--accent)}.pete-seat-pos{position:absolute;top:-.5rem;right:-.5rem;display:grid;place-items:center;min-width:1.45rem;height:1.45rem;padding:0 .3rem;border-radius:999px;font-size:.58rem;font-weight:800;letter-spacing:.02em;background:hsla(0,0%,100%,.9);color:#2b2118;box-shadow:0 2px 0 rgba(0,0,0,.3)}.pete-seat-pos[data-pos=BTN]{background:#f5d76e}.pete-seat-spot{height:3.6rem;width:3.6rem;margin-bottom:.5rem}.pete-seat-spot .pete-disc{height:1.6rem;width:1.6rem;margin:-.8rem 0 0 -.8rem}.pete-seat-spot .pete-spot-total{font-size:.66rem;padding:.05rem .45rem}.pete-poker-middle{display:flex;flex-direction:column;align-items:center;gap:.35rem;padding:.5rem 0}.pete-poker-pot{margin-top:.9rem}.pete-poker-board{display:flex;gap:.35rem;min-height:6rem;--card-h:6rem;--card-w:4.3rem}.pete-poker-pot{display:flex;flex-direction:column;align-items:center;gap:.1rem}.pete-poker-pot-pile{position:relative;width:7rem;height:3rem}.pete-poker-pot-label{font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:hsla(0,0%,100%,.42)}.pete-poker-pot-total{font-family:var(--font-display,inherit);font-size:1.5rem;font-weight:800;font-variant-numeric:tabular-nums;color:#fff;text-shadow:0 2px 0 rgba(0,0,0,.28)}.pete-poker-side{font-size:.68rem;font-weight:700;color:hsla(0,0%,100%,.5)}.pete-poker-you .pete-seat{width:auto}.pete-poker-you .pete-seat-cards{--card-h:6.8rem;--card-w:4.85rem;gap:.3rem;min-height:6.8rem}.pete-poker-you .pete-seat-plate{padding:.4rem 1.1rem}.pete-poker-you .pete-seat-name,.pete-poker-you .pete-seat-stack{font-size:.95rem}.pete-poker-verdict{border-radius:999px;background:hsla(0,0%,100%,.95);padding:.5rem 1.25rem;font-family:var(--font-display,inherit);font-size:1.05rem;font-weight:700;color:#2b2118;box-shadow:0 4px 0 rgba(0,0,0,.18)}.pete-poker-verdict[data-tone=win]{background:#4caf7d;color:#fff}.pete-poker-verdict[data-tone=lose]{background:hsla(0,0%,100%,.9);color:#7a5c50}.pete-raise{display:flex;align-items:center;gap:.6rem;flex:1 1 14rem;min-width:0}.pete-raise input[type=range]{flex:1;min-width:0;accent-color:var(--accent)}.pete-raise-to{font-family:var(--font-display,inherit);font-size:1.15rem;font-weight:800;font-variant-numeric:tabular-nums;min-width:3.5rem;text-align:right}.pete-poker-log{max-height:7rem;overflow-y:auto;font-size:.75rem;line-height:1.5;color:hsla(0,0%,100%,.55)}.pete-poker-log b{color:hsla(0,0%,100%,.85);font-weight:700}@media (max-width:640px){.pete-poker{--seat-w:5.6rem}.pete-seat-cards{--card-h:3.5rem;--card-w:2.5rem;min-height:3.5rem}.pete-poker-board{--card-h:5rem;--card-w:3.55rem;min-height:5rem;gap:.25rem}.pete-poker-you .pete-seat-cards{--card-h:6rem;--card-w:4.3rem;min-height:6rem}.pete-poker-pot-total{font-size:1.25rem}}.last\:border-0:last-child{border-width:0}.empty\:hidden:empty{display:none}.hover\:-translate-y-0\.5:hover{--tw-translate-y:-0.125rem}.hover\:-translate-y-0\.5:hover,.hover\:-translate-y-1:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-1:hover{--tw-translate-y:-0.25rem}.hover\:text-\[color\:var\(--ink\)\]:hover{color:var(--ink)}.hover\:text-white\/80:hover{color:hsla(0,0%,100%,.8)}.hover\:brightness-105:hover{--tw-brightness:brightness(1.05);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:shadow-pete-lg:hover{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.focus\:border-\[color\:var\(--accent\)\]:focus{border-color:var(--accent)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:translate-y-px:active{--tw-translate-y:1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:-rotate-6{--tw-rotate:-6deg}.group:hover .group-hover\:-rotate-6,.group:hover .group-hover\:scale-105{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:underline{text-decoration-line:underline}@media (min-width:640px){.sm\:ml-auto{margin-left:auto}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:inline-flex{display:inline-flex}.sm\:h-56{height:14rem}.sm\:basis-auto{flex-basis:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-2{gap:.5rem}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:gap-5{gap:1.25rem}.sm\:gap-8{gap:2rem}.sm\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:p-8{padding:2rem}.sm\:pr-28{padding-right:7rem}.sm\:pt-10{padding-top:2.5rem}.sm\:pt-12{padding-top:3rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-6xl{font-size:3.75rem;line-height:1}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[auto\2c 1fr\]{grid-template-columns:auto 1fr}.lg\:grid-cols-\[minmax\(0\2c 1fr\)\2c auto\]{grid-template-columns:minmax(0,1fr) auto}.lg\:items-start{align-items:flex-start}.lg\:gap-8{gap:2rem}.lg\:p-8{padding:2rem}}@media (prefers-color-scheme:dark){.dark\:text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.dark\:text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.dark\:text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.dark\:text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}} \ No newline at end of file +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{font-family:Nunito,system-ui,sans-serif}html{scroll-behavior:smooth}body,html{overflow-x:hidden}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pete-channel-nav{scrollbar-width:none;-ms-overflow-style:none}.pete-channel-nav::-webkit-scrollbar{display:none}.pete-reader-backdrop{position:absolute;inset:0;background:rgba(20,14,6,.55);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}html[data-phase=night] .pete-reader-backdrop{background:rgba(6,8,20,.65)}.pete-reader-shell{position:relative;height:100%;width:100%;max-width:44rem;margin:0 auto;display:flex;flex-direction:column;padding:max(env(safe-area-inset-top),.75rem) 1rem .75rem;gap:.75rem}.pete-reader-bar{display:flex;align-items:center;justify-content:space-between;gap:.75rem;color:#fff}.pete-reader-progress{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:600;font-size:.85rem;letter-spacing:.02em;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-nav{display:flex;align-items:center;gap:.4rem}.pete-reader-btn{display:inline-flex;align-items:center;justify-content:center;min-width:2.25rem;height:2.25rem;padding:0 .7rem;border-radius:9999px;background:hsla(0,0%,100%,.14);color:#fff;font-weight:700;font-size:.95rem;border:2px solid hsla(0,0%,100%,.18);cursor:pointer;transition:background .15s ease,transform .15s ease,opacity .15s ease}.pete-reader-btn:hover{background:hsla(0,0%,100%,.28);transform:translateY(-1px)}.pete-reader-btn:disabled{opacity:.35;cursor:default;transform:none}.pete-reader-btn-open{background:var(--accent);border-color:transparent;color:#1c1305;text-decoration:none}.pete-reader-btn-open:hover{background:var(--accent);filter:brightness(1.08)}.pete-reader-btn[aria-expanded=true],.pete-reader-btn[aria-pressed=true]{background:var(--accent);border-color:transparent;color:#1c1305}.pete-reader-type{position:relative;display:inline-flex}.pete-reader-typemenu[hidden]{display:none}.pete-reader-typemenu{position:absolute;top:calc(100% + .5rem);right:0;z-index:10;width:15rem;padding:.75rem;border-radius:1rem;background:var(--card);color:var(--ink);border:2px solid rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(20,14,6,.28);display:flex;flex-direction:column;gap:.6rem}html[data-phase=night] .pete-reader-typemenu{border-color:hsla(0,0%,100%,.12)}.pete-reader-typerow{display:flex;align-items:center;justify-content:space-between;gap:.6rem}.pete-reader-typelabel{font-size:.8rem;font-weight:700;opacity:.7}.pete-reader-typebtns{display:inline-flex;gap:.25rem}.pete-reader-typebtns button{min-width:2rem;height:2rem;padding:0 .55rem;border-radius:.6rem;background:rgba(20,14,6,.06);color:var(--ink);font-weight:700;font-size:.82rem;border:2px solid transparent;cursor:pointer;transition:background .15s ease}html[data-phase=night] .pete-reader-typebtns button{background:hsla(0,0%,100%,.08)}.pete-reader-typebtns button:hover{background:rgba(20,14,6,.12)}.pete-reader-typebtns button.is-active{background:var(--accent);color:#1c1305;border-color:transparent}.pete-reader-voicerow[hidden]{display:none}.pete-reader-voice{font:inherit;font-size:.8rem;font-weight:600;padding:.25rem .4rem;border-radius:.55rem;border:1px solid rgba(20,14,6,.18);background:rgba(20,14,6,.06);color:inherit;max-width:11rem;cursor:pointer}html[data-phase=night] .pete-reader-voice{border-color:hsla(0,0%,100%,.14);background:hsla(0,0%,100%,.08)}.pete-reader-scroll{flex:1 1 auto;overflow-y:auto;-webkit-overflow-scrolling:touch;border-radius:1.75rem}.pete-reader-scroll:focus{outline:none}.pete-reader-article{background:var(--card);color:var(--ink);border-radius:1.75rem;border:2px solid rgba(0,0,0,.06);box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.18);padding:clamp(1.25rem,4vw,2.5rem)}html[data-phase=night] .pete-reader-article{border-color:hsla(0,0%,100%,.08)}.pete-reader-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.75rem;margin-bottom:.85rem}.pete-reader-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:9999px;padding:.15rem .65rem;color:#fff;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.pete-reader-source{font-weight:700;opacity:.75}.pete-reader-time{opacity:.55}.pete-reader-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;line-height:1.15;letter-spacing:-.01em;font-size:clamp(1.6rem,4.5vw,2.4rem);margin-bottom:1rem}.pete-reader-hero{width:100%;border-radius:1.1rem;margin-bottom:1.25rem;background:rgba(0,0,0,.05)}.pete-reader-scroll{--reader-fs:1.08rem}.pete-reader-body{font-size:var(--reader-fs);line-height:1.75}.pete-reader-body p{margin-bottom:1.05em}.pete-reader-body p:last-child{margin-bottom:0}.pete-reader-scroll.is-size-s{--reader-fs:0.98rem}.pete-reader-scroll.is-size-m{--reader-fs:1.08rem}.pete-reader-scroll.is-size-l{--reader-fs:1.22rem}.pete-reader-scroll.is-size-xl{--reader-fs:1.4rem}.pete-reader-scroll.is-serif .pete-reader-body{font-family:Georgia,Iowan Old Style,Times New Roman,serif}.pete-reader-scroll.is-sepia .pete-reader-article{background:#f6ecd6;color:#4a3a28;border-color:rgba(74,58,40,.16)}.pete-reader-scroll.is-sepia .pete-reader-note{border-top-color:rgba(74,58,40,.18);opacity:.8}.pete-reader-scroll.is-sepia .pete-reader-hero{background:rgba(74,58,40,.08)}.pete-reader-body p.is-speaking{background:color-mix(in srgb,var(--accent) 32%,transparent);border-radius:.4rem;box-shadow:0 0 0 .35rem color-mix(in srgb,var(--accent) 32%,transparent)}.pete-reader-note{margin-top:1.25rem;padding-top:1rem;border-top:1px solid rgba(0,0,0,.1);font-size:.85rem;opacity:.7}html[data-phase=night] .pete-reader-note{border-color:hsla(0,0%,100%,.12)}.pete-reader-note a{color:var(--accent);font-weight:700}.pete-reader-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:.75rem;padding:clamp(2rem,8vw,4rem) 1rem;color:var(--ink)}.pete-reader-empty-emoji{font-size:2.5rem}.pete-reader-toast{position:absolute;left:50%;bottom:4.5rem;transform:translate(-50%,.5rem);z-index:20;padding:.5rem .9rem;border-radius:9999px;background:rgba(20,14,6,.9);color:#fff;font-weight:700;font-size:.85rem;box-shadow:0 8px 24px rgba(0,0,0,.3);opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease}.pete-reader-toast.is-shown{opacity:1;transform:translate(-50%)}.pete-reader-hint{text-align:center;color:hsla(0,0%,100%,.85);font-size:.75rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-hint kbd{font-family:ui-monospace,monospace;background:hsla(0,0%,100%,.16);border-radius:.3rem;padding:.05rem .3rem}@media (max-width:640px){.pete-reader-hint{display:none}.pete-reader-bar{flex-wrap:wrap}.pete-reader-nav{flex-wrap:wrap;justify-content:flex-end;row-gap:.4rem}}.pete-reader-related{width:100%;margin:.85rem auto 0}.pete-reader-related-title{font-family:Fredoka,Nunito,system-ui,sans-serif;font-weight:700;font-size:.95rem;color:#fff;margin:0 0 .6rem;text-shadow:0 1px 2px rgba(0,0,0,.4)}.pete-reader-related-grid{display:grid;gap:.6rem}.pete-reader-related-card{display:flex;align-items:center;gap:.75rem;background:var(--card);color:var(--ink);border-radius:1rem;border:2px solid rgba(0,0,0,.06);padding:.6rem;text-decoration:none;transition:transform .15s ease,box-shadow .15s ease}.pete-reader-related-card:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(60,40,20,.16)}html[data-phase=night] .pete-reader-related-card{border-color:hsla(0,0%,100%,.08)}.pete-reader-related-thumb{width:4.5rem;height:3.25rem;flex-shrink:0;-o-object-fit:cover;object-fit:cover;border-radius:.6rem;background:rgba(0,0,0,.06)}.pete-reader-related-meta{min-width:0;display:flex;flex-direction:column;gap:.25rem}.pete-reader-related-eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:.4rem;font-size:.7rem}.pete-reader-related-source{font-weight:700;opacity:.7}.pete-reader-related-headline{font-weight:700;line-height:1.25;font-size:.92rem;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}[data-story-card][data-read="1"]{opacity:.5;transition:opacity .2s ease}[data-story-card][data-read="1"]:hover{opacity:1}[data-story-card][data-read="1"]:after{content:"✓";position:absolute;top:.6rem;right:.6rem;z-index:6;display:grid;place-items:center;height:1.5rem;width:1.5rem;border-radius:9999px;background:rgba(20,14,6,.72);color:#fff;font-size:.8rem;font-weight:700}.pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,#2f7d5b,#24614a 55%,#1c4d3c)}.pete-shoe{position:absolute;top:1.25rem;right:1.25rem;height:4.2rem;width:3rem;border-radius:.55rem;background:linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.18);box-shadow:0 4px 0 rgba(0,0,0,.18),inset 0 0 0 3px hsla(0,0%,100%,.12)}.pete-shoe:after{content:"";position:absolute;inset:.45rem .4rem auto .4rem;height:.5rem;border-radius:.2rem;background:rgba(0,0,0,.22)}.pete-hand{display:flex;flex-wrap:wrap;gap:.6rem;min-height:8.6rem}.pete-card{perspective:700px;height:var(--card-h,8.4rem);width:var(--card-w,6rem);animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards}.pete-card-inner{position:relative;height:100%;width:100%;transform-style:preserve-3d;transition:transform .45s cubic-bezier(.4,0,.2,1)}.pete-card[data-face=down] .pete-card-inner{transform:rotateY(180deg)}.pete-card-back,.pete-card-front{position:absolute;inset:0;display:grid;border-radius:.55rem;backface-visibility:hidden;-webkit-backface-visibility:hidden;box-shadow:0 3px 0 rgba(0,0,0,.18),0 6px 14px rgba(0,0,0,.22)}.pete-card-front{place-items:stretch;background:#fdfaf2;border:2px solid rgba(30,20,10,.12);color:#2b2118;font-family:Fredoka,Nunito,system-ui,sans-serif;line-height:1;overflow:hidden}.pete-card-front[data-red="1"]{color:#cc3d4a}.pete-card-back{transform:rotateY(180deg);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);border:2px solid rgba(0,0,0,.15)}.pete-card-svg{width:100%;height:100%;display:block;fill:currentColor}.pete-card-idx{font-size:26px;font-weight:700;text-anchor:middle;fill:currentColor}.pete-card-panel{fill:currentColor;fill-opacity:.06;stroke:currentColor;stroke-opacity:.35;stroke-width:1.5}.pete-card-court{font-size:34px;font-weight:700;text-anchor:middle;fill:currentColor}@keyframes pete-deal{0%{opacity:0;transform:translate(var(--deal-x,14rem),var(--deal-y,-7rem)) scale(.72) rotate(9deg)}62%{opacity:1;transform:translateY(.35rem) scale(1.05) rotate(calc(var(--tilt, 0deg) - 2deg))}to{opacity:1;transform:translate(0) scale(1) rotate(var(--tilt,0deg))}}.pete-card:after{content:"";position:absolute;inset:auto 6% -.35rem 6%;height:.6rem;border-radius:999px;background:rgba(0,0,0,.35);filter:blur(5px);animation:pete-thud .42s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-thud{0%,55%{opacity:0;transform:scale(.4)}72%{opacity:.9;transform:scale(1.15)}to{opacity:.45;transform:scale(1)}}.pete-hand[data-won="1"] .pete-card{animation:pete-deal .42s cubic-bezier(.22,1,.36,1) backwards,pete-win .6s ease .1s}@keyframes pete-win{0%,to{transform:rotate(var(--tilt,0deg)) translateY(0)}40%{transform:rotate(var(--tilt,0deg)) translateY(-.6rem)}}.pete-hand[data-won="-1"] .pete-card{filter:saturate(.45) brightness(.78);transition:filter .5s ease .2s}.pete-disc{position:relative;border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 62%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.22);box-shadow:0 3px 0 rgba(0,0,0,.28),0 6px 14px rgba(0,0,0,.2)}.pete-disc:before{content:"";position:absolute;inset:11%;border-radius:999px;border:2px dashed hsla(0,0%,100%,.55)}.pete-disc[data-chip="5"]{--chip:#5aa9e6}.pete-disc[data-chip="25"]{--chip:#4caf7d}.pete-disc[data-chip="100"]{--chip:#2b2118}.pete-disc[data-chip="500"]{--chip:#b079d6}.pete-chip{transition:transform .12s ease}.pete-chip>span{position:relative}.pete-chip:active{transform:translateY(1px) scale(.94)}.pete-spot{position:relative;display:grid;place-items:center;height:7rem;width:7rem;flex:none;border-radius:999px;border:2px dashed hsla(0,0%,100%,.35);background:radial-gradient(circle at 50% 40%,hsla(0,0%,100%,.1),transparent 70%);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08);transition:box-shadow .3s ease,border-color .3s ease}.pete-spot[data-live="1"]{border-color:rgba(var(--glow,242,181,61),.85);box-shadow:inset 0 0 0 6px rgba(0,0,0,.08),0 0 22px rgba(var(--glow,242,181,61),.35)}.pete-spot-label{font-size:.65rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:hsla(0,0%,100%,.35)}.pete-spot[data-live="1"] .pete-spot-label{opacity:0}.bj-hands{display:flex;flex-wrap:wrap;align-items:flex-start;gap:.75rem 1rem}.bj-hand{display:flex;align-items:center;gap:.85rem;border-radius:1.5rem;padding:.4rem .7rem .4rem .4rem;transition:opacity .25s ease,box-shadow .25s ease,background .25s ease}.bj-hand[data-live="0"]{opacity:.5}.bj-hand[data-live="1"]{background:rgba(0,0,0,.16);box-shadow:0 0 0 2px rgba(var(--glow,242,181,61),.5)}.bj-hands[data-count="2"]{--card-h:6.6rem;--card-w:4.7rem}.bj-hands[data-count="3"],.bj-hands[data-count="4"]{--card-h:5rem;--card-w:3.6rem}.bj-hands[data-count="2"] .pete-hand{min-height:6.8rem}.bj-hands[data-count="3"] .pete-hand,.bj-hands[data-count="4"] .pete-hand{min-height:5.2rem;gap:.35rem}.bj-hands[data-count="2"] .pete-spot{height:5.5rem;width:5.5rem}.bj-hands[data-count="3"] .pete-spot,.bj-hands[data-count="4"] .pete-spot{height:4.25rem;width:4.25rem}.bj-hands[data-count="3"] .pete-spot-label,.bj-hands[data-count="4"] .pete-spot-label{display:none}.bj-splitting{animation:bj-split-lift .32s ease forwards}@keyframes bj-split-lift{0%{transform:translateY(0) rotate(0)}to{transform:translateY(-1.1rem) rotate(6deg)}}.pete-stack{position:absolute;inset:0;pointer-events:none}.pete-stack .pete-disc{position:absolute;left:50%;top:50%;height:2.6rem;width:2.6rem;margin:-1.3rem 0 0 -1.3rem;transform:translateY(calc(-.5rem*var(--i, 0))) rotate(var(--spin,0deg));box-shadow:0 2px 0 rgba(0,0,0,.35),0 4px 8px rgba(0,0,0,.25);animation:pete-chip-land .28s cubic-bezier(.34,1.56,.64,1) backwards}@keyframes pete-chip-land{0%{transform:translateY(calc(-.5rem*var(--i, 0) - 1.1rem)) rotate(var(--spin,0deg)) scale(1.18)}}.pete-spot-total{position:absolute;bottom:-.6rem;left:50%;transform:translateX(-50%);white-space:nowrap;border-radius:999px;background:rgba(0,0,0,.45);padding:.1rem .6rem;font-size:.72rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-fly-layer{position:fixed;inset:0;z-index:60;pointer-events:none;overflow:hidden}.pete-fly{position:absolute;height:2.6rem;width:2.6rem;margin:-1.3rem 0 0 -1.3rem;will-change:transform}.pete-rack{position:absolute;top:1.25rem;right:5.75rem;display:flex;align-items:flex-end;gap:.4rem;padding:.5rem .6rem .45rem;border-radius:.75rem;background:rgba(0,0,0,.2);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.06)}.pete-rack span{display:block;width:1.7rem;border-radius:.35rem;background:linear-gradient(90deg,rgba(0,0,0,.28),transparent 35%,transparent 65%,rgba(0,0,0,.28)),repeating-linear-gradient(180deg,hsla(0,0%,100%,.18) 0 .06rem,var(--chip,#e07a5f) .06rem .3rem,rgba(0,0,0,.35) .3rem .36rem);border:1px solid rgba(0,0,0,.35);height:calc(.36rem*var(--stack, 3))}.pete-rack span[data-chip="5"]{--chip:#5aa9e6}.pete-rack span[data-chip="25"]{--chip:#4caf7d}.pete-rack span[data-chip="100"]{--chip:#2b2118}.pete-rack span[data-chip="500"]{--chip:#b079d6}@media (max-width:639px){.pete-rack:not([data-at=rail]){gap:.25rem;padding:.35rem .4rem .3rem}.pete-rack:not([data-at=rail]) span{width:1.15rem}.pete-rack:not([data-at]){right:1rem}}.pete-spark{position:absolute;height:.75rem;width:.45rem;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,.35);will-change:transform,opacity}.pete-dealer-think{animation:pete-think .9s ease-in-out infinite}@keyframes pete-think{0%,to{opacity:.6}50%{opacity:1;transform:translateY(-1px)}}.pete-gallows{overflow:visible;fill:none;stroke-linecap:round;stroke-linejoin:round}.pete-gallows-frame path{stroke:rgba(0,0,0,.35);stroke-width:6}.pete-gallows-frame path:last-child{stroke:rgba(0,0,0,.3);stroke-width:3.5}.pete-gallows-body>*{stroke:#fff;stroke-width:5;opacity:0;filter:drop-shadow(0 2px 0 rgba(0,0,0,.25))}.pete-gallows-body>[data-on="1"]{opacity:1}.pete-part-draw{stroke-dasharray:260;animation:pete-part .45s cubic-bezier(.22,1,.36,1) backwards}@keyframes pete-part{0%{stroke-dashoffset:260}to{stroke-dashoffset:0}}.pete-shake{animation:pete-shake .42s cubic-bezier(.36,.07,.19,.97)}@keyframes pete-shake{10%,90%{transform:translateX(-1.5px)}20%,80%{transform:translateX(3px)}30%,50%,70%{transform:translateX(-5px)}40%,60%{transform:translateX(5px)}}.pete-board{display:flex;flex-wrap:wrap;gap:.35rem 1.1rem;min-height:5rem;align-items:center}.pete-word{display:flex;gap:.3rem}.pete-tile{display:grid;place-items:center;height:2.9rem;width:2.2rem;border-radius:.5rem;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.4rem;font-weight:700;color:#fff;background:rgba(0,0,0,.22);border-bottom:3px solid rgba(0,0,0,.28);text-transform:uppercase}.pete-tile[data-up="1"]{background:hsla(0,0%,100%,.95);color:#2b2118;border-bottom-color:rgba(0,0,0,.35)}.pete-tile[data-punct="1"]{background:none;border:0;width:auto;min-width:.7rem;color:hsla(0,0%,100%,.55)}.pete-tile-hit{animation:pete-tile-hit .34s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-tile-hit{0%{transform:rotateX(90deg) scale(1.1)}to{transform:rotateX(0) scale(1)}}.pete-missed{display:grid;place-items:center;height:1.5rem;min-width:1.5rem;padding:0 .3rem;border-radius:.375rem;background:rgba(0,0,0,.25);font-size:.75rem;font-weight:700;color:hsla(0,0%,100%,.5);text-decoration:line-through;text-transform:uppercase}.pete-meter{display:flex;align-items:baseline;gap:.5rem;border-radius:999px;background:rgba(0,0,0,.28);padding:.4rem .9rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:box-shadow .3s ease}.pete-meter-label{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:hsla(0,0%,100%,.45)}.pete-meter-value{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.5rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--accent)}.pete-meter[data-cold="1"] .pete-meter-value{color:hsla(0,0%,100%,.55)}.pete-meter[data-hit="1"]{box-shadow:inset 0 0 0 2px rgba(204,61,74,.9);animation:pete-meter-hit .4s ease}@keyframes pete-meter-hit{0%{transform:scale(1)}35%{transform:scale(.94)}to{transform:scale(1)}}.pete-keys{display:grid;gap:.35rem}.pete-key-row{display:flex;justify-content:center;gap:.35rem}.pete-key-row[data-digits="1"]{margin-top:.25rem;opacity:.75}.pete-key-row[data-digits="1"] .pete-key{height:2rem;min-width:1.8rem;font-size:.8rem}.pete-key{height:2.75rem;min-width:2.2rem;flex:0 1 2.4rem;border-radius:.6rem;background:color-mix(in srgb,var(--ink) 6%,transparent);border:2px solid color-mix(in srgb,var(--ink) 10%,transparent);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-weight:700;color:var(--ink);transition:transform .08s ease,background .15s ease,opacity .15s ease}.pete-key:hover:not(:disabled){background:color-mix(in srgb,var(--ink) 12%,transparent)}.pete-key:active:not(:disabled){transform:translateY(1px) scale(.96)}.pete-key:disabled{cursor:default}.pete-key[data-state=hit]{background:#4caf7d;border-color:#3d9367;color:#fff;opacity:1}.pete-key[data-state=miss]{background:color-mix(in srgb,var(--ink) 4%,transparent);color:color-mix(in srgb,var(--ink) 30%,transparent);text-decoration:line-through}.pete-key:disabled[data-state=""]{opacity:.4}.pete-tier{background:color-mix(in srgb,var(--ink) 4%,transparent);border-color:color-mix(in srgb,var(--ink) 10%,transparent)}.pete-tier:hover{background:color-mix(in srgb,var(--ink) 8%,transparent)}.pete-tier[data-on="1"]{background:color-mix(in srgb,var(--accent) 18%,transparent);border-color:var(--accent)}.pete-clock{position:relative;height:.6rem;border-radius:999px;background:rgba(0,0,0,.3);overflow:hidden;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.08)}.pete-clock-fill{height:100%;width:100%;border-radius:999px;background:var(--accent);transform-origin:left center;transform:scaleX(1)}.pete-clock[data-hot="1"] .pete-clock-fill{background:#cc3d4a}.pete-clock[data-hot="1"]{animation:pete-clock-pulse .7s ease-in-out infinite}@keyframes pete-clock-pulse{0%,to{box-shadow:inset 0 0 0 2px rgba(204,61,74,.35)}50%{box-shadow:inset 0 0 0 2px rgba(204,61,74,.95)}}.pete-answer{position:relative;display:flex;align-items:center;gap:.75rem;width:100%;border-radius:1rem;padding:.85rem 1rem;text-align:left;font-weight:600;color:#fff;background:rgba(0,0,0,.26);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.1);transition:background .15s ease,transform .1s ease,box-shadow .2s ease}.pete-answer:hover:not(:disabled){background:rgba(0,0,0,.36);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.28)}.pete-answer:active:not(:disabled){transform:translateY(1px)}.pete-answer:disabled{cursor:default}.pete-answer-key{display:grid;place-items:center;height:1.75rem;width:1.75rem;flex:none;border-radius:.6rem;background:hsla(0,0%,100%,.12);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:hsla(0,0%,100%,.7)}.pete-answer[data-state=right]{background:rgba(46,160,103,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.5)}.pete-answer[data-state=wrong]{background:rgba(204,61,74,.9);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.35);animation:pete-answer-no .45s ease}.pete-answer[data-state=missed]{box-shadow:inset 0 0 0 2px rgba(46,160,103,.95)}.pete-answer[data-state=dim]{opacity:.4}@keyframes pete-answer-no{0%,to{transform:translateX(0)}20%{transform:translateX(-6px)}45%{transform:translateX(5px)}70%{transform:translateX(-3px)}}.pete-ladder{display:flex;flex-wrap:wrap;gap:.3rem}.pete-rung{height:.5rem;width:1.1rem;border-radius:999px;background:hsla(0,0%,100%,.14);transition:background .3s ease,transform .3s ease}.pete-rung[data-on="1"]{background:var(--accent);transform:scaleY(1.35)}.pete-solitaire{--card-w:clamp(2.5rem,10.6vw,5.2rem);--card-h:calc(var(--card-w)*1.4);--fan-up:calc(var(--card-h)*0.29);--fan-down:calc(var(--card-h)*0.14)}.pete-slot{position:relative;display:grid;place-items:center;height:var(--card-h);width:var(--card-w);flex:none;border-radius:.55rem;border:2px dashed hsla(0,0%,100%,.25);background:rgba(0,0,0,.12);box-shadow:inset 0 2px 8px rgba(0,0,0,.18);transition:border-color .18s ease,background .18s ease,transform .12s ease}.pete-slot-glyph{font-size:calc(var(--card-w)*.42);line-height:1;color:hsla(0,0%,100%,.3)}.pete-slot-glyph[data-red="1"]{color:hsla(0,100%,83%,.35)}.pete-slot>.pete-card{position:absolute;inset:0;height:100%;width:100%}.pete-stock{border-style:solid;border-color:rgba(0,0,0,.15);background:repeating-linear-gradient(45deg,hsla(0,0%,100%,.1) 0 6px,transparent 6px 12px),linear-gradient(150deg,#b4553f,#8d3f2f);cursor:pointer}.pete-stock:hover{filter:brightness(1.08)}.pete-stock:active{transform:translateY(1px) scale(.98)}.pete-stock[data-empty="1"]{background:rgba(0,0,0,.12);border-style:dashed;border-color:hsla(0,0%,100%,.25)}.pete-stock[data-dead="1"]{cursor:default;opacity:.4}.pete-stock[data-dead="1"]:hover{filter:none}.pete-stock[data-dead="1"]:active{transform:none}.pete-slot-count{position:absolute;bottom:-.5rem;left:50%;transform:translateX(-50%);border-radius:999px;background:rgba(0,0,0,.5);padding:.05rem .5rem;font-size:.7rem;font-weight:700;font-variant-numeric:tabular-nums;color:#fff}.pete-slot-recycle{color:hsla(0,0%,100%,.45)}.pete-slot-recycle svg{height:1.4rem;width:1.4rem}.pete-waste{position:relative;display:flex;height:var(--card-h);min-width:var(--card-w)}.pete-waste .pete-card+.pete-card{margin-left:calc(var(--card-w)*-.72)}.pete-waste .pete-card{position:relative}.pete-tableau{display:grid;grid-template-columns:repeat(7,var(--card-w));justify-content:space-between;gap:.5rem;align-items:start;min-height:calc(var(--card-h)*2.6)}.pete-col{display:flex;flex-direction:column;align-items:center;min-height:var(--card-h)}.pete-col .pete-card{position:relative}.pete-col .pete-card[data-face=down]+.pete-card{margin-top:calc(var(--fan-down) - var(--card-h))}.pete-col .pete-card[data-face=up]+.pete-card{margin-top:calc(var(--fan-up) - var(--card-h))}.pete-card[data-live="1"]{cursor:pointer}.pete-card[data-live="1"]:hover .pete-card-front{filter:brightness(1.06)}.pete-card[data-held="1"]{z-index:5;transform:translateY(-.55rem);transition:transform .14s cubic-bezier(.34,1.56,.64,1)}.pete-card[data-held="1"] .pete-card-front{box-shadow:0 3px 0 rgba(0,0,0,.18),0 10px 22px rgba(0,0,0,.32),0 0 0 3px rgba(242,181,61,.9)}.pete-col[data-drop="1"] .pete-card:last-child .pete-card-front,.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{border-color:rgba(242,181,61,.9);box-shadow:0 0 0 3px rgba(242,181,61,.45)}.pete-col[data-drop="1"] .pete-slot,.pete-slot[data-drop="1"]{background:rgba(242,181,61,.12)}.pete-nope{animation:pete-shake .4s cubic-bezier(.36,.07,.19,.97)}.pete-home-flash{animation:pete-home .5s ease-out}@keyframes pete-home{0%{box-shadow:0 0 0 0 rgba(242,181,61,.9)}to{box-shadow:0 0 0 1.4rem rgba(242,181,61,0)}}.pete-rail{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;justify-content:center;gap:1rem 1.5rem}@media (min-width:1024px){.pete-rail{flex-direction:column;justify-content:flex-start;width:9.5rem;gap:1.5rem}}.pete-rack[data-at=rail]{position:static;justify-content:center}.pete-uno[data-c=red]{--play:#d64545}.pete-uno[data-c=blue]{--play:#3d7fd6}.pete-uno[data-c=yellow]{--play:#e0b02c}.pete-uno[data-c=green]{--play:#46a86b}.pete-uno:before{content:"";position:absolute;inset:0;pointer-events:none;background:radial-gradient(80% 55% at 50% 45%,var(--play,transparent),transparent 70%);opacity:.16;transition:opacity .5s ease,background .5s ease}.pete-uno-card{position:relative;height:var(--uno-h,6.4rem);width:var(--uno-w,4.3rem);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),0 6px 14px rgba(0,0,0,.18);flex:none}.pete-uno-face{position:relative;display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;overflow:hidden;background:var(--uno,#2b2118);color:#fff}.pete-uno-card[data-c=red]{--uno:#d64545}.pete-uno-card[data-c=blue]{--uno:#3d7fd6}.pete-uno-card[data-c=yellow]{--uno:#e0b02c}.pete-uno-card[data-c=green]{--uno:#46a86b}.pete-uno-card[data-c=wild]{--uno:#2b2118}.pete-uno-card[data-named=red]{box-shadow:0 0 0 3px #d64545,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=blue]{box-shadow:0 0 0 3px #3d7fd6,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=yellow]{box-shadow:0 0 0 3px #e0b02c,0 3px 0 rgba(0,0,0,.22)}.pete-uno-card[data-named=green]{box-shadow:0 0 0 3px #46a86b,0 3px 0 rgba(0,0,0,.22)}.pete-uno-oval{position:relative;z-index:1;display:grid;place-items:center;height:78%;width:62%;border-radius:50%;transform:rotate(-24deg);background:#fff;color:var(--uno,#2b2118);font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.45rem;font-weight:700;line-height:1;text-shadow:0 1px 0 rgba(0,0,0,.1)}.pete-uno-card[data-c=wild] .pete-uno-oval{color:#2b2118;font-size:1.15rem}.pete-uno-corner{position:absolute;z-index:1;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.62rem;font-weight:700;line-height:1;color:#fff;text-shadow:0 1px 1px rgba(0,0,0,.35)}.pete-uno-corner[data-at=tl]{top:.22rem;left:.28rem}.pete-uno-corner[data-at=br]{bottom:.22rem;right:.28rem;transform:rotate(180deg)}.pete-uno-wheel{position:absolute;inset:0;background:conic-gradient(#d64545 0 25%,#e0b02c 0 50%,#46a86b 0 75%,#3d7fd6 0);opacity:.92}.pete-uno-card-back{padding:.28rem}.pete-uno-back{display:grid;place-items:center;height:100%;width:100%;border-radius:.38rem;background:radial-gradient(120% 80% at 50% 0,hsla(0,0%,100%,.16),transparent 60%),#2b2118}.pete-uno-back:after{content:"UNO";display:block;transform:rotate(-24deg);padding:.1rem .35rem;border-radius:999px;background:#e0b02c;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.6rem;font-weight:700;letter-spacing:.02em}.pete-uno-hand{display:flex;flex-wrap:wrap;justify-content:center;gap:.4rem;min-height:6.8rem;padding-top:.6rem}.pete-uno-hand .pete-uno-card{animation:pete-uno-in .34s cubic-bezier(.22,1,.36,1) backwards;animation-delay:calc(var(--i, 0)*45ms);transition:transform .16s ease,filter .16s ease}.pete-uno-hand .pete-uno-card[data-settled="1"]{animation:none}.pete-uno-hand .pete-uno-card[data-on="1"]{cursor:pointer;transform:translateY(-.5rem)}.pete-uno-hand .pete-uno-card[data-on="1"]:hover{transform:translateY(-1.1rem) scale(1.03)}.pete-uno-hand .pete-uno-card[data-on="0"]{filter:brightness(.62) saturate(.7)}@keyframes pete-uno-in{0%{transform:translateY(2rem) rotate(6deg);opacity:0}to{transform:translateY(-.5rem);opacity:1}}.pete-uno-seat{display:flex;flex-direction:column;align-items:center;gap:.3rem;position:relative;padding:.5rem .7rem;border-radius:1rem;transition:background .25s ease,transform .25s ease}.pete-uno-seat[data-turn="1"]{background:hsla(0,0%,100%,.12);transform:translateY(-2px)}.pete-uno-seat[data-uno="1"] .pete-uno-count{color:#ffd76e}.pete-uno-fan{display:flex;--uno-h:3.2rem;--uno-w:2.15rem}.pete-uno-fan .pete-uno-card{margin-left:-1.35rem;transform:rotate(calc((var(--i, 0) - (var(--n, 1) - 1)/2)*4deg));transform-origin:bottom center;box-shadow:0 2px 0 rgba(0,0,0,.22)}.pete-uno-fan .pete-uno-card:first-child{margin-left:0}.pete-uno-fan .pete-uno-back:after{font-size:.4rem;padding:.06rem .22rem}.pete-uno-name{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.9rem;font-weight:700;color:#fff;line-height:1}.pete-uno-count{font-size:.7rem;font-weight:700;color:hsla(0,0%,100%,.55);line-height:1}.pete-uno-deck{position:relative;--uno-h:6.4rem;--uno-w:4.3rem;height:var(--uno-h);width:var(--uno-w);border-radius:.55rem;padding:.28rem;background:#fff;box-shadow:0 3px 0 rgba(0,0,0,.22),.28rem .28rem 0 -.06rem hsla(0,0%,100%,.35),.5rem .5rem 0 -.12rem hsla(0,0%,100%,.18);transition:transform .15s ease}.pete-uno-deck:not(:disabled):hover{transform:translateY(-3px)}.pete-uno-deck:disabled{opacity:.75}.pete-uno-deck-count{position:absolute;bottom:-.55rem;left:50%;transform:translateX(-50%);padding:.1rem .5rem;border-radius:999px;background:rgba(0,0,0,.55);color:#fff;font-size:.65rem;font-weight:700;line-height:1.4}.pete-uno-shuffle{animation:pete-uno-shuffle .42s ease}@keyframes pete-uno-shuffle{0%,to{transform:none}30%{transform:translateY(-4px) rotate(-3deg)}65%{transform:translateY(2px) rotate(2deg)}}.pete-uno-discard{display:grid;place-items:center;height:6.4rem;width:4.3rem;border-radius:.55rem;box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.14)}.pete-uno-land{animation:pete-uno-land .3s cubic-bezier(.34,1.56,.64,1)}@keyframes pete-uno-land{0%{transform:scale(1.14) rotate(-4deg)}to{transform:none}}.pete-uno-colour{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.15rem .6rem;border-radius:999px;color:#fff;background:rgba(0,0,0,.3)}.pete-uno-colour[data-c=red]{background:#d64545}.pete-uno-colour[data-c=blue]{background:#3d7fd6}.pete-uno-colour[data-c=yellow]{background:#e0b02c;color:#2b2118}.pete-uno-colour[data-c=green]{background:#46a86b}.pete-uno-badge{position:absolute;top:-.4rem;left:50%;z-index:5;padding:.2rem .6rem;border-radius:999px;background:#fff;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.75rem;font-weight:700;white-space:nowrap;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-badge .9s ease forwards}.pete-uno-badge[data-tone=bad]{background:#cc3d4a;color:#fff}.pete-uno-badge[data-tone=uno]{background:#e0b02c}@keyframes pete-uno-badge{0%{transform:translate(-50%,.4rem) scale(.7);opacity:0}25%{transform:translate(-50%,-.5rem) scale(1.06);opacity:1}75%{transform:translate(-50%,-.9rem) scale(1);opacity:1}to{transform:translate(-50%,-1.6rem) scale(.95);opacity:0}}.pete-uno-wild{position:absolute;inset:0;z-index:10;display:grid;place-items:center;background:rgba(10,20,15,.55);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.pete-uno-wild-box{padding:1.25rem 1.5rem;border-radius:1.25rem;background:rgba(20,40,30,.92);box-shadow:0 12px 30px rgba(0,0,0,.35);text-align:center}.pete-uno-swatch{padding:.7rem 1.4rem;border-radius:.75rem;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1rem;font-weight:700;color:#fff;background:var(--sw,#666);box-shadow:0 3px 0 rgba(0,0,0,.3);transition:transform .12s ease,filter .12s ease}.pete-uno-swatch:hover{transform:translateY(-2px);filter:brightness(1.08)}.pete-uno-swatch:active{transform:translateY(1px)}.pete-uno-swatch[data-c=red]{--sw:#d64545}.pete-uno-swatch[data-c=blue]{--sw:#3d7fd6}.pete-uno-swatch[data-c=yellow]{--sw:#e0b02c;color:#2b2118}.pete-uno-swatch[data-c=green]{--sw:#46a86b}.pete-uno-call{margin-top:.85rem;padding:.9rem 2.6rem;border-radius:999px;background:#e0b02c;color:#2b2118;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:1.7rem;font-weight:700;letter-spacing:.02em;box-shadow:0 5px 0 rgba(0,0,0,.3),0 0 0 0 rgba(224,176,44,.55);animation:pete-uno-call .9s ease-in-out infinite}.pete-uno-call:hover{filter:brightness(1.07)}.pete-uno-call:active{transform:translateY(2px);box-shadow:0 3px 0 rgba(0,0,0,.3)}@keyframes pete-uno-call{0%,to{transform:scale(1);box-shadow:0 5px 0 rgba(0,0,0,.3),0 0 0 0 rgba(224,176,44,.5)}50%{transform:scale(1.05);box-shadow:0 5px 0 rgba(0,0,0,.3),0 0 0 .9rem rgba(224,176,44,0)}}.pete-uno-timer{margin-top:.8rem;height:.35rem;border-radius:999px;background:hsla(0,0%,100%,.16);overflow:hidden}.pete-uno-timer>span{display:block;height:100%;width:100%;background:#e0b02c;transform-origin:left center;animation:pete-uno-drain linear forwards}@keyframes pete-uno-drain{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.pete-uno-catch{position:absolute;top:-.55rem;left:50%;transform:translateX(-50%);z-index:6;padding:.22rem .7rem;border-radius:999px;background:#cc3d4a;color:#fff;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.72rem;font-weight:700;white-space:nowrap;box-shadow:0 3px 0 rgba(0,0,0,.25);animation:pete-uno-catch 1.1s ease-in-out infinite;transition:filter .12s ease}.pete-uno-catch:hover{filter:brightness(1.12)}.pete-uno-catch:disabled{opacity:.45;animation:none}@keyframes pete-uno-catch{0%,to{transform:translateX(-50%) scale(1)}50%{transform:translateX(-50%) scale(1.08)}}@media (prefers-reduced-motion:reduce){.pete-uno-call,.pete-uno-catch{animation:none}}.pete-uno-card .pete-uno-oval[data-size=mid]{font-size:1.05rem}.pete-uno-card .pete-uno-oval[data-size=words]{font-size:.5rem;line-height:1.1;letter-spacing:.02em;text-align:center;padding:0 .15rem}.pete-uno-card[data-glow="1"]{position:relative;z-index:0}.pete-uno-card[data-glow="1"]:before{content:"";position:absolute;inset:-.35rem;z-index:-1;border-radius:.9rem;pointer-events:none;background:conic-gradient(from 0deg,#d64545,#e0b02c,#46a86b,#3d7fd6,#d64545);filter:blur(7px);opacity:.75;animation:pete-uno-glow 3.2s linear infinite}.pete-uno-card[data-glow="1"] .pete-uno-face:after{content:"";position:absolute;inset:0;z-index:2;pointer-events:none;background:linear-gradient(115deg,transparent 35%,hsla(0,0%,100%,.55) 50%,transparent 65%);background-size:260% 100%;animation:pete-uno-shine 2.6s ease-in-out infinite}@keyframes pete-uno-glow{to{transform:rotate(1turn)}}@keyframes pete-uno-shine{0%{background-position:140% 0}55%{background-position:-40% 0}to{background-position:-40% 0}}.pete-uno-pending{font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:.18rem .7rem;border-radius:999px;color:#fff;background:#cc3d4a;box-shadow:0 3px 0 rgba(0,0,0,.2);animation:pete-uno-bill 1.1s ease-in-out infinite}@keyframes pete-uno-bill{0%,to{transform:scale(1)}50%{transform:scale(1.06)}}.pete-uno-seat[data-out="1"]{opacity:.4;filter:grayscale(1)}.pete-uno-seat[data-out="1"] .pete-uno-count{color:#ffb3ba}.pete-seg{display:inline-flex;padding:.2rem;border-radius:999px;background:color-mix(in srgb,var(--ink) 6%,transparent)}.pete-seg-btn{padding:.3rem .9rem;border-radius:999px;font-family:Fredoka,ui-sans-serif,system-ui,sans-serif;font-size:.8rem;font-weight:700;color:color-mix(in srgb,var(--ink) 55%,transparent);transition:background .15s ease,color .15s ease}.pete-seg-btn:hover{color:var(--ink)}.pete-seg-btn[data-on="1"]{background:var(--accent);color:#fff;box-shadow:0 2px 0 rgba(0,0,0,.15)}@media (max-width:639px){.pete-uno-hand{gap:.3rem}.pete-uno-deck,.pete-uno-hand{--uno-h:5.2rem;--uno-w:3.5rem}.pete-uno-card .pete-uno-oval[data-size=words]{font-size:.42rem}.pete-uno-card .pete-uno-oval[data-size=mid]{font-size:.9rem}.pete-uno-discard{--uno-h:5.2rem;--uno-w:3.5rem;height:5.2rem;width:3.5rem}.pete-uno-hand .pete-uno-card{padding:.22rem}.pete-uno-oval{font-size:1.15rem}.pete-uno-seat{padding:.35rem .45rem}}@media (prefers-reduced-motion:reduce){.pete-card,.pete-card:after,.pete-dealer-think,.pete-stack .pete-disc{animation:none}.pete-card-inner{transition:none}.pete-hand[data-won="1"] .pete-card,.pete-home-flash,.pete-meter[data-hit="1"],.pete-nope,.pete-part-draw,.pete-shake,.pete-tile-hit{animation:none}.pete-card[data-held="1"]{transition:none}.pete-answer[data-state=wrong],.pete-clock[data-hot="1"]{animation:none}.pete-rung{transition:none}.pete-uno-card[data-glow="1"]:before{animation:none}.pete-uno-card[data-glow="1"] .pete-uno-face:after{display:none}.pete-uno-pending{animation:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.-right-6{right:-1.5rem}.-top-6{top:-1.5rem}.-z-10{z-index:-10}.-z-\[5\]{z-index:-5}.z-50{z-index:50}.order-none{order:0}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.-mt-1{margin-top:-.25rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-7{margin-top:1.75rem}.mt-8{margin-top:2rem}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-2,.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-3{-webkit-line-clamp:3}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-\[16\/10\]{aspect-ratio:16/10}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.max-h-\[55vh\]{max-height:55vh}.max-h-\[60vh\]{max-height:60vh}.min-h-\[1\.75rem\]{min-height:1.75rem}.min-h-\[16rem\]{min-height:16rem}.min-h-\[2\.75rem\]{min-height:2.75rem}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-9{width:2.25rem}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[28rem\]{min-width:28rem}.min-w-\[52rem\]{min-width:52rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[7rem\]{max-width:7rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.-translate-y-0{--tw-translate-y:-0px}.-translate-y-0,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-10{-moz-column-gap:2.5rem;column-gap:2.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-3{row-gap:.75rem}.gap-y-4{row-gap:1rem}.gap-y-8{row-gap:2rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.justify-self-center{justify-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-dashed{border-style:dashed}.border-transparent{border-color:transparent}.bg-\[\#cc3d4a\]{--tw-bg-opacity:1;background-color:rgb(204 61 74/var(--tw-bg-opacity,1))}.bg-\[color\:var\(--accent\)\]{background-color:var(--accent)}.bg-\[color\:var\(--bg\)\]{background-color:var(--bg)}.bg-\[color\:var\(--bg-grad\)\]{background-color:var(--bg-grad)}.bg-\[color\:var\(--card\)\]{background-color:var(--card)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/25{background-color:rgba(0,0,0,.25)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/15{background-color:rgba(16,185,129,.15)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/15{background-color:rgba(239,68,68,.15)}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/90{background-color:hsla(0,0%,100%,.9)}.bg-white\/95{background-color:hsla(0,0%,100%,.95)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-10{padding-bottom:2.5rem}.pb-2{padding-bottom:.5rem}.pb-20{padding-bottom:5rem}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pr-24{padding-right:6rem}.pr-32{padding-right:8rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.pt-\[10vh\]{padding-top:10vh}.pt-\[12vh\]{padding-top:12vh}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.align-\[-4px\]{vertical-align:-4px}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[0\.65rem\]{font-size:.65rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12rem\]{font-size:12rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#1c1305\]{--tw-text-opacity:1;color:rgb(28 19 5/var(--tw-text-opacity,1))}.text-\[\#20180c\]{--tw-text-opacity:1;color:rgb(32 24 12/var(--tw-text-opacity,1))}.text-\[\#2b2118\]{--tw-text-opacity:1;color:rgb(43 33 24/var(--tw-text-opacity,1))}.text-\[color\:var\(--accent\)\]{color:var(--accent)}.text-\[color\:var\(--ink\)\]{color:var(--ink)}.text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/40{color:hsla(0,0%,100%,.4)}.text-white\/50{color:hsla(0,0%,100%,.5)}.text-white\/55{color:hsla(0,0%,100%,.55)}.text-white\/60{color:hsla(0,0%,100%,.6)}.text-white\/70{color:hsla(0,0%,100%,.7)}.text-white\/85{color:hsla(0,0%,100%,.85)}.text-white\/90{color:hsla(0,0%,100%,.9)}.underline{text-decoration-line:underline}.decoration-4{text-decoration-thickness:4px}.underline-offset-2{text-underline-offset:2px}.underline-offset-4{text-underline-offset:4px}.accent-\[color\:var\(--accent\)\]{accent-color:var(--accent)}.opacity-20{opacity:.2}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale,.sepia{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sepia{--tw-sepia:sepia(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-1000{transition-duration:1s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.font-display{font-family:Fredoka,Nunito,system-ui,sans-serif;letter-spacing:-.01em}.shadow-pete{box-shadow:0 4px 0 rgba(60,40,20,.1),0 8px 24px rgba(60,40,20,.08)}.shadow-pete-lg{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.bg-theme-gaming{background-color:#4caf7d}.bg-theme-tech{background-color:#5aa9e6}.bg-theme-politics{background-color:#e07a5f}.bg-theme-eu{background-color:#039}.bg-theme-music{background-color:#b079d6}.bg-theme-anime{background-color:#ec5e8a}.bg-theme-foss{background-color:#d97706}.bg-theme-kids{background-color:#14b8a6}.bg-theme-finance{background-color:#10b981}.bg-theme-lego{background-color:#d01012}.bg-theme-adventure{background-color:#6d4bd8}.text-theme-gaming{color:#2d8a5a}.text-theme-tech{color:#2f7fb8}.text-theme-politics{color:#b8523a}.text-theme-eu{color:#039}.text-theme-music{color:#8a4fb8}.text-theme-anime{color:#c33a6a}.text-theme-foss{color:#b8530a}.text-theme-kids{color:#0f766e}.text-theme-finance{color:#059669}.text-theme-lego{color:#b00d0e}.text-theme-adventure{color:#5836b8}.decoration-theme-gaming{text-decoration-color:#4caf7d}.decoration-theme-tech{text-decoration-color:#5aa9e6}.decoration-theme-politics{text-decoration-color:#e07a5f}.decoration-theme-eu{text-decoration-color:#039}.decoration-theme-music{text-decoration-color:#b079d6}.decoration-theme-anime{text-decoration-color:#ec5e8a}.decoration-theme-foss{text-decoration-color:#d97706}.decoration-theme-kids{text-decoration-color:#14b8a6}.decoration-theme-finance{text-decoration-color:#10b981}.decoration-theme-lego{text-decoration-color:#d01012}.decoration-theme-adventure{text-decoration-color:#6d4bd8}.border-theme-gaming{border-color:#4caf7d}.border-theme-tech{border-color:#5aa9e6}.border-theme-politics{border-color:#e07a5f}.border-theme-eu{border-color:#039}.border-theme-music{border-color:#b079d6}.border-theme-anime{border-color:#ec5e8a}.border-theme-foss{border-color:#d97706}.border-theme-kids{border-color:#14b8a6}.border-theme-finance{border-color:#10b981}.border-theme-lego{border-color:#d01012}.border-theme-adventure{border-color:#6d4bd8}.glow-theme-gaming{box-shadow:0 0 0 2px rgba(76,175,125,.35),0 0 24px 4px rgba(76,175,125,.45)}.glow-theme-gaming,.glow-theme-tech{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-tech{box-shadow:0 0 0 2px rgba(90,169,230,.35),0 0 24px 4px rgba(90,169,230,.45)}.glow-theme-politics{box-shadow:0 0 0 2px rgba(224,122,95,.35),0 0 24px 4px rgba(224,122,95,.45)}.glow-theme-eu,.glow-theme-politics{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-eu{box-shadow:0 0 0 2px rgba(0,51,153,.35),0 0 24px 4px rgba(0,51,153,.45)}.glow-theme-music{box-shadow:0 0 0 2px rgba(176,121,214,.35),0 0 24px 4px rgba(176,121,214,.45)}.glow-theme-anime,.glow-theme-music{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-anime{box-shadow:0 0 0 2px rgba(236,94,138,.35),0 0 24px 4px rgba(236,94,138,.45)}.glow-theme-foss{box-shadow:0 0 0 2px rgba(217,119,6,.35),0 0 24px 4px rgba(217,119,6,.45)}.glow-theme-foss,.glow-theme-kids{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-kids{box-shadow:0 0 0 2px rgba(20,184,166,.35),0 0 24px 4px rgba(20,184,166,.45)}.glow-theme-finance{box-shadow:0 0 0 2px rgba(16,185,129,.35),0 0 24px 4px rgba(16,185,129,.45)}.glow-theme-finance,.glow-theme-lego{animation:pete-glow-pulse 2.4s ease-in-out infinite}.glow-theme-lego{box-shadow:0 0 0 2px rgba(208,16,18,.35),0 0 24px 4px rgba(208,16,18,.45)}.glow-theme-adventure{box-shadow:0 0 0 2px rgba(109,75,216,.35),0 0 24px 4px rgba(109,75,216,.45);animation:pete-glow-pulse 2.4s ease-in-out infinite}@keyframes pete-glow-pulse{0%,to{filter:brightness(1)}50%{filter:brightness(1.08)}}.line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.paywall-stamp{position:absolute;inset:0;pointer-events:none;display:flex;align-items:center;justify-content:center;z-index:5;background:rgba(255,250,240,.18)}.paywall-stamp:before{content:"PAYWALLED";font-family:var(--font-display,ui-sans-serif),system-ui,sans-serif;font-weight:900;font-size:clamp(1rem,3.2vw,1.75rem);letter-spacing:.12em;color:rgba(180,30,30,.85);border:.28rem double rgba(180,30,30,.85);padding:.3rem .9rem;transform:rotate(-15deg);text-shadow:0 1px 0 hsla(0,0%,100%,.4);box-shadow:inset 0 0 0 2px hsla(0,0%,100%,.15);background:rgba(255,240,230,.55);border-radius:.25rem;filter:contrast(1.05)}:root,html[data-phase=day]{--bg:#fff7e4;--bg-grad:#ffeec2;--card:#fff;--ink:#3a2e1f;--accent:#f2a541}html[data-phase=dawn]{--bg:#ffe7d6;--bg-grad:#ffc9c9;--card:#fff4ea;--ink:#4a2e2a;--accent:#ff8a65}html[data-phase=dusk]{--bg:#ffd6a8;--bg-grad:#f7a07e;--card:#fff1de;--ink:#3d2417;--accent:#e6553a}html[data-phase=night]{--bg:#1a1f3a;--bg-grad:#2a3358;--card:#2d365a;--ink:#f1ecd8;--accent:#f9d976}html[data-room=casinopolis]{--bg:#16211c;--card:#23342b;--ink:#f6ecd8;--accent:#f2b53d;--felt-a:#2f7d5b;--felt-b:#24614a;--felt-c:#1c4d3c;--glow:242,181,61}html[data-room=casino-night]{--bg:#140f2e;--card:#241a4d;--ink:#f2ecff;--accent:#ffcc2f;--felt-a:#4a2fa8;--felt-b:#351f80;--felt-c:#241659;--glow:126,86,255}html[data-room] .cs-room{background:radial-gradient(80% 55% at 50% -8%,rgba(var(--glow),.18),transparent 62%),radial-gradient(120% 90% at 50% 120%,rgba(0,0,0,.45),transparent 55%)}html[data-room=casino-night] .cs-room:before{content:"";position:absolute;inset:0 -50% auto -50%;height:.5rem;background:repeating-linear-gradient(90deg,rgba(255,204,47,.55) 0 .5rem,transparent .5rem 2.25rem);filter:blur(1px);animation:cs-bulbs 2.4s linear infinite}@keyframes cs-bulbs{0%{transform:translateX(0)}to{transform:translateX(2.75rem)}}html[data-room] .shadow-pete{box-shadow:0 4px 0 rgba(0,0,0,.3),0 10px 26px rgba(0,0,0,.3)}html[data-room] .shadow-pete-lg{box-shadow:0 6px 0 rgba(0,0,0,.34),0 20px 40px rgba(0,0,0,.38)}html[data-room] .cs-comb svg{filter:drop-shadow(0 3px 0 rgba(0,0,0,.35))}html[data-room] .pete-felt{background:radial-gradient(120% 90% at 50% -10%,hsla(0,0%,100%,.1),transparent 60%),linear-gradient(160deg,var(--felt-a) 0,var(--felt-b) 55%,var(--felt-c) 100%)}@media (prefers-reduced-motion:reduce){html[data-room=casino-night] .cs-room:before{animation:none}}.cs-stack{display:block;width:2.75rem;height:calc(.45rem*var(--stack, 1) + .55rem);border-radius:999px;background:radial-gradient(circle at 50% 30%,hsla(0,0%,100%,.3),transparent 60%),var(--chip,#e07a5f);border:2px solid rgba(0,0,0,.25);box-shadow:0 3px 0 rgba(0,0,0,.3),inset 0 -.4rem 0 rgba(0,0,0,.16),inset 0 .35rem 0 hsla(0,0%,100%,.14)}.cs-stack[data-chip="5"]{--chip:#5aa9e6}.cs-stack[data-chip="25"]{--chip:#4caf7d}.cs-stack[data-chip="100"]{--chip:#2b2118}.cs-stack[data-chip="500"]{--chip:#b079d6}.pete-poker{--seat-w:7.5rem}.pete-poker-seats{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:center;gap:.75rem}.pete-seat{position:relative;display:flex;flex-direction:column;align-items:center;gap:.4rem;width:var(--seat-w);transition:opacity .3s ease,filter .3s ease}.pete-seat[data-state=folded]{opacity:.38;filter:grayscale(.7)}.pete-seat[data-state=out]{opacity:.2}.pete-seat[data-turn="1"] .pete-seat-plate{box-shadow:0 0 0 2px var(--accent),0 0 1.6rem -.2rem var(--accent);animation:pete-seat-wait 1.6s ease-in-out infinite}@keyframes pete-seat-wait{0%,to{transform:translateY(0)}50%{transform:translateY(-2px)}}.pete-seat-cards{display:flex;justify-content:center;gap:.15rem;min-height:4.4rem;--card-h:4.4rem;--card-w:3.15rem}.pete-seat-cards[data-mucked="1"]{opacity:0;transform:translateY(-.6rem) scale(.9);transition:all .35s ease}.pete-seat-plate{position:relative;display:flex;flex-direction:column;align-items:center;width:100%;padding:.3rem .4rem;border-radius:.85rem;background:rgba(0,0,0,.32);border:1px solid hsla(0,0%,100%,.12);transition:box-shadow .25s ease}.pete-seat-name{font-family:var(--font-display,inherit);font-weight:700;font-size:.8rem;line-height:1.1;color:hsla(0,0%,100%,.92);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pete-seat-stack{font-weight:700;font-size:.78rem;font-variant-numeric:tabular-nums;color:hsla(0,0%,100%,.58)}.pete-seat[data-state=allin] .pete-seat-stack:after{content:" all in";color:var(--accent)}.pete-seat-pos{position:absolute;top:-.5rem;right:-.5rem;display:grid;place-items:center;min-width:1.45rem;height:1.45rem;padding:0 .3rem;border-radius:999px;font-size:.58rem;font-weight:800;letter-spacing:.02em;background:hsla(0,0%,100%,.9);color:#2b2118;box-shadow:0 2px 0 rgba(0,0,0,.3)}.pete-seat-pos[data-pos=BTN]{background:#f5d76e}.pete-seat-spot{height:3.6rem;width:3.6rem;margin-bottom:.5rem}.pete-seat-spot .pete-disc{height:1.6rem;width:1.6rem;margin:-.8rem 0 0 -.8rem}.pete-seat-spot .pete-spot-total{font-size:.66rem;padding:.05rem .45rem}.pete-poker-middle{display:flex;flex-direction:column;align-items:center;gap:.35rem;padding:.5rem 0}.pete-poker-pot{margin-top:.9rem}.pete-poker-board{display:flex;gap:.35rem;min-height:6rem;--card-h:6rem;--card-w:4.3rem}.pete-poker-pot{display:flex;flex-direction:column;align-items:center;gap:.1rem}.pete-poker-pot-pile{position:relative;width:7rem;height:3rem}.pete-poker-pot-label{font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:hsla(0,0%,100%,.42)}.pete-poker-pot-total{font-family:var(--font-display,inherit);font-size:1.5rem;font-weight:800;font-variant-numeric:tabular-nums;color:#fff;text-shadow:0 2px 0 rgba(0,0,0,.28)}.pete-poker-side{font-size:.68rem;font-weight:700;color:hsla(0,0%,100%,.5)}.pete-poker-you .pete-seat{width:auto}.pete-poker-you .pete-seat-cards{--card-h:6.8rem;--card-w:4.85rem;gap:.3rem;min-height:6.8rem}.pete-poker-you .pete-seat-plate{padding:.4rem 1.1rem}.pete-poker-you .pete-seat-name,.pete-poker-you .pete-seat-stack{font-size:.95rem}.pete-poker-verdict{border-radius:999px;background:hsla(0,0%,100%,.95);padding:.5rem 1.25rem;font-family:var(--font-display,inherit);font-size:1.05rem;font-weight:700;color:#2b2118;box-shadow:0 4px 0 rgba(0,0,0,.18)}.pete-poker-verdict[data-tone=win]{background:#4caf7d;color:#fff}.pete-poker-verdict[data-tone=lose]{background:hsla(0,0%,100%,.9);color:#7a5c50}.pete-raise{display:flex;align-items:center;gap:.6rem;flex:1 1 14rem;min-width:0}.pete-raise input[type=range]{flex:1;min-width:0;accent-color:var(--accent)}.pete-raise-to{font-family:var(--font-display,inherit);font-size:1.15rem;font-weight:800;font-variant-numeric:tabular-nums;min-width:3.5rem;text-align:right}.pete-poker-log{max-height:7rem;overflow-y:auto;font-size:.75rem;line-height:1.5;color:hsla(0,0%,100%,.55)}.pete-poker-log b{color:hsla(0,0%,100%,.85);font-weight:700}@media (max-width:640px){.pete-poker{--seat-w:5.6rem}.pete-seat-cards{--card-h:3.5rem;--card-w:2.5rem;min-height:3.5rem}.pete-poker-board{--card-h:5rem;--card-w:3.55rem;min-height:5rem;gap:.25rem}.pete-poker-you .pete-seat-cards{--card-h:6rem;--card-w:4.3rem;min-height:6rem}.pete-poker-pot-total{font-size:1.25rem}}.last\:border-0:last-child{border-width:0}.empty\:hidden:empty{display:none}.hover\:-translate-y-0\.5:hover{--tw-translate-y:-0.125rem}.hover\:-translate-y-0\.5:hover,.hover\:-translate-y-1:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-1:hover{--tw-translate-y:-0.25rem}.hover\:text-\[color\:var\(--ink\)\]:hover{color:var(--ink)}.hover\:text-white\/80:hover{color:hsla(0,0%,100%,.8)}.hover\:brightness-105:hover{--tw-brightness:brightness(1.05);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:shadow-pete-lg:hover{box-shadow:0 6px 0 rgba(60,40,20,.12),0 16px 32px rgba(60,40,20,.12)}.focus\:border-\[color\:var\(--accent\)\]:focus{border-color:var(--accent)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:translate-y-px:active{--tw-translate-y:1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:-rotate-6{--tw-rotate:-6deg}.group:hover .group-hover\:-rotate-6,.group:hover .group-hover\:scale-105{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:underline{text-decoration-line:underline}@media (min-width:640px){.sm\:ml-auto{margin-left:auto}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:inline-flex{display:inline-flex}.sm\:h-56{height:14rem}.sm\:basis-auto{flex-basis:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-2{gap:.5rem}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:gap-5{gap:1.25rem}.sm\:gap-8{gap:2rem}.sm\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.sm\:p-10{padding:2.5rem}.sm\:p-6{padding:1.5rem}.sm\:p-8{padding:2rem}.sm\:pr-28{padding-right:7rem}.sm\:pt-10{padding-top:2.5rem}.sm\:pt-12{padding-top:3rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-6xl{font-size:3.75rem;line-height:1}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[auto\2c 1fr\]{grid-template-columns:auto 1fr}.lg\:grid-cols-\[minmax\(0\2c 1fr\)\2c auto\]{grid-template-columns:minmax(0,1fr) auto}.lg\:items-start{align-items:flex-start}.lg\:gap-8{gap:2rem}.lg\:p-8{padding:2rem}}@media (prefers-color-scheme:dark){.dark\:text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.dark\:text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.dark\:text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.dark\:text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}} \ No newline at end of file diff --git a/internal/web/static/js/blackjack.js b/internal/web/static/js/blackjack.js index 0fc5645..9c5e5bf 100644 --- a/internal/web/static/js/blackjack.js +++ b/internal/web/static/js/blackjack.js @@ -1,8 +1,8 @@ // The blackjack table. // -// The browser holds no game. It sends intents — deal, hit, stand, double — and -// the server answers with the cards you're allowed to see plus the *script* of -// how they got there: one event per card off the shoe, in the order the shoe +// The browser holds no game. It sends intents — deal, hit, stand, double, split +// — and the server answers with the cards you're allowed to see plus the *script* +// of how they got there: one event per card off the shoe, in the order the shoe // gave them up. This file's job is to play that script back at a human speed // rather than snapping the finished hand into place. // @@ -16,6 +16,10 @@ // taken away. Nothing about the money changes on this table without something // crossing the felt to make it change — including the count in the chip bar, // which is deliberately not updated until the chips that justify it have landed. +// +// Since split, "the spot" is not one place. A hand has a spot, and a split makes +// a second hand with a second bet on a second spot — two bets that win and lose +// separately, which is the whole point of splitting and has to look like it. (function () { "use strict"; @@ -25,9 +29,7 @@ var FX = window.PeteFX; var dealerEl = root.querySelector("[data-dealer]"); - var playerEl = root.querySelector("[data-player]"); var dTotalEl = root.querySelector("[data-dealer-total]"); - var pTotalEl = root.querySelector("[data-player-total]"); var dLabelEl = root.querySelector("[data-dealer-label]"); var verdictEl = root.querySelector("[data-verdict]"); var betting = root.querySelector("[data-betting]"); @@ -36,26 +38,22 @@ var dealBtn = root.querySelector("[data-deal]"); var msgEl = root.querySelector("[data-table-msg]"); - // The three places a chip can be: your pile (in the bar above), the spot in - // front of you, and the house's rack on the felt. + // Where the chips live: your pile (in the bar above), a spot in front of each + // hand, and the house's rack on the felt. var purseEl = document.querySelector("[data-chips]"); - var spotEl = root.querySelector("[data-spot]"); - var stackEl = root.querySelector("[data-stack]"); - var spotTotalEl = root.querySelector("[data-spot-total]"); var houseEl = root.querySelector("[data-house]"); - - // The spot owns the chips on the felt and the number under them — see PeteFX. - // Nothing is bet until a chip is on it, so `bet` starts at nothing rather than - // at a default stake nobody put down. - var spot = FX.spot({ spot: spotEl, stack: stackEl, total: spotTotalEl }); + var handsEl = root.querySelector("[data-hands]"); + var handTpl = root.querySelector("[data-hand-template]"); var bet = 0; // what you're building between hands + var base = 0; // what one hand of the last deal cost, for standing it back up var busy = false; // a request is in flight, or cards are still landing - var hand = null; // the hand as the server last described it + var hand = null; // the deal as the server last described it var DEAL_MS = 380; // one card's flight, and the gap before the next var FLIP_MS = 450; var BEAT_MS = 600; // the dealer thinking before they draw out + var SPLIT_MS = 340; // the card lifting out of the hand it's leaving var reduced = FX.reduced; function pace(ms) { return reduced ? 0 : ms; } @@ -80,46 +78,137 @@ function cardEl(face) { return CARDS.el(face); } var turnOver = CARDS.turnOver; + // ---- the hands ------------------------------------------------------------- + // + // A box per hand: its chips, its cards, its total, its own verdict. There is + // always at least one, and before a deal that one is where you build your bet — + // so the chips you stack are already sitting on the hand they're about to buy. + + var hands = []; + + function makeHand(at) { + var el = handTpl.content.firstElementChild.cloneNode(true); + var spotEl = el.querySelector("[data-spot]"); + var box = { + el: el, + spotEl: spotEl, // where a chip flies to, as opposed to what counts it + cards: el.querySelector("[data-cards]"), + total: el.querySelector("[data-total]"), + verdict: el.querySelector("[data-hand-outcome]"), + ranks: [], // what's in it, for a running total while the cards are landing + spot: FX.spot({ + spot: spotEl, + stack: el.querySelector("[data-stack]"), + total: el.querySelector("[data-spot-total]"), + }), + }; + if (at === undefined || at >= hands.length) { + handsEl.appendChild(el); + hands.push(box); + } else { + handsEl.insertBefore(el, hands[at].el); + hands.splice(at, 0, box); + } + handsEl.dataset.count = hands.length; + return box; + } + + // reset tears the row down to n empty hands. It does not preserve chips: a + // caller that has some to keep (a deal, whose stake is already on the spot) + // reads the amount first and puts it back. + function reset(n) { + handsEl.innerHTML = ""; + hands = []; + for (var i = 0; i < (n || 1); i++) makeHand(); + } + + function ensure(n) { while (hands.length < n) makeHand(); } + + // The bet you build between hands is hand zero's. + function betSpot() { return hands[0].spot; } + + // live marks the hand you're being asked about, and dims the ones you aren't. + // + // -1 means nobody is acting — the dealer is drawing, or the deal is over — and + // that is *not* the same as every hand being inactive: a settled hand is the + // thing you are reading, so it goes back to full brightness rather than sitting + // there greyed out. Hence the empty string: it matches neither CSS rule. + function live(i) { + hands.forEach(function (h, k) { + h.el.dataset.live = i < 0 ? "" : k === i ? "1" : "0"; + }); + } + + // The running total under a hand, worked out from the cards you can see. The + // server sends totals too, but only with the finished state — and a hand you're + // playing needs to know what it's holding *now*, not once it's over. It's a + // readout, never a decision: every ruling is the server's. + var TEN = { K: 10, Q: 10, J: 10 }; + function totalOf(ranks) { + var total = 0, aces = 0; + ranks.forEach(function (r) { + if (r === "A") { aces++; total += 11; } + else total += TEN[r] || parseInt(r, 10) || 0; + }); + while (total > 21 && aces > 0) { total -= 10; aces--; } + return { total: total, soft: aces > 0 }; + } + + function showTotal(box) { + if (!box.ranks.length) { box.total.classList.add("hidden"); return; } + var v = totalOf(box.ranks); + box.total.textContent = v.total + (v.soft ? " (soft)" : ""); + box.total.classList.remove("hidden"); + } + + var SHORT = { + blackjack: "Blackjack", win: "Won", dealer_bust: "Won", + lose: "Lost", bust: "Bust", push: "Push", + }; + + // A hand's own result, on the hand. With one hand this says the same thing as + // the pill above it and is a bit redundant; with four it is the only way to + // read what happened, because "you win" is not true of all of them. + function showHandVerdict(box, h) { + var text = h && SHORT[h.outcome]; + if (!text) { box.verdict.classList.add("hidden"); return; } + box.verdict.textContent = text; + box.verdict.classList.remove("hidden"); + } + // ---- the money on the felt ------------------------------------------------- - // stake moves chips from your pile onto the spot: the bet you build before a - // deal, and the second bet a double puts down beside it. - function stake(amount, from) { - return spot.pour(from || purseEl, amount); - } - // settleChips is what the felt does about the outcome, after the cards have - // finished telling you what it is. It reads the same two numbers the ledger - // moved: `bet` (already off your pile since the deal) and `payout` (what comes - // back — stake plus winnings less rake, or nothing at all). + // finished telling you what it is. Each hand is paid on its own, in order, left + // to right — because each hand *is* its own bet, and a split that lost one and + // won the other has to be watchable as exactly that. function settleChips(final) { - var payout = final.payout || 0; - var back = payout - final.bet; // what the house is adding, if anything + var chain = Promise.resolve(); + (final.hands || []).forEach(function (h, i) { + chain = chain.then(function () { + var box = hands[i]; + if (!box) return; + var payout = h.payout || 0; - if (payout <= 0) { - // The house takes it. The stack goes to the rack and doesn't come back. - return spot.sweep(houseEl, final.bet, { gap: 45, lift: 0.6, fade: true }); - } - - // The house pays first, into the spot beside your stake, so you watch the - // winnings arrive on top of the bet that earned them. - return spot - .pour(houseEl, back, { gap: 60 }) - .then(function () { return wait(back > 0 ? 380 : 200); }) - // Paid, then swept up: the whole lot comes back to your pile, and only then - // does the number in the bar move. - .then(function () { return spot.sweep(purseEl, payout, { gap: 40, lift: 0.8 }); }); + if (payout <= 0) { + // The house takes it. The stack goes to the rack and doesn't come back. + return box.spot.sweep(houseEl, h.bet, { gap: 45, lift: 0.6, fade: true }); + } + // The house pays first, into the spot beside the stake, so you watch the + // winnings arrive on top of the bet that earned them. + var back = payout - h.bet; + return box.spot + .pour(houseEl, back, { gap: 60 }) + .then(function () { return wait(back > 0 ? 340 : 160); }) + .then(function () { return box.spot.sweep(purseEl, payout, { gap: 40, lift: 0.8 }); }); + }); + }); + return chain; } - function totals(v) { - if (v.total) { - pTotalEl.textContent = v.total + (v.soft ? " (soft)" : ""); - pTotalEl.classList.remove("hidden"); - } else { - pTotalEl.classList.add("hidden"); - } - // While the hole card is down, the dealer's total is only what's showing — - // so say so, rather than printing a number that quietly means something else. + // While the hole card is down, the dealer's total is only what's showing — so + // say so, rather than printing a number that quietly means something else. + function dealerTotal(v) { if (v.dealer && v.dealer.length) { dTotalEl.textContent = v.hole ? v.dealer_total + " showing" : String(v.dealer_total); dTotalEl.classList.remove("hidden"); @@ -128,19 +217,28 @@ } } - // paint puts a hand on the felt with no animation. This is the resume path: - // you reloaded, or Pete restarted, and your cards are simply there — including - // the stake, which is still on the spot because the server still has it. + // paint puts a deal on the felt with no animation. This is the resume path: you + // reloaded, or Pete restarted, and your cards are simply there — including the + // stakes, which are still on their spots because the server still has them. function paint(v) { dealerEl.innerHTML = ""; - playerEl.innerHTML = ""; - if (!v) { setPhase(null); spot.render(0); return; } + if (!v) { reset(1); setPhase(null); return; } + + reset(v.hands.length || 1); + v.hands.forEach(function (h, i) { + var box = hands[i]; + (h.cards || []).forEach(function (c) { + box.cards.appendChild(cardEl(c)); + box.ranks.push(c.rank); + }); + box.spot.render(v.phase === "done" ? 0 : h.bet); + showTotal(box); + showHandVerdict(box, v.phase === "done" ? h : null); + }); - v.player.forEach(function (c) { playerEl.appendChild(cardEl(c)); }); v.dealer.forEach(function (c) { dealerEl.appendChild(cardEl(c)); }); if (v.hole) dealerEl.appendChild(cardEl(null)); - spot.render(v.phase === "done" ? 0 : v.bet); - totals(v); + dealerTotal(v); setPhase(v); } @@ -155,12 +253,17 @@ function verdict(v) { var text = VERDICTS[v.outcome] || ""; + // Across several hands "you win" is a claim about the money, not about the + // cards: you can win one, lose one, and still be up. The pill reports the + // deal; the badge on each hand reports the hand. + if ((v.hands || []).length > 1) { + text = v.net > 0 ? "You're up on the deal." : v.net < 0 ? "Down on the deal." : "All square."; + } if (!text) { verdictEl.classList.add("hidden"); return; } if (v.net > 0) text += " +" + v.net.toLocaleString(); else if (v.net < 0) text += " " + v.net.toLocaleString(); verdictEl.textContent = text; verdictEl.classList.remove("hidden"); - playerEl.dataset.won = v.net > 0 ? "1" : v.net < 0 ? "-1" : "0"; // The one thing in this room that gets confetti. A natural is rare, it pays // 3:2, and if everything celebrated then nothing would. @@ -176,15 +279,20 @@ // setPhase swaps the controls: bet between hands, act during one. function setPhase(v) { hand = v; - var live = !!v && v.phase === "player"; - betting.classList.toggle("hidden", live); - actions.classList.toggle("hidden", !live); + var acting = !!v && v.phase === "player"; + betting.classList.toggle("hidden", acting); + actions.classList.toggle("hidden", !acting); - if (live) { + if (acting) { var dbl = actions.querySelector('[data-move="double"]'); + var spl = actions.querySelector('[data-move="split"]'); if (dbl) dbl.disabled = !v.can_double; + if (spl) spl.disabled = !v.can_split; + live(v.active || 0); + } else { + live(-1); } - if (!v || v.phase !== "player") verdictEl.classList.toggle("hidden", !(v && v.outcome)); + if (!acting) verdictEl.classList.toggle("hidden", !(v && v.outcome)); } // ---- the script ----------------------------------------------------------- @@ -209,38 +317,77 @@ if (!settles) money(); - // Whatever the server says the stake is, that's what has to be on the spot. - // Two things get here: a double, which puts a second bet down beside the - // first, and a deal whose bet was typed rather than stacked (you kept last - // hand's number and just pressed Deal). Either way the chips go down before - // the card they're buying does. - if (final && final.bet > spot.amount) { - var extra = final.bet - spot.amount; - chain = chain.then(function () { return stake(extra); }); + // A deal whose bet was typed rather than stacked (you kept last hand's number + // and just pressed Deal) has chips to put down before the first card does. + // Everything else that costs money — a double, a split — announces itself as + // an event, and pays for itself there. + if (final && final.hands && final.hands.length && events.length && events[0].kind === "deal") { + var want = final.hands[0].bet; + var have = betSpot().amount; + if (want > have) { + chain = chain.then(function () { return betSpot().pour(purseEl, want - have); }); + } } events.forEach(function (e) { chain = chain.then(function () { + var box; switch (e.kind) { case "deal": + // Clear the felt, but not the stake: those chips are yours, they are + // already on the spot, and they are what this deal is riding on. + var staked = betSpot().amount; dealerEl.innerHTML = ""; - playerEl.innerHTML = ""; - playerEl.dataset.won = "0"; + reset(1); + betSpot().render(staked); verdictEl.classList.add("hidden"); FX.sfx("shuffle"); return; case "player_card": - playerEl.appendChild(cardEl(e.card)); + ensure(e.hand + 1); + box = hands[e.hand]; + box.cards.appendChild(cardEl(e.card)); + box.ranks.push(e.card.rank); + showTotal(box); + live(e.hand); FX.sfx("deal"); return wait(DEAL_MS); + case "split": + // The second card lifts out of the hand it was in and becomes a hand + // of its own, and the bet that follows it is a second bet: the same + // chips again, out of your pile, onto a spot that didn't exist a + // moment ago. + var src = hands[e.hand]; + var moved = src.cards.lastElementChild; + var stake = src.spot.amount; + var fresh = makeHand(e.hand + 1); + if (moved) moved.classList.add("bj-splitting"); + FX.sfx("deal", { v: 1 }); + return wait(SPLIT_MS).then(function () { + if (moved) { + moved.classList.remove("bj-splitting"); + fresh.cards.appendChild(moved); + } + fresh.ranks.push(src.ranks.pop()); + showTotal(src); + showTotal(fresh); + return fresh.spot.pour(purseEl, stake); + }); + + case "double": + // The stake goes down again on that hand, and only that hand. + box = hands[e.hand]; + return box.spot.pour(purseEl, box.spot.amount); + case "dealer_card": // The dealer takes a moment before the first card they draw out. // Card, card, card with no breath in between is a machine dealing; // the pause is the only thing on this table that plays as suspense. var beat = drew ? Promise.resolve() : think(); drew = true; + live(-1); return beat.then(function () { dealerEl.appendChild(cardEl(e.card)); FX.sfx("deal", { v: 1 }); @@ -256,6 +403,7 @@ case "reveal": // The hole card turns over. Its face is in the final hand — this is // the first moment the server has been willing to say what it was. + live(-1); if (!hole) hole = dealerEl.querySelector('.pete-card[data-face="down"]'); if (hole && final && final.dealer && final.dealer[1]) { turnOver(hole, final.dealer[1]); @@ -271,12 +419,16 @@ return chain.then(function () { if (!final) { paint(null); money(); return; } - totals(final); + dealerTotal(final); if (!settles) { setPhase(final); return; } - // The hand is over: nothing is on offer while the money is moving. Hit and - // Stand go now, and Deal comes back at the far end. + // The deal is over: nothing is on offer while the money is moving. The + // actions go now, and Deal comes back at the far end. actions.classList.add("hidden"); + live(-1); + (final.hands || []).forEach(function (h, i) { + if (hands[i]) showHandVerdict(hands[i], h); + }); verdict(final); // The chips move, and the bar catches up with them when they arrive. The // betting controls come back last, once the felt is clear: offering Deal @@ -284,16 +436,20 @@ // has to refuse. return settleChips(final) .then(money) - .then(function () { return standing(final.bet); }) + .then(function () { return standing(base || (final.hands[0] && final.hands[0].bet)); }) .then(function () { setPhase(final); }); }); } // standing leaves your bet up for the next hand, the way you would at a table: - // the stake that just settled goes straight back on the spot. It costs nothing - // — chips on the spot are a proposal until you press Deal — and it's what keeps - // the number in the panel honest, because otherwise a settled hand leaves - // "your bet: 300" printed over an empty spot. + // one hand's stake goes straight back on the spot. It costs nothing — chips on + // the spot are a proposal until you press Deal — and it's what keeps the number + // in the panel honest, because otherwise a settled hand leaves "your bet: 300" + // printed over an empty spot. + // + // It's one hand's worth, not the whole deal's: a split cost you four hundred, + // and standing four hundred back up on one spot would be betting a stranger's + // money on your behalf. function standing(amount) { var money = window.PeteGames.view(); if (!amount || !money || money.chips < amount) { @@ -303,7 +459,7 @@ } bet = amount; showBet(); - return stake(amount); + return betSpot().pour(purseEl, amount); } // think is the dealer's beat: a pause with something to look at, so it reads as @@ -331,7 +487,8 @@ say(err.message, "bad"); // Whatever we thought was on the felt, the server is the authority on it. return window.PeteGames.refresh().then(function (v) { - if (v && !v.hand) spot.render(0); + if (v && !v.hand) { reset(1); setPhase(null); } + else if (v && v.hand) paint(v.hand); }); }) .then(function () { busy = false; }); @@ -368,8 +525,9 @@ // but the spot's total moves now, so a Deal pressed mid-flight still knows // the chip is on its way and doesn't put a second one down. var target = bet; + var spot = betSpot(); spot.amount = bet; - FX.fly(btn, spotEl, { denom: d }).then(function () { + FX.fly(btn, hands[0].spotEl, { denom: d }).then(function () { if (bet >= target) spot.render(target); // unless Clear got there first }); }); @@ -378,8 +536,8 @@ var clearBtn = root.querySelector("[data-bet-clear]"); if (clearBtn) { clearBtn.addEventListener("click", function () { - if (busy || !spot.amount) { bet = 0; showBet(); return; } - spot.sweep(purseEl, null, { gap: 40, lift: 0.7 }); + if (busy || !betSpot().amount) { bet = 0; showBet(); return; } + betSpot().sweep(purseEl, null, { gap: 40, lift: 0.7 }); bet = 0; showBet(); }); @@ -388,6 +546,7 @@ if (dealBtn) { dealBtn.addEventListener("click", function () { if (bet <= 0) { say("Put something on it first.", "bad"); return; } + base = bet; // one hand's worth, which is what a split doubles and a stand puts back send("/api/games/blackjack/deal", { bet: bet }); }); } @@ -403,15 +562,17 @@ if (/^(input|textarea|select)$/i.test((e.target.tagName || ""))) return; if (!hand || hand.phase !== "player" || busy) return; - var move = { h: "hit", s: "stand", d: "double" }[e.key.toLowerCase()]; + var move = { h: "hit", s: "stand", d: "double", p: "split" }[e.key.toLowerCase()]; if (!move) return; if (move === "double" && !hand.can_double) return; + if (move === "split" && !hand.can_split) return; e.preventDefault(); send("/api/games/blackjack/move", { move: move }); }); // The money bar owns the first fetch; the table picks up whatever it found, - // including a hand left sitting on the felt by a reload or a redeploy. + // including a deal left sitting on the felt by a reload or a redeploy. + reset(1); var resumed = false; window.PeteGames.onUpdate(function (v) { if (!resumed) { diff --git a/internal/web/templates/blackjack.html b/internal/web/templates/blackjack.html index 2a73efb..35dcd6e 100644 --- a/internal/web/templates/blackjack.html +++ b/internal/web/templates/blackjack.html @@ -51,21 +51,36 @@ - +
You -
-
+
+
+ +
@@ -116,9 +131,14 @@ hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition"> Double +

- h hit · s stand · d double + h hit · s stand · d double · p split