diff --git a/internal/games/uno/call.go b/internal/games/uno/call.go
new file mode 100644
index 0000000..ea72701
--- /dev/null
+++ b/internal/games/uno/call.go
@@ -0,0 +1,201 @@
+package uno
+
+import "math/rand/v2"
+
+// Calling UNO, and catching the seat that didn't.
+//
+// This was the one rule on the box the table wasn't playing. A hand going down to
+// one card used to emit the "uno" event by itself, which made the call a thing
+// that *happened to you* rather than a thing you did — and a rule nobody can fail
+// is not a rule, it's an announcement.
+//
+// So now: play your second-to-last card and you owe the table a word. Say it
+// (Move.Uno) and you're safe. Stay quiet and the bots get one look at you, right
+// then, before any of them plays — because a bot that has moved on is a bot that
+// has stopped watching your hand. Miss it and you take two.
+//
+// It runs the other way too, and that half is the fun one. A bot forgets often
+// enough to be worth watching for, and when it does it says *nothing* — there is
+// no event, no badge, no tell on the felt except the count beside its fan reading
+// "1 card" with no UNO on it. Catch it (MoveCatch) and it takes two. Call a seat
+// that had nothing to hide and you take two yourself, which is what stops the
+// catch button from being a thing you simply mash every turn.
+//
+// The whole rule turns on one asymmetry, and it's deliberate: the bots get a
+// *roll* to notice you, and you get to actually look. Attention is the edge here,
+// and it's the player's to take.
+
+const (
+ // CatchPenalty is what silence costs, both ways. Two, as printed on the box.
+ CatchPenalty = 2
+
+ // botForget is how often a bot goes down to one card without saying so. High
+ // enough that watching the counts pays — at a full table you'll get a catch to
+ // make every few games — and low enough that a quiet seat is still a thing you
+ // have to notice rather than assume.
+ botForget = 0.30
+
+ // botAlert is the chance a *single* bot notices that you didn't call. They each
+ // get a look, so forgetting is punished about 75% of the time heads-up and 98%
+ // at a full table: the more seats there are watching you, the less you get away
+ // with, which is the right shape for it.
+ botAlert = 0.75
+)
+
+// botForgets rolls for whether a bot muffs its call.
+func botForgets(rng *rand.Rand) bool { return rng.Float64() < botForget }
+
+// declare records whether a seat that is now on one card said so, and — if it did
+// — announces it. A seat on any other number of cards owes nothing and this does
+// nothing, which is what makes it safe to call after every play.
+func (s *State) declare(seat int, called bool, evs *[]Event) {
+ if s.Phase == PhaseDone || len(s.Hands[seat]) != 1 {
+ return
+ }
+ s.ensureCalled()
+ s.Called[seat] = called
+ if called {
+ *evs = append(*evs, Event{Kind: EvUno, Seat: seat, Left: 1})
+ }
+}
+
+// botsCatch is the window. You have just played, you are holding one card, and
+// you didn't say the word — so every bot still in the game gets one look, in seat
+// order, and the first to see it takes you for two.
+//
+// It runs before runBots on purpose. The catch has to land while the table is
+// still looking at the card you played, not three turns later.
+func (s *State) botsCatch(evs *[]Event, rng *rand.Rand) {
+ if s.Phase == PhaseDone || len(s.Hands[You]) != 1 || s.called(You) {
+ return
+ }
+ for _, seat := range s.alive() {
+ if seat == You {
+ continue
+ }
+ if rng.Float64() >= botAlert {
+ continue // this one wasn't looking
+ }
+ s.penalise(You, seat, EvCaught, evs, rng)
+ return // caught once is caught. They don't queue up to take turns at you
+ }
+}
+
+// playerCatches calls out a seat you think is holding one card in silence.
+//
+// It is not a turn: right or wrong, the turn stays where it was, which is with
+// you. What it costs is the risk — a seat that did call, or isn't on one card at
+// all, is a seat you have just accused of nothing, and that is two cards to you.
+func (s *State) playerCatches(m Move, rng *rand.Rand) ([]Event, error) {
+ seat := m.Seat
+ if seat == You || seat < 0 || seat >= len(s.Hands) || !s.live(seat) {
+ return nil, ErrNoCatch
+ }
+ var evs []Event
+ if len(s.Hands[seat]) == 1 && !s.called(seat) {
+ s.penalise(seat, You, EvCaught, &evs, rng) // got them
+ } else {
+ s.penalise(You, seat, EvMiscall, &evs, rng) // they were clean, and you weren't looking
+ }
+ return evs, nil
+}
+
+// penalise makes a seat take the price of the call: cards off the deck, quietly —
+// no draw event, because what the table is being told about is the catch, and a
+// draw event alongside it would animate the same two cards twice.
+//
+// In No Mercy those two cards can be the two that bury them, which is a fine way
+// to go: caught on one card, dead on twenty-five.
+func (s *State) penalise(victim, by int, kind string, evs *[]Event, rng *rand.Rand) {
+ got := s.drawCards(victim, CatchPenalty, evs, rng)
+ if len(got) == 0 {
+ return // the table has nothing left to punish anybody with
+ }
+ s.ensureCalled()
+ s.Called[victim] = false
+ *evs = append(*evs, s.mine(Event{
+ Kind: kind, Seat: victim, By: by, N: len(got), Left: len(s.Hands[victim]),
+ }))
+ s.mercy(victim, evs, rng)
+}
+
+// UnoAt is which cards in your hand would leave you holding exactly one, if you
+// played them. It is the table's cue to ask you for the call, and it comes from
+// here rather than from the browser counting your cards because No Mercy's
+// "discard all" doesn't take one card out of your hand — it takes every card of
+// its colour, and the browser guessing at that is the browser getting it wrong.
+//
+// It answers for every card, legal or not. The table only ever asks about a card
+// you were allowed to play anyway.
+func (s State) UnoAt() []int {
+ if s.Phase == PhaseDone || s.Turn != You {
+ return nil
+ }
+ hand := s.Hands[You]
+ var out []int
+ for i, c := range hand {
+ left := len(hand) - 1
+ if c.Value == DiscardAll {
+ for j, other := range hand {
+ if j != i && other.Color == c.Color && !other.IsWild() {
+ left--
+ }
+ }
+ }
+ if left == 1 {
+ out = append(out, i)
+ }
+ }
+ return out
+}
+
+// Catchable is which seats are, right now, sitting on one card they never
+// announced. It is what the browser puts a button on.
+//
+// This leaks nothing. The card counts are already on the felt and the UNO badge
+// already isn't — this is the same two facts, subtracted, and doing that
+// subtraction on the server is only so that the rule for what counts as catchable
+// lives in one place instead of two.
+func (s State) Catchable() []int {
+ if s.Phase == PhaseDone || s.Turn != You {
+ return nil // you can only catch a seat on your own turn
+ }
+ var out []int
+ for _, seat := range s.alive() {
+ if seat != You && len(s.Hands[seat]) == 1 && !s.called(seat) {
+ out = append(out, seat)
+ }
+ }
+ return out
+}
+
+// called reports whether a seat holding one card announced it. Callers outside
+// the package read the Called slice, which is on the state they already hold.
+func (s State) called(seat int) bool {
+ return seat < len(s.Called) && s.Called[seat]
+}
+
+// ensureCalled grows the slice to fit the table. A game dealt before this rule
+// existed has no Called at all, and the seats in it are all — correctly —
+// uncalled: nobody in that game ever said the word, because there was no way to.
+func (s *State) ensureCalled() {
+ for len(s.Called) < len(s.Hands) {
+ s.Called = append(s.Called, false)
+ }
+}
+
+// tidyCalls forgets the calls that no longer mean anything. A seat's call is only
+// ever about the one card it is holding — draw into two and the word you said is
+// spent, and saying it again is a new thing you have to do.
+//
+// Without this, a seat that called on one card, was made to draw, and worked its
+// way back down to one would still be wearing the old call, and could never be
+// caught again for the rest of the game.
+func (s *State) tidyCalls() {
+ s.ensureCalled()
+ for seat := range s.Called {
+ if len(s.Hands[seat]) != 1 {
+ s.Called[seat] = false
+ }
+ }
+}
diff --git a/internal/games/uno/call_test.go b/internal/games/uno/call_test.go
new file mode 100644
index 0000000..2d6995e
--- /dev/null
+++ b/internal/games/uno/call_test.go
@@ -0,0 +1,323 @@
+package uno
+
+import "testing"
+
+// The UNO call, and the catch on the other side of it. See call.go.
+
+// oneCardAway sets you up holding two cards, both of which go on the pile, so
+// playing either one takes you to UNO.
+//
+// The bot is given a hand that can't touch a red pile and a deck that can't help
+// it, so whatever it does on its turn, it does not make you draw. That matters:
+// a hand that grows spends the call (see tidyCalls), which is correct and would
+// otherwise make these tests flap on the seeds where the bot happens to turn up
+// a +2.
+func oneCardAway(t *testing.T, seed uint64) State {
+ t.Helper()
+ s := deal(t, duel(), 100, seed)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}, {Red, Two}}
+ s.Hands[1] = []Card{{Blue, Three}, {Green, Four}, {Yellow, Six}}
+ s.Deck = make([]Card, 24)
+ for i := range s.Deck {
+ s.Deck[i] = Card{Blue, Nine} // nothing here plays on a red one, and nothing bites
+ }
+ s.Turn = You
+ s.Phase = PhasePlay
+ return s
+}
+
+// TestCallingUnoKeepsYouSafe — say the word and the table has nothing on you.
+// Across a spread of seeds, not one bot ever gets a catch.
+func TestCallingUnoKeepsYouSafe(t *testing.T) {
+ for seed := uint64(0); seed < 200; seed++ {
+ s := oneCardAway(t, seed)
+ next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0, Uno: true})
+ if err != nil {
+ t.Fatalf("seed %d: play: %v", seed, err)
+ }
+ if hasKind(evs, EvCaught) {
+ t.Fatalf("seed %d: caught after calling UNO", seed)
+ }
+ if !hasKind(evs, EvUno) {
+ t.Fatalf("seed %d: called UNO and the table never said so", seed)
+ }
+ if !next.Called[You] {
+ t.Fatalf("seed %d: the call wasn't recorded", seed)
+ }
+ }
+}
+
+// TestForgettingUnoGetsYouCaught — stay quiet on one card and the bot takes you
+// for two. It gets one look, so it misses sometimes; over 400 games it should
+// land near botAlert, and the two cards should actually arrive.
+func TestForgettingUnoGetsYouCaught(t *testing.T) {
+ caught, games := 0, 400
+ for seed := uint64(0); seed < uint64(games); seed++ {
+ s := oneCardAway(t, seed)
+ next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
+ if err != nil {
+ t.Fatalf("seed %d: play: %v", seed, err)
+ }
+ if !hasKind(evs, EvCaught) {
+ continue
+ }
+ caught++
+ // One card left after the play, plus the two the catch cost.
+ if n := len(next.Hands[You]); n != 3 {
+ t.Fatalf("seed %d: caught and holding %d, want 3", seed, n)
+ }
+ if hasKind(evs, EvUno) {
+ t.Fatalf("seed %d: an UNO was announced by a seat that never called", seed)
+ }
+ }
+ rate := float64(caught) / float64(games)
+ if rate < botAlert-0.08 || rate > botAlert+0.08 {
+ t.Errorf("one bot caught you %.0f%% of the time, want about %.0f%% (botAlert)",
+ rate*100, botAlert*100)
+ }
+}
+
+// TestMoreBotsMeansLessGettingAwayWithIt — every seat gets its own look, so
+// forgetting at a full table is very nearly always punished.
+func TestMoreBotsMeansLessGettingAwayWithIt(t *testing.T) {
+ away := func(tier Tier, seed uint64) bool {
+ s := deal(t, tier, 100, seed)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}, {Red, Two}}
+ s.Turn = You
+ s.Phase = PhasePlay
+ _, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
+ if err != nil {
+ t.Fatalf("play: %v", err)
+ }
+ return !hasKind(evs, EvCaught)
+ }
+ var got [2]float64
+ for i, tier := range []Tier{duel(), table()} {
+ escapes := 0
+ for seed := uint64(0); seed < 400; seed++ {
+ if away(tier, seed) {
+ escapes++
+ }
+ }
+ got[i] = float64(escapes) / 400
+ }
+ if got[1] >= got[0] {
+ t.Errorf("you got away with it %.0f%% of the time against one bot and %.0f%% against two; "+
+ "two pairs of eyes should catch you more often, not less", got[0]*100, got[1]*100)
+ }
+}
+
+// quietBot puts a bot on one card it never called, with the turn back on you.
+func quietBot(t *testing.T, called bool) State {
+ t.Helper()
+ s := deal(t, duel(), 100, 21)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}, {Blue, Two}, {Green, Three}}
+ s.Hands[1] = []Card{{Yellow, Nine}}
+ s.Called = []bool{false, called}
+ s.Turn = You
+ s.Phase = PhasePlay
+ return s
+}
+
+// TestCatchingAQuietBot — it's on one card and it never said so. Two cards to it,
+// and the turn is still yours: catching is not a move you spend a turn on.
+func TestCatchingAQuietBot(t *testing.T) {
+ s := quietBot(t, false)
+ before := total(census(s))
+
+ next, evs, err := ApplyMove(s, Move{Kind: MoveCatch, Seat: 1})
+ if err != nil {
+ t.Fatalf("catch: %v", err)
+ }
+ if !hasKind(evs, EvCaught) {
+ t.Fatal("no catch event")
+ }
+ if n := len(next.Hands[1]); n != 3 {
+ t.Errorf("the bot holds %d, want 3: one card, plus the two it just took", n)
+ }
+ if n := len(next.Hands[You]); n != 3 {
+ t.Errorf("your hand is %d, want 3: a catch costs you nothing", n)
+ }
+ if next.Turn != You {
+ t.Errorf("the turn went to seat %d: a catch is not a turn", next.Turn)
+ }
+ if total(census(next)) != before {
+ t.Error("the catch lost a card")
+ }
+}
+
+// TestCatchingACleanBotCostsYou — it called, or it isn't on one card at all.
+// Either way you've accused it of nothing, and that is two cards to you.
+func TestCatchingACleanBotCostsYou(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ state State
+ }{
+ {"it called", quietBot(t, true)},
+ {"it isn't even close", func() State {
+ s := quietBot(t, false)
+ s.Hands[1] = []Card{{Yellow, Nine}, {Yellow, Eight}}
+ return s
+ }()},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ next, evs, err := ApplyMove(tc.state, Move{Kind: MoveCatch, Seat: 1})
+ if err != nil {
+ t.Fatalf("catch: %v", err)
+ }
+ if !hasKind(evs, EvMiscall) {
+ t.Fatal("no miscall event")
+ }
+ if n := len(next.Hands[You]); n != 5 {
+ t.Errorf("your hand is %d, want 5: three, plus the two a bad call cost", n)
+ }
+ if next.Turn != You {
+ t.Errorf("the turn went to seat %d: even a bad catch isn't a turn", next.Turn)
+ }
+ })
+ }
+}
+
+// TestYouCannotCatchYourself, or a seat that isn't at the table.
+func TestYouCannotCatchYourself(t *testing.T) {
+ s := quietBot(t, false)
+ for _, seat := range []int{You, -1, 9} {
+ if _, _, err := ApplyMove(s, Move{Kind: MoveCatch, Seat: seat}); err != ErrNoCatch {
+ t.Errorf("catching seat %d: got %v, want ErrNoCatch", seat, err)
+ }
+ }
+}
+
+// TestACallIsSpentWhenTheHandGrows. Call on one card, get made to draw, and work
+// your way back down to one: that is a new call you owe, not the old one still
+// standing. Without this a seat could be caught out once and never again.
+func TestACallIsSpentWhenTheHandGrows(t *testing.T) {
+ s := deal(t, duel(), 100, 5)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}}
+ s.Called = []bool{true, false}
+ s.Turn = You
+ s.Phase = PhasePlay
+
+ // Draw, and the hand is two: the word you said was about a card you no longer
+ // hold on its own.
+ next, _, err := ApplyMove(s, Move{Kind: MoveDraw})
+ if err != nil {
+ t.Fatalf("draw: %v", err)
+ }
+ if next.Called[You] {
+ t.Error("the call survived the hand growing; it should be spent")
+ }
+}
+
+// TestCatchableIsWhatTheTableCanSee — a quiet bot on one card, and nobody else.
+func TestCatchable(t *testing.T) {
+ s := quietBot(t, false)
+ if got := s.Catchable(); len(got) != 1 || got[0] != 1 {
+ t.Errorf("Catchable() = %v, want [1]", got)
+ }
+ clean := quietBot(t, true)
+ if got := clean.Catchable(); len(got) != 0 {
+ t.Errorf("Catchable() = %v on a bot that called, want none", got)
+ }
+ // And not on somebody else's turn: you can only call it out when it's on you.
+ off := quietBot(t, false)
+ off.Turn = 1
+ if got := off.Catchable(); len(got) != 0 {
+ t.Errorf("Catchable() = %v off-turn, want none", got)
+ }
+}
+
+// TestUnoAtSeesThroughDiscardAll — the whole reason the table asks the engine
+// which cards take you to one, rather than counting your hand itself. "Discard
+// all" takes every card of its colour with it, so a six-card hand can land on
+// one, and a browser subtracting one from six gets a player caught.
+func TestUnoAtSeesThroughDiscardAll(t *testing.T) {
+ s := deal(t, nmDuel(), 100, 3)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, DiscardAll}, {Red, One}, {Red, Nine}, {Red, Seven}, {Blue, Two}}
+ s.Turn = You
+ s.Phase = PhasePlay
+
+ // Index 0 dumps itself and the three other reds: five cards become one.
+ // Index 4 is an ordinary play: five become four.
+ got := s.UnoAt()
+ if len(got) != 1 || got[0] != 0 {
+ t.Errorf("UnoAt() = %v, want [0]: only the discard-all lands you on one card", got)
+ }
+}
+
+// TestUnoAtIsTheOrdinaryCaseToo — two cards in hand, and either of them is a call.
+func TestUnoAtIsTheOrdinaryCaseToo(t *testing.T) {
+ s := oneCardAway(t, 1)
+ got := s.UnoAt()
+ if len(got) != 2 {
+ t.Errorf("UnoAt() = %v, want both cards: either one leaves you holding one", got)
+ }
+}
+
+// TestGoingOutNeedsNoCall — your last card is not one card, it's none. Nobody
+// owes the table a word for winning.
+func TestGoingOutNeedsNoCall(t *testing.T) {
+ s := deal(t, duel(), 100, 9)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Red, One}}
+ s.Turn = You
+ s.Phase = PhasePlay
+
+ next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
+ if err != nil {
+ t.Fatalf("play the last card: %v", err)
+ }
+ if !next.Outcome.Won() {
+ t.Fatalf("outcome %q, want a win", next.Outcome)
+ }
+ if hasKind(evs, EvCaught) {
+ t.Error("caught for not calling UNO on the card that won the game")
+ }
+}
+
+// TestABotThatForgetsSaysNothing — the tell is the absence of the badge, and the
+// count beside the fan. If a quiet bot emitted anything at all there'd be nothing
+// to spot.
+func TestABotThatForgetsSaysNothing(t *testing.T) {
+ quiet := 0
+ for seed := uint64(0); seed < 300 && quiet < 1; seed++ {
+ s := deal(t, duel(), 100, seed)
+ s.Color = Red
+ s.Discard = []Card{{Red, Five}}
+ s.Hands[You] = []Card{{Blue, Two}, {Blue, Three}, {Blue, Four}}
+ s.Hands[1] = []Card{{Red, One}, {Red, Nine}}
+ s.Turn = 1
+ s.Phase = PhasePlay
+ s.Turn = You // the bot plays on the back of your move
+
+ // Draw, handing the turn over: the bot plays a red and lands on one card.
+ next, evs, err := ApplyMove(s, Move{Kind: MoveDraw})
+ if err != nil {
+ t.Fatalf("draw: %v", err)
+ }
+ if len(next.Hands[1]) != 1 || next.Called[1] {
+ continue // it either didn't get down to one, or it remembered
+ }
+ quiet++
+ if hasKind(evs, EvUno) {
+ t.Error("a bot that forgot to call still announced it")
+ }
+ if len(next.Catchable()) != 1 {
+ t.Error("a quiet bot on one card isn't catchable")
+ }
+ }
+ if quiet == 0 {
+ t.Skip("no bot forgot in 300 games; botForget may have been turned down")
+ }
+}
diff --git a/internal/games/uno/nomercy.go b/internal/games/uno/nomercy.go
index f42ee70..6b66a00 100644
--- a/internal/games/uno/nomercy.go
+++ b/internal/games/uno/nomercy.go
@@ -152,7 +152,7 @@ func (s *State) roulette(victim int, color Color, evs *[]Event, rng *rand.Rand)
}
if got > 0 {
e := Event{Kind: EvRoulette, Seat: victim, N: got, Color: color, Left: len(s.Hands[victim])}
- *evs = append(*evs, e)
+ *evs = append(*evs, s.mine(e))
}
if s.mercy(victim, evs, rng) {
return
@@ -181,8 +181,8 @@ func (s *State) discardAll(seat int, color Color, evs *[]Event) int {
top := s.Discard[len(s.Discard)-1]
s.Discard = append(s.Discard[:len(s.Discard)-1], dumped...)
s.Discard = append(s.Discard, top)
- *evs = append(*evs, Event{Kind: EvDiscardAll, Seat: seat, N: len(dumped),
- Color: color, Left: len(kept)})
+ *evs = append(*evs, s.mine(Event{Kind: EvDiscardAll, Seat: seat, N: len(dumped),
+ Color: color, Left: len(kept)}))
}
return len(dumped)
}
@@ -205,7 +205,7 @@ func (s *State) mercy(seat int, evs *[]Event, rng *rand.Rand) bool {
s.Hands[seat] = nil
s.Out[seat] = true
s.Pending = 0 // a dead seat pays no bill, and leaves none behind
- *evs = append(*evs, Event{Kind: EvMercy, Seat: seat, N: n, Left: 0})
+ *evs = append(*evs, s.mine(Event{Kind: EvMercy, Seat: seat, N: n, Left: 0}))
if seat == You {
s.lose(evs)
diff --git a/internal/games/uno/nomercy_test.go b/internal/games/uno/nomercy_test.go
index a9087d2..a7b4b03 100644
--- a/internal/games/uno/nomercy_test.go
+++ b/internal/games/uno/nomercy_test.go
@@ -120,6 +120,19 @@ func playMove(s State, idx int, rng *rand.Rand) Move {
if s.Hands[You][idx].IsWild() {
m.Color = Red + Color(rng.IntN(4))
}
+ // It calls UNO. That is not a strategy, it is a button — the table puts it in
+ // front of you and waits — and what these multiples price is *bad card play*,
+ // not a player who ignores the felt shouting at them.
+ //
+ // A player who genuinely never calls loses far more than this one does, and
+ // nothing here is priced for them. That is the rule having teeth, which is the
+ // point of it. Naive is the floor for someone playing the game badly, not for
+ // someone not playing it.
+ for _, at := range s.UnoAt() {
+ if at == idx {
+ m.Uno = true
+ }
+ }
return m
}
@@ -323,7 +336,10 @@ func TestDiscardAllTakesTheColourWithIt(t *testing.T) {
s.Phase = PhasePlay
before := total(census(s))
- next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
+ // With the call, because this play takes three reds out of the hand at once and
+ // lands on exactly one card — which is precisely the case a browser counting
+ // "hand length minus one" would miss, and the reason UnoAt is the engine's job.
+ next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0, Uno: true})
if err != nil {
t.Fatalf("play discard-all: %v", err)
}
diff --git a/internal/games/uno/uno.go b/internal/games/uno/uno.go
index 6e1d913..8a45cd7 100644
--- a/internal/games/uno/uno.go
+++ b/internal/games/uno/uno.go
@@ -39,6 +39,7 @@ var (
ErrMustPlayNow = errors.New("uno: play the card you drew, or pass")
ErrMustStack = errors.New("uno: answer the stack with a draw card, or take it")
ErrNoStack = errors.New("uno: there's no stack to take")
+ ErrNoCatch = errors.New("uno: there's nobody to catch there")
ErrUnknownMove = errors.New("uno: unknown move")
ErrBadBet = errors.New("uno: bet must be positive")
ErrUnknownTier = errors.New("uno: no such tier")
@@ -233,6 +234,13 @@ func (t Tier) Deck() []Card {
// instead of the 8% they were meant to. The numbers below are the honest ones.
//
// This is exactly the drift TestTheMultiplesAreStillPriced now exists to stop.
+//
+// Re-measured again when the UNO call went in (call.go): 40.1% / 28.5% / 23.1%,
+// which is the same game. That is the point — the naive player *calls*, because
+// calling is a button and not a strategy, so the rule costs them nothing and
+// these multiples stand. What the rule adds is upside for a player who watches
+// the counts and catches a quiet bot, which is the good play these tiers are
+// meant to leave room for.
var Tiers = []Tier{
{Slug: "duel", Name: "Duel", Bots: 1, Base: 2.4,
Blurb: "One bot, head to head. A reverse is a skip with two at the table."},
@@ -246,7 +254,7 @@ var Tiers = []Tier{
//
// The multiples are measured, not guessed, and they are *not* the normal ones —
// the naive strategy (play the first legal card; take a stack you can't answer)
-// wins 46.7% / 31.2% / 25.3% here, against 40.3 / 29.2 / 23.3 on the normal deck.
+// wins 45.6% / 31.8% / 27.4% here, against 40.1 / 28.5 / 23.1 on the normal deck.
//
// Which is to say: **No Mercy is easier than UNO**, at every table size, and so
// it pays less. That reads backwards until you see why. The mercy rule kills
@@ -264,7 +272,11 @@ var NoMercyTiers = []Tier{
Blurb: "One bot, 168 cards. Stack the draws or eat them."},
{Slug: "nm-table", Name: "No Mercy Table", Bots: 2, Base: 3.1, NoMercy: true,
Blurb: "Two bots. A +10 answered twice is somebody's whole hand."},
- {Slug: "nm-full", Name: "No Mercy Full House", Bots: 3, Base: 3.8, NoMercy: true,
+ // Re-priced 2026-07-14 with the call rule in: 3.8 was paying a *negative* house
+ // edge here (-0.2%), which is the house paying you to sit down. The naive win
+ // rate at this table is 27.4%, not the 25.3% it was priced off — three bots
+ // burying each other is even better for you than the last measurement caught.
+ {Slug: "nm-full", Name: "No Mercy Full House", Bots: 3, Base: 3.5, NoMercy: true,
Blurb: "Three bots. Twenty-five cards and you're out of the game."},
}
@@ -325,6 +337,11 @@ type State struct {
Out []bool `json:"out,omitempty"`
Pending int `json:"pending,omitempty"`
+ // Called is who, holding one card, said so. It is only ever meaningful for a
+ // seat on exactly one card — every other seat's entry is false and means
+ // nothing — and it is what a catch is tested against. See call.go.
+ Called []bool `json:"called,omitempty"`
+
Seed1 uint64 `json:"seed1"`
Seed2 uint64 `json:"seed2"`
Step uint64 `json:"step"` // how many moves have been applied; the rng's other half
@@ -346,7 +363,26 @@ type Event struct {
Color Color `json:"color,omitempty"` // the colour now in play, on a wild
N int `json:"n,omitempty"` // how many cards were drawn
Left int `json:"left"` // cards left in that seat's hand afterwards
+ By int `json:"by"` // who caught them, on a catch. Seat zero is a real answer here, so never omitempty
Text string `json:"text,omitempty"`
+
+ // Hand is *your* hand as it stands after this event, and it is only ever set
+ // on an event that changed it. The table plays a lap of events back over
+ // several seconds, and for all of them the hand on screen has to be the hand
+ // you actually hold — a +4 that lands on you at the top of the lap must show
+ // up in your fan as it lands, not when the lap ends. There is no leak here:
+ // this is the one hand the browser is already entitled to see.
+ Hand []Card `json:"hand,omitempty"`
+}
+
+// mine stamps the player's hand onto an event that just changed it. Events about
+// a bot's hand carry nothing — the browser never learns those, and stamping is
+// scoped to seat zero precisely so it can't start.
+func (s *State) mine(e Event) Event {
+ if e.Seat == You {
+ e.Hand = append([]Card(nil), s.Hands[You]...)
+ }
+ return e
}
// The kinds an Event comes in.
@@ -359,7 +395,9 @@ type Event struct {
// pass the turn moves on with nothing played
// skip a seat loses its turn
// reverse the direction flips
-// uno a hand is down to one card
+// uno a hand is down to one card, and its owner said so
+// caught a seat went down to one card *quietly*, and somebody noticed: +2
+// miscall you called a seat that had nothing to hide, and paid for it: +2
// reshuffle the discard goes back under
// settle it's over
//
@@ -379,6 +417,8 @@ const (
EvSkip = "skip"
EvReverse = "reverse"
EvUno = "uno"
+ EvCaught = "caught"
+ EvMiscall = "miscall"
EvReshuffle = "reshuffle"
EvSettle = "settle"
@@ -392,19 +432,26 @@ const (
// Move is what the player sends: play this card, take one off the deck, or —
// having taken one you can play — decline to play it.
type Move struct {
- Kind string `json:"kind"` // "play" | "draw" | "pass"
+ Kind string `json:"kind"` // "play" | "draw" | "pass" | "take" | "catch"
Index int `json:"index"` // which card of your hand, for a play
Color Color `json:"color"` // the colour you name, for a wild
+ Uno bool `json:"uno"` // "…and UNO!", for a play that leaves you on one card
+ Seat int `json:"seat"` // whose silence you're calling, for a catch
}
// Move kinds. Take is No Mercy's: it is how you give in to a stack you can't
// answer, and it is a *decision*, so it gets a name of its own rather than being
// bolted onto draw.
+//
+// Catch is the other half of the UNO call — see call.go. It is a move you make
+// out of turn order (it costs you nothing but the risk of being wrong), so it is
+// a kind rather than a flag on the moves that do cost a turn.
const (
- MovePlay = "play"
- MoveDraw = "draw"
- MovePass = "pass"
- MoveTake = "take"
+ MovePlay = "play"
+ MoveDraw = "draw"
+ MovePass = "pass"
+ MoveTake = "take"
+ MoveCatch = "catch"
)
// New deals a game: a shuffled deck, seven each, and a card turned over.
@@ -434,6 +481,7 @@ func New(bet int64, t Tier, rakePct float64, seed1, seed2 uint64) (State, []Even
seats := t.Bots + 1
s.Out = make([]bool, seats)
+ s.Called = make([]bool, seats)
s.Hands = make([][]Card, seats)
for i := range s.Hands {
s.Hands[i] = make([]Card, 0, HandSize)
@@ -460,7 +508,7 @@ func New(bet int64, t Tier, rakePct float64, seed1, seed2 uint64) (State, []Even
break
}
- return s, []Event{{Kind: EvDeal, Card: s.topPtr(), Color: s.Color}}, nil
+ return s, []Event{s.mine(Event{Kind: EvDeal, Card: s.topPtr(), Color: s.Color})}, nil
}
// ApplyMove is the engine. Your move goes in; your move, and every bot turn it
@@ -479,6 +527,7 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
next := s.clone()
next.Step++
+ next.ensureCalled()
rng := stepRNG(next.Seed1, next.Seed2, next.Step)
var evs []Event
@@ -492,6 +541,8 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
evs, err = next.playerPasses()
case MoveTake:
evs, err = next.playerTakes(rng)
+ case MoveCatch:
+ evs, err = next.playerCatches(m, rng)
default:
return s, nil, ErrUnknownMove
}
@@ -499,8 +550,17 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
return s, nil, err // the caller's state, untouched
}
+ // Before anybody moves: did you go down to one card without saying so? This is
+ // the only window there is. The bots are about to take their turns, and a bot
+ // that has played on is a bot that has stopped looking at your hand.
+ next.botsCatch(&evs, rng)
+
// The bots take their turns on the back of yours, and the whole run comes
// back as one script. This is the reason solo UNO needs no socket.
+ //
+ // A catch is not a turn — it leaves the turn where it was, which is with you —
+ // so this is a no-op after one, and that is the whole mechanism by which
+ // catching a bot costs you nothing but the risk of being wrong.
next.runBots(&evs, rng)
// And if that left a table nobody can move at, it ends here rather than
@@ -508,6 +568,7 @@ func ApplyMove(s State, m Move) (State, []Event, error) {
if next.Phase != PhaseDone && next.stalled() {
next.stuck(&evs)
}
+ next.tidyCalls()
return next, evs, nil
}
@@ -543,6 +604,11 @@ func (s *State) playerPlays(m Move, rng *rand.Rand) ([]Event, error) {
var evs []Event
s.discard(You, card, m.Color, &evs)
s.after(You, card, &evs, rng)
+ // Whether you called is checked *after* the card has finished resolving, not
+ // after it left your hand. No Mercy's "discard all" takes every card of a
+ // colour with it, so it can drop you from six cards to one in a single play —
+ // and the seat that gets there that way owes the call just the same.
+ s.declare(You, m.Uno, &evs)
return evs, nil
}
@@ -693,6 +759,11 @@ func (s *State) botPlays(seat int, card Card, idx int, evs *[]Event, rng *rand.R
}
s.discard(seat, card, color, evs)
s.after(seat, card, evs, rng)
+ // A bot that has just gone down to one card mostly remembers to say so. When it
+ // doesn't, it says nothing at all — no event, no badge — and the only thing on
+ // the table that gives it away is the count beside its fan reading "1 card".
+ // Spotting that is the player's to do.
+ s.declare(seat, !botForgets(rng), evs)
}
// stalled reports whether the table is dead: nothing left to draw anywhere, and
@@ -737,10 +808,11 @@ func (s *State) discard(seat int, card Card, color Color, evs *[]Event) {
}
s.Discard = append(s.Discard, card)
e := Event{Kind: EvPlay, Seat: seat, Card: &card, Color: s.Color, Left: len(s.Hands[seat])}
- *evs = append(*evs, e)
- if len(s.Hands[seat]) == 1 {
- *evs = append(*evs, Event{Kind: EvUno, Seat: seat})
- }
+ *evs = append(*evs, s.mine(e))
+ // No UNO event here any more. Going down to one card used to *be* the call,
+ // which meant nobody at this table could ever fail to make it. Now the call is
+ // a thing a seat does or doesn't do, and it is announced — or conspicuously not
+ // — by the two functions that know whether it was made. See call.go.
}
// after resolves what the card just played does, and moves the turn on. It is
@@ -834,18 +906,7 @@ func (s *State) punish(victim, n int, evs *[]Event, rng *rand.Rand) {
// A bot's cards go face down: the event carries the count, never the card. The
// only hand whose faces cross the wire is yours.
func (s *State) deal(seat, n int, forced bool, evs *[]Event, rng *rand.Rand) []Card {
- got := make([]Card, 0, n)
- for i := 0; i < n; i++ {
- if len(s.Deck) == 0 && !s.reshuffle(evs, rng) {
- break
- }
- c, ok := s.pop()
- if !ok {
- break
- }
- s.Hands[seat] = append(s.Hands[seat], c)
- got = append(got, c)
- }
+ got := s.drawCards(seat, n, evs, rng)
if len(got) == 0 {
return got
}
@@ -858,7 +919,28 @@ func (s *State) deal(seat, n int, forced bool, evs *[]Event, rng *rand.Rand) []C
c := got[0]
e.Card = &c // your own card, and only yours, comes face up
}
- *evs = append(*evs, e)
+ *evs = append(*evs, s.mine(e))
+ return got
+}
+
+// drawCards is deal without the announcement: cards come off the deck (with a
+// reshuffle under them if it runs dry) and go into a hand, and nothing is said
+// about it. deal wraps it to say the usual thing; a catch wraps it to say a
+// different thing. It hands back what was actually drawn, which can be fewer
+// cards than asked for when the table has nothing left to give.
+func (s *State) drawCards(seat, n int, evs *[]Event, rng *rand.Rand) []Card {
+ got := make([]Card, 0, n)
+ for i := 0; i < n; i++ {
+ if len(s.Deck) == 0 && !s.reshuffle(evs, rng) {
+ break
+ }
+ c, ok := s.pop()
+ if !ok {
+ break
+ }
+ s.Hands[seat] = append(s.Hands[seat], c)
+ got = append(got, c)
+ }
return got
}
@@ -1105,6 +1187,7 @@ func (s State) clone() State {
s.Discard = append([]Card(nil), s.Discard...)
s.Bots = append([]string(nil), s.Bots...)
s.Out = append([]bool(nil), s.Out...)
+ s.Called = append([]bool(nil), s.Called...)
return s
}
diff --git a/internal/web/games_uno.go b/internal/web/games_uno.go
index 1b1074e..3323a00 100644
--- a/internal/web/games_uno.go
+++ b/internal/web/games_uno.go
@@ -45,11 +45,12 @@ func viewUnoCard(c uno.Card) unoCardView {
// unoSeatView is one seat at the table: a name, and a number of cards. A bot's
// cards are a *count*. There is no field here for what they are.
type unoSeatView struct {
- Name string `json:"name"`
- Cards int `json:"cards"`
- You bool `json:"you"`
- Uno bool `json:"uno"` // down to one card
- Out bool `json:"out"` // No Mercy: buried at twenty-five, and not coming back
+ Name string `json:"name"`
+ Cards int `json:"cards"`
+ You bool `json:"you"`
+ Uno bool `json:"uno"` // down to one card
+ Called bool `json:"called"` // …and said so. A seat where Uno is true and this isn't is a seat you can catch
+ Out bool `json:"out"` // No Mercy: buried at twenty-five, and not coming back
}
// unoView is a game as its player may see it.
@@ -63,6 +64,14 @@ type unoView struct {
Color string `json:"color"` // the colour in play, which a wild renames
Deck int `json:"deck"` // cards left to draw
+ // The UNO call. UnoAt is which of your cards would leave you holding exactly
+ // one if you played it — the table asks for the call on those, and it asks the
+ // engine which they are because No Mercy's "discard all" makes counting the
+ // hand the wrong answer. Catchable is which seats are sitting on one card they
+ // never announced.
+ UnoAt []int `json:"uno_at"`
+ Catchable []int `json:"catchable"`
+
Turn int `json:"turn"`
Dir int `json:"dir"`
@@ -104,7 +113,10 @@ func viewUno(g uno.State) unoView {
// alone, which is why a buried seat is asked about rather than inferred.
for i, n := range g.Counts() {
live := g.Live(i)
- seat := unoSeatView{Cards: n, You: i == uno.You, Uno: live && n == 1, Out: !live}
+ seat := unoSeatView{
+ Cards: n, You: i == uno.You, Uno: live && n == 1, Out: !live,
+ Called: i < len(g.Called) && g.Called[i],
+ }
if i == uno.You {
seat.Name = "You"
} else if i-1 < len(g.Bots) {
@@ -127,6 +139,16 @@ func viewUno(g uno.State) unoView {
if v.Playable == nil {
v.Playable = []int{}
}
+ // Empty arrays, never null: the table indexes into these, and `null.indexOf`
+ // is a broken game rather than a quiet no-op.
+ v.UnoAt = g.UnoAt()
+ if v.UnoAt == nil {
+ v.UnoAt = []int{}
+ }
+ v.Catchable = g.Catchable()
+ if v.Catchable == nil {
+ v.Catchable = []int{}
+ }
return v
}
@@ -141,16 +163,32 @@ type unoEventView struct {
Color string `json:"color,omitempty"`
N int `json:"n,omitempty"`
Left int `json:"left"` // never omitempty: a seat that goes out leaves zero, and zero is the number the table has to draw
+ By int `json:"by"` // who did the catching. Seat zero is you, and "you" is a real answer, so never omitempty either
Text string `json:"text,omitempty"`
+
+ // Hand is your hand as it stands after this event, on the events that changed
+ // it. The table redraws your fan from this as the lap plays back — see the
+ // engine's Event.Hand for why. It is your own hand, so there is nothing here
+ // the browser wasn't already holding.
+ Hand []unoCardView `json:"hand,omitempty"`
}
func viewUnoEvents(evs []uno.Event) []unoEventView {
out := make([]unoEventView, 0, len(evs))
for _, e := range evs {
- v := unoEventView{Kind: e.Kind, Seat: e.Seat, N: e.N, Left: e.Left, Text: e.Text}
+ v := unoEventView{Kind: e.Kind, Seat: e.Seat, N: e.N, Left: e.Left, By: e.By, Text: e.Text}
if e.Color != uno.Wild {
v.Color = e.Color.String()
}
+ // The engine only stamps a hand on an event about seat zero. This is the belt
+ // to that brace: whatever the engine thinks it's doing, no hand but yours
+ // leaves this building.
+ if e.Seat == uno.You && e.Hand != nil {
+ v.Hand = make([]unoCardView, 0, len(e.Hand))
+ for _, c := range e.Hand {
+ v.Hand = append(v.Hand, viewUnoCard(c))
+ }
+ }
if e.Card != nil {
// The engine only ever attaches a card to an event the seat is entitled
// to see it in — a card played face up, or one *you* drew. This check is
@@ -268,6 +306,8 @@ func (s *Server) handleUnoMove(w http.ResponseWriter, r *http.Request) {
msg = "answer the stack with a draw card, or take it"
case errors.Is(err, uno.ErrNoStack):
msg = "there's nothing pointed at you to take"
+ case errors.Is(err, uno.ErrNoCatch):
+ msg = "there's nobody in that seat to catch"
}
writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": msg})
return
diff --git a/internal/web/static/css/input.css b/internal/web/static/css/input.css
index 10a77a9..f06962e 100644
--- a/internal/web/static/css/input.css
+++ b/internal/web/static/css/input.css
@@ -1563,6 +1563,10 @@ html[data-phase="night"] {
animation-delay: calc(var(--i, 0) * 45ms);
transition: transform 0.16s ease, filter 0.16s ease;
}
+ /* A card that was already in your hand before this redraw doesn't get dealt to
+ you again. The hand is redrawn several times a lap now (every event that
+ changes it), and without this every one of them re-fans the whole thing. */
+ .pete-uno-hand .pete-uno-card[data-settled="1"] { animation: none; }
.pete-uno-hand .pete-uno-card[data-on="1"] {
cursor: pointer;
transform: translateY(-0.5rem);
@@ -1750,6 +1754,85 @@ html[data-phase="night"] {
.pete-uno-swatch[data-c="yellow"] { --sw: #e0b02c; color: #2b2118; }
.pete-uno-swatch[data-c="green"] { --sw: #46a86b; }
+ /* ---- calling it, and catching the seat that didn't -------------------------
+ The call is a button with a clock on it. It wants to be the loudest thing on
+ the felt for the two-and-a-bit seconds it exists, because the whole rule is
+ that you can miss it. */
+ .pete-uno-call {
+ margin-top: 0.85rem;
+ padding: 0.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: 0.02em;
+ box-shadow: 0 5px 0 rgba(0,0,0,0.3), 0 0 0 0 rgba(224,176,44,0.55);
+ animation: pete-uno-call 0.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,0.3); }
+ @keyframes pete-uno-call {
+ 0%, 100% { transform: scale(1); box-shadow: 0 5px 0 rgba(0,0,0,0.3), 0 0 0 0 rgba(224,176,44,0.5); }
+ 50% { transform: scale(1.05); box-shadow: 0 5px 0 rgba(0,0,0,0.3), 0 0 0 0.9rem rgba(224,176,44,0); }
+ }
+
+ /* How long you've got, draining left to right. The duration is set on the
+ element: the window lives in uno.js, and a bar that disagrees with the clock
+ it's drawing is worse than no bar. */
+ .pete-uno-timer {
+ margin-top: 0.8rem;
+ height: 0.35rem;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.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 {
+ from { transform: scaleX(1); }
+ to { transform: scaleX(0); }
+ }
+
+ /* A seat on one card that never said so. It gets a button, and the button is
+ the only thing on the felt that says it — there's no badge, because not
+ announcing is exactly what it did. */
+ .pete-uno-catch {
+ position: absolute;
+ top: -0.55rem;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 6;
+ padding: 0.22rem 0.7rem;
+ border-radius: 999px;
+ background: #cc3d4a;
+ color: #fff;
+ font-family: "Fredoka", ui-sans-serif, system-ui, sans-serif;
+ font-size: 0.72rem;
+ font-weight: 700;
+ white-space: nowrap;
+ box-shadow: 0 3px 0 rgba(0,0,0,0.25);
+ animation: pete-uno-catch 1.1s ease-in-out infinite;
+ transition: filter 0.12s ease;
+ }
+ .pete-uno-catch:hover { filter: brightness(1.12); }
+ .pete-uno-catch:disabled { opacity: 0.45; animation: none; }
+ @keyframes pete-uno-catch {
+ 0%, 100% { 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; }
+ }
+
/* ---- No Mercy -------------------------------------------------------------
A rules dial, not a fourth table, and the felt says so: the same table, the
same cards, plus six faces the normal box doesn't print and one thing that
diff --git a/internal/web/static/css/output.css b/internal/web/static/css/output.css
index 6325cb6..941377f 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-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-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}.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
diff --git a/internal/web/static/js/blackjack.js b/internal/web/static/js/blackjack.js
index ee0ada0..0fc5645 100644
--- a/internal/web/static/js/blackjack.js
+++ b/internal/web/static/js/blackjack.js
@@ -164,7 +164,13 @@
// The one thing in this room that gets confetti. A natural is rare, it pays
// 3:2, and if everything celebrated then nothing would.
+ //
+ // The *sound* is not so precious: a win is a win and you should hear it. So
+ // the fanfare rides on the money, not on the confetti.
if (v.outcome === "blackjack") FX.burst(verdictEl, { count: 34 });
+ else if (v.net > 0) FX.sfx("win");
+ else if (v.net < 0) FX.sfx("lose");
+ else FX.sfx("push");
}
// setPhase swaps the controls: bet between hands, act during one.
@@ -221,10 +227,12 @@
playerEl.innerHTML = "";
playerEl.dataset.won = "0";
verdictEl.classList.add("hidden");
+ FX.sfx("shuffle");
return;
case "player_card":
playerEl.appendChild(cardEl(e.card));
+ FX.sfx("deal");
return wait(DEAL_MS);
case "dealer_card":
@@ -235,12 +243,14 @@
drew = true;
return beat.then(function () {
dealerEl.appendChild(cardEl(e.card));
+ FX.sfx("deal", { v: 1 });
return wait(DEAL_MS);
});
case "dealer_hole":
hole = cardEl(null);
dealerEl.appendChild(hole);
+ FX.sfx("deal", { v: 2 });
return wait(DEAL_MS);
case "reveal":
diff --git a/internal/web/static/js/casino-cards.js b/internal/web/static/js/casino-cards.js
index c8d663a..aa65c90 100644
--- a/internal/web/static/js/casino-cards.js
+++ b/internal/web/static/js/casino-cards.js
@@ -155,11 +155,17 @@
}
// turnOver flips a face-down card up, now that we've been told what it is.
+ //
+ // Unlike el(), this is only ever a *gesture* — a card that was down and is now
+ // up, because something happened. It never runs on a repaint, which is why the
+ // flip sound can live here and be right for every table at once, and why the
+ // deal sound cannot: el() runs every time a board is redrawn.
function turnOver(wrap, face) {
if (!wrap) return;
paint(wrap.querySelector(".pete-card-front"), face);
wrap.dataset.face = "up";
wrap.dataset.key = face.label;
+ if (window.PeteSFX) window.PeteSFX.play("flip");
}
window.PeteCards = {
diff --git a/internal/web/static/js/casino-fx.js b/internal/web/static/js/casino-fx.js
index 6c978d5..0a4fc30 100644
--- a/internal/web/static/js/casino-fx.js
+++ b/internal/web/static/js/casino-fx.js
@@ -18,6 +18,14 @@
var reduced =
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ // The room's noise, if it's loaded. Everything that flies through this file
+ // makes a sound when it lands, which is how every table got sound at once
+ // without any of them being told about it: a chip is a chip wherever it's
+ // thrown from. A game that wants something more specific says so per call.
+ function sfx(name, opts) {
+ if (window.PeteSFX) window.PeteSFX.play(name, opts);
+ }
+
var layer = null;
function stage() {
if (!layer) {
@@ -107,13 +115,23 @@
// than averaging itself back down to nothing.
var sMid = Math.max(s0, s1) * 1.12;
+ var wait = reduced ? 0 : opts.delay || 0;
+
+ // It makes a noise when it lands, and the noise is scheduled on the audio
+ // clock for the moment it lands — not fired by a timer that goes off when the
+ // animation ends. A timer that drifts thirty milliseconds is a card you hear
+ // before you see, and that is worse than silence.
+ if (opts.sound) {
+ sfx(opts.sound, { delay: (wait + dur) / 1000, v: opts.index || 0 });
+ }
+
var anim = node.animate(
[
{ transform: t(a.x, a.y, s0, opts.fromSpin || 0), opacity: 1, offset: 0 },
{ transform: t(midX, midY, sMid, spin * 0.6), opacity: 1, offset: 0.5 },
{ transform: t(b.x, b.y, s1, spin), opacity: opts.fade ? 0 : 1, offset: 1 },
],
- { duration: dur, easing: "cubic-bezier(0.33, 0, 0.25, 1)", delay: reduced ? 0 : opts.delay || 0, fill: "both" }
+ { duration: dur, easing: "cubic-bezier(0.33, 0, 0.25, 1)", delay: wait, fill: "both" }
);
return anim.finished
@@ -127,6 +145,9 @@
// was — UNO wanted the same throw with a card in it, so the throw moved out.
function fly(from, to, opts) {
opts = opts || {};
+ // A chip clinks. Every table in the room bets, pays and sweeps through this
+ // one function, so saying it once here is every table having chip sounds.
+ if (opts.sound === undefined) opts = Object.assign({ sound: "chip" }, opts);
return flyNode(disc(opts.denom || 25), from, to, opts);
}
@@ -225,6 +246,7 @@
var n = amount == null ? api.amount : amount;
var left = api.amount - n;
if (n <= 0) return Promise.resolve();
+ sfx("sweep"); // the pile leaving the felt, under the chips landing
var chain = flyMany(els.spot, to, chipsFor(n, 8), Object.assign({ gap: 40, lift: 0.8 }, opts || {}));
api.render(left > 0 ? left : 0);
return chain;
@@ -233,8 +255,10 @@
return api;
}
- // burst: confetti out of a point. Saved for the things worth celebrating.
+ // burst: confetti out of a point. Saved for the things worth celebrating — so
+ // it is also, everywhere in the room, the sound of winning.
function burst(target, opts) {
+ sfx("win"); // before the reduced-motion bail: no confetti still deserves the fanfare
if (reduced) return Promise.resolve();
opts = opts || {};
var c = centre(target);
@@ -326,6 +350,7 @@
window.PeteFX = {
reduced: reduced,
+ sfx: sfx, // so a table can make a noise without reaching past this file
chipsFor: chipsFor,
disc: disc,
jitter: jitter,
diff --git a/internal/web/static/js/casino-sfx.js b/internal/web/static/js/casino-sfx.js
new file mode 100644
index 0000000..6a1ed09
--- /dev/null
+++ b/internal/web/static/js/casino-sfx.js
@@ -0,0 +1,265 @@
+// The noise the room makes.
+//
+// There are no audio files here and there is nothing to download. Every sound in
+// this casino is *made* — an oscillator, a burst of filtered noise, an envelope —
+// the same bargain the weather engine takes with its clouds. A card is a short
+// slap of noise through a bandpass; a chip is two detuned sines with a click on
+// the front; a win is four notes going up. It costs about six kilobytes and no
+// round trips, and it means a sound can be pitched, stretched and detuned per
+// call instead of being the same wav 300 times.
+//
+// Two rules hold the whole file up.
+//
+// **Nothing is built until it's asked for.** A browser will not let a page make a
+// noise before the user has touched it, and a page that builds an AudioContext on
+// load gets one in the "suspended" state and a warning in the console for its
+// trouble. So the context is made on the first sound anybody actually asks for —
+// which, in a casino, is a click on a chip.
+//
+// **Muted means silent, not quiet.** When the room is muted nothing is
+// constructed, nothing is scheduled and no context is opened. Mute is the
+// default-off switch for the entire file, checked before anything else happens.
+//
+// Exposed as window.PeteSFX. Nothing in here knows what blackjack is.
+(function () {
+ "use strict";
+
+ var KEY = "pete.sfx.off";
+
+ var ctx = null;
+ var master = null;
+ var noiseBuf = null;
+ var muted = false;
+ var listeners = [];
+
+ try { muted = localStorage.getItem(KEY) === "1"; } catch (e) {}
+
+ // ---- the instrument --------------------------------------------------------
+
+ function boot() {
+ if (ctx) return ctx;
+ var AC = window.AudioContext || window.webkitAudioContext;
+ if (!AC) return null;
+ try {
+ ctx = new AC();
+ } catch (e) {
+ return null;
+ }
+ master = ctx.createGain();
+ master.gain.value = 0.45; // the room is furniture, not a nightclub
+ master.connect(ctx.destination);
+ return ctx;
+ }
+
+ // A second of white noise, made once and played at different speeds and through
+ // different filters for the rest of the session. Card flicks, riffles, the
+ // transient on a chip: all of them are this buffer wearing a different coat.
+ function noise() {
+ if (noiseBuf) return noiseBuf;
+ var n = ctx.sampleRate;
+ noiseBuf = ctx.createBuffer(1, n, n);
+ var d = noiseBuf.getChannelData(0);
+ for (var i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
+ return noiseBuf;
+ }
+
+ // env is the shape of every sound in here: up fast, down slow, and never to
+ // zero — an exponential ramp to actual zero is undefined, and a browser that
+ // hits one either throws or clicks.
+ function env(g, t0, attack, decay, peak) {
+ g.gain.setValueAtTime(0.0001, t0);
+ g.gain.exponentialRampToValueAtTime(Math.max(0.0001, peak), t0 + attack);
+ g.gain.exponentialRampToValueAtTime(0.0001, t0 + attack + decay);
+ }
+
+ // tone is one note.
+ function tone(t0, freq, o) {
+ o = o || {};
+ var osc = ctx.createOscillator();
+ var g = ctx.createGain();
+ osc.type = o.type || "sine";
+ osc.frequency.setValueAtTime(freq, t0);
+ if (o.to) osc.frequency.exponentialRampToValueAtTime(o.to, t0 + (o.glide || o.decay || 0.1));
+ env(g, t0, o.attack || 0.004, o.decay || 0.12, o.gain == null ? 0.3 : o.gain);
+ osc.connect(g).connect(master);
+ osc.start(t0);
+ osc.stop(t0 + (o.attack || 0.004) + (o.decay || 0.12) + 0.02);
+ }
+
+ // hiss is a burst of the noise buffer through a filter, which is what a card,
+ // a riffle and the knock on a chip all are.
+ function hiss(t0, o) {
+ o = o || {};
+ var src = ctx.createBufferSource();
+ src.buffer = noise();
+ src.playbackRate.value = o.rate || 1;
+
+ var f = ctx.createBiquadFilter();
+ f.type = o.filter || "bandpass";
+ f.frequency.setValueAtTime(o.freq || 2000, t0);
+ if (o.sweepTo) f.frequency.exponentialRampToValueAtTime(o.sweepTo, t0 + (o.decay || 0.1));
+ f.Q.value = o.q == null ? 1.1 : o.q;
+
+ var g = ctx.createGain();
+ env(g, t0, o.attack || 0.003, o.decay || 0.09, o.gain == null ? 0.3 : o.gain);
+
+ src.connect(f).connect(g).connect(master);
+ src.start(t0);
+ src.stop(t0 + (o.attack || 0.003) + (o.decay || 0.09) + 0.02);
+ }
+
+ // ---- the sounds ------------------------------------------------------------
+ //
+ // Each one takes the time it starts at, and a `v` — a small per-call variation,
+ // usually the index of the card or chip in a run. Four cards dealt with the
+ // identical sample four times is a machine gun; four cards each a semitone off
+ // is a hand dealing.
+
+ var SOUNDS = {
+ // A card landing: mostly air, with a slap on the front of it.
+ card: function (t, v) {
+ hiss(t, { freq: 1800 + v * 140, q: 0.9, decay: 0.075, rate: 1 + v * 0.05, gain: 0.3 });
+ hiss(t, { filter: "highpass", freq: 4200, decay: 0.02, gain: 0.16 });
+ },
+
+ // Dealing is the same card, lower and softer: it's being placed, not thrown.
+ deal: function (t, v) {
+ hiss(t, { freq: 1250 + v * 110, q: 1.1, decay: 0.085, rate: 0.9, gain: 0.24 });
+ },
+
+ // A card turning over — shorter, brighter, and it comes in two halves, which
+ // is what a flip actually sounds like.
+ flip: function (t) {
+ hiss(t, { freq: 2600, q: 1.4, decay: 0.035, gain: 0.22 });
+ hiss(t + 0.045, { freq: 1500, q: 1.0, decay: 0.06, gain: 0.24 });
+ },
+
+ // The riffle. One long sweep of noise with the filter climbing through it,
+ // and a rattle laid over the top so it isn't just a shhh.
+ shuffle: function (t) {
+ hiss(t, { freq: 700, sweepTo: 3400, q: 0.7, attack: 0.02, decay: 0.34, gain: 0.2 });
+ for (var i = 0; i < 9; i++) {
+ hiss(t + 0.03 + i * 0.032, { freq: 2400 + i * 130, q: 2.2, decay: 0.02, gain: 0.09 });
+ }
+ },
+
+ // A chip landing on a chip. Two detuned partials well above the staff, with a
+ // knock underneath — the knock is most of what makes it read as *clay* rather
+ // than as a bell.
+ chip: function (t, v) {
+ var f = 1750 + v * 55;
+ tone(t, f, { type: "sine", decay: 0.07, gain: 0.16 });
+ tone(t, f * 1.34, { type: "sine", decay: 0.05, gain: 0.1 });
+ hiss(t, { filter: "lowpass", freq: 900, decay: 0.03, gain: 0.3, q: 0.5 });
+ },
+
+ // Chips sliding away across felt.
+ sweep: function (t) {
+ hiss(t, { freq: 1600, sweepTo: 500, q: 0.6, attack: 0.015, decay: 0.26, gain: 0.16 });
+ },
+
+ // Four notes up. This is the only sound in the room allowed to be pleased.
+ win: function (t) {
+ [523.25, 659.25, 783.99, 1046.5].forEach(function (f, i) {
+ tone(t + i * 0.085, f, { type: "triangle", decay: 0.26, gain: 0.24 });
+ });
+ },
+
+ // Two notes down, and the second one is flat. Nobody needs telling twice.
+ lose: function (t) {
+ tone(t, 311.13, { type: "triangle", decay: 0.24, gain: 0.22 });
+ tone(t + 0.15, 233.08, { type: "triangle", decay: 0.42, gain: 0.22 });
+ },
+
+ // Nothing happened and nothing was lost.
+ push: function (t) {
+ tone(t, 392, { type: "sine", decay: 0.2, gain: 0.16 });
+ },
+
+ // A button. Short enough that you feel it rather than hear it.
+ blip: function (t) {
+ tone(t, 880, { type: "sine", decay: 0.055, gain: 0.12 });
+ },
+
+ // UNO, called. A bright stab plus a zip upwards: it is a shout, and it is the
+ // one thing on that table you want to feel good about pressing.
+ uno: function (t) {
+ [659.25, 830.61, 987.77].forEach(function (f) {
+ tone(t, f, { type: "triangle", decay: 0.3, gain: 0.2 });
+ });
+ tone(t, 500, { type: "sawtooth", to: 1600, glide: 0.16, decay: 0.18, gain: 0.09 });
+ },
+
+ // Got them. A rising snap with a thump on the end — the sound of a finger
+ // being pointed.
+ catch: function (t) {
+ tone(t, 300, { type: "square", to: 900, glide: 0.11, decay: 0.12, gain: 0.13 });
+ hiss(t + 0.1, { filter: "lowpass", freq: 1100, decay: 0.1, gain: 0.34, q: 0.7 });
+ tone(t + 0.1, 174.61, { type: "triangle", decay: 0.22, gain: 0.2 });
+ },
+
+ // Something bad landed on you: a stack, a +4, a catch you walked into.
+ bad: function (t) {
+ tone(t, 155.56, { type: "square", to: 98, glide: 0.18, decay: 0.22, gain: 0.14 });
+ hiss(t, { filter: "lowpass", freq: 700, decay: 0.14, gain: 0.3, q: 0.6 });
+ },
+
+ // A clock you can hear. Used sparingly, and never in a run of more than a few.
+ tick: function (t) {
+ hiss(t, { freq: 3200, q: 3, decay: 0.02, gain: 0.14 });
+ },
+ };
+
+ // ---- the door --------------------------------------------------------------
+
+ // A browser will not make a noise until the user has touched the page, and a
+ // context built before that arrives suspended. This wakes it on the first real
+ // gesture — after which play() can schedule freely.
+ function wake() {
+ if (ctx && ctx.state === "suspended") ctx.resume().catch(function () {});
+ }
+ ["pointerdown", "keydown", "touchstart"].forEach(function (ev) {
+ window.addEventListener(ev, wake, { passive: true });
+ });
+
+ // play makes a sound, or — if the room is muted — makes nothing at all, opens no
+ // context and builds no graph.
+ //
+ // `v` is the variation: pass the index of the card or chip in a run and the
+ // sound moves a little each time, which is the difference between a hand and a
+ // machine. `delay` schedules it ahead in seconds, so a caller that knows a card
+ // lands in 400ms can say so rather than sleeping.
+ function play(name, opts) {
+ if (muted) return;
+ var s = SOUNDS[name];
+ if (!s) return;
+ if (!boot()) return;
+ wake();
+ if (ctx.state !== "running") return; // not yet touched: no sound, and no error
+ try {
+ s(ctx.currentTime + ((opts && opts.delay) || 0), ((opts && opts.v) || 0));
+ } catch (e) {
+ /* a sound is never worth throwing over */
+ }
+ }
+
+ function setMuted(on) {
+ muted = !!on;
+ try {
+ if (muted) localStorage.setItem(KEY, "1");
+ else localStorage.removeItem(KEY);
+ } catch (e) {}
+ if (window.PetePrefs) window.PetePrefs.push();
+ listeners.forEach(function (fn) { fn(muted); });
+ if (!muted) play("blip"); // so you know what you just turned back on
+ }
+
+ window.PeteSFX = {
+ play: play,
+ muted: function () { return muted; },
+ toggle: function () { setMuted(!muted); return muted; },
+ set: setMuted,
+ // onChange lets the mute button re-label itself without owning the state.
+ onChange: function (fn) { listeners.push(fn); fn(muted); },
+ };
+})();
diff --git a/internal/web/static/js/games.js b/internal/web/static/js/games.js
index 7aefbb0..fc3c713 100644
--- a/internal/web/static/js/games.js
+++ b/internal/web/static/js/games.js
@@ -154,6 +154,25 @@
});
}
+ // The room's volume. The button lives in the chip bar, which is the one thing
+ // every table has — the sound belongs to the room, not to any one game — and
+ // the state itself belongs to PeteSFX, which persists it. This only draws it.
+ var sfxBtn = document.querySelector("[data-sfx-toggle]");
+ if (sfxBtn && window.PeteSFX) {
+ var onIcon = sfxBtn.querySelector("[data-sfx-on]");
+ var offIcon = sfxBtn.querySelector("[data-sfx-off]");
+ var sfxLbl = sfxBtn.querySelector("[data-sfx-label]");
+
+ window.PeteSFX.onChange(function (muted) {
+ sfxBtn.setAttribute("aria-pressed", muted ? "true" : "false");
+ sfxBtn.title = muted ? "Sound is off" : "Sound is on";
+ if (onIcon) onIcon.classList.toggle("hidden", muted);
+ if (offIcon) offIcon.classList.toggle("hidden", !muted);
+ if (sfxLbl) sfxLbl.textContent = muted ? "Turn the sound on" : "Mute the room";
+ });
+ sfxBtn.addEventListener("click", function () { window.PeteSFX.toggle(); });
+ }
+
window.PeteGames = {
// onUpdate registers a listener called on every fresh view of the money.
onUpdate: function (fn) { listeners.push(fn); if (view) fn(view); },
diff --git a/internal/web/static/js/hangman.js b/internal/web/static/js/hangman.js
index 0ee4846..ba8953e 100644
--- a/internal/web/static/js/hangman.js
+++ b/internal/web/static/js/hangman.js
@@ -117,6 +117,9 @@
at.forEach(function (i, n) {
var t = boardEl.querySelector('.pete-tile[data-at="' + i + '"]');
if (!t) return;
+ // One note per tile, climbing: a letter that turns up three times should
+ // sound like it's worth three times as much, because it is.
+ FX.sfx("tick", { delay: pace(n * 90) / 1000, v: n });
setTimeout(function () {
// Left as it comes: the tile is uppercased in CSS, and doing it here too
// would mean the resume path (which paints the phrase's own casing) and
@@ -126,6 +129,7 @@
t.classList.add("pete-tile-hit");
}, pace(n * 90));
});
+ FX.sfx("flip");
return wait(FLIP_MS + (at.length - 1) * 90);
}
@@ -292,6 +296,8 @@
// Confetti for a phrase guessed outright — the one call you make on your own
// rather than by grinding the alphabet.
if (v.outcome === "solved" && v.net > 0) FX.burst(verdictEl, { count: 30 });
+ else if (v.net > 0) FX.sfx("win");
+ else if (v.net < 0) FX.sfx("lose");
}
function setPhase(v) {
@@ -362,6 +368,7 @@
// they are one event: this is what a wrong guess costs, all of it.
drawGallows(countMisses(events, e), true);
shake();
+ FX.sfx("bad");
if (final) knock(final);
return wait(MISS_MS);
diff --git a/internal/web/static/js/holdem.js b/internal/web/static/js/holdem.js
index 5ba6d87..01606ba 100644
--- a/internal/web/static/js/holdem.js
+++ b/internal/web/static/js/holdem.js
@@ -301,6 +301,7 @@
pot.render(0);
potTotal.textContent = "0";
sideEl.classList.add("hidden");
+ FX.sfx("shuffle");
return wait(140);
case "rebuy":
@@ -377,6 +378,7 @@
var face = (i === 0 && s.cards) ? s.cards[round] : null;
var card = Cards.el(face, { deal: true, tilt: i !== 0 });
built.cards.appendChild(card);
+ FX.sfx("deal", { delay: 0.07 * i, v: i });
beats.push(wait(70 * i));
});
return Promise.all(beats).then(function () { return wait(180); });
@@ -394,10 +396,12 @@
if (e.text === "fold") {
s.root.dataset.state = "folded";
s.cards.dataset.mucked = "1";
+ FX.sfx("card", { v: 2 }); // cards going into the muck
return wait(320);
}
if (e.text === "check") {
flash(s.root);
+ FX.sfx("blip"); // a knuckle on the table
return wait(320);
}
// call, raise, allin: chips leave their stack for their spot.
@@ -437,9 +441,10 @@
// The board turns one card at a time, even the flop. Three cards appearing
// at once is a screenshot; three cards appearing in a row is a flop.
var chain = Promise.resolve();
- (e.cards || []).forEach(function (c) {
+ (e.cards || []).forEach(function (c, i) {
chain = chain.then(function () {
boardEl.appendChild(Cards.el(c, { deal: true, tilt: false }));
+ FX.sfx("card", { v: i });
return wait(240);
});
});
@@ -478,12 +483,15 @@
});
if (busted) {
show("You're out of chips. Sit down again when you're ready.", "lose");
+ FX.sfx("lose");
return;
}
if (!events.some(function (e) { return e.kind === "end"; })) return;
var me = final.seats[0];
if (won > 0) {
+ // The pot coming your way already burst (and so already cheered) back in
+ // the "win" event. This is only the words.
show(showed
? "You win " + money(won) + " with " + article(handName(events)) + "."
: "They folded. You take " + money(won) + ".", "win");
@@ -491,6 +499,7 @@
show("Folded.", "lose");
} else {
show("No good this time.", "lose");
+ FX.sfx("lose");
}
function show(text, tone) {
diff --git a/internal/web/static/js/prefs.js b/internal/web/static/js/prefs.js
index e4e1d9c..4723a6c 100644
--- a/internal/web/static/js/prefs.js
+++ b/internal/web/static/js/prefs.js
@@ -10,7 +10,7 @@
(function () {
// The localStorage keys we sync. The weather *cache* is deliberately excluded:
// it's transient and per-device.
- var SYNCED = ["pete.disabledSources.v1", "pete.weather.loc.v1", "pete-weather-off"];
+ var SYNCED = ["pete.disabledSources.v1", "pete.weather.loc.v1", "pete-weather-off", "pete.sfx.off"];
var user = window.PETE_USER || null;
var serverPrefs = window.PETE_PREFS || null;
diff --git a/internal/web/static/js/solitaire.js b/internal/web/static/js/solitaire.js
index d447130..ba1f52a 100644
--- a/internal/web/static/js/solitaire.js
+++ b/internal/web/static/js/solitaire.js
@@ -396,6 +396,36 @@
});
}
+ // noises is the board's soundtrack, walked off the *same* STEP_MS ladder the
+ // animation is walked off. That matters more than which sound goes where: a
+ // card you hear land a step before it lands is worse than a card that lands in
+ // silence, so this counts its way through the script exactly as planOf and
+ // flashHome do, and any change to their timing has to be made here too.
+ function noises(events) {
+ var at = 0;
+ (events || []).forEach(function (e) {
+ switch (e.kind) {
+ case "draw":
+ FX.sfx("card", { delay: at / 1000 });
+ break;
+ case "recycle":
+ FX.sfx("shuffle", { delay: at / 1000 });
+ break;
+ case "move":
+ FX.sfx("deal", { delay: (at + MOVE_MS) / 1000 });
+ at += STEP_MS;
+ break;
+ case "home":
+ // A card reaching a foundation is the only move that pays, so it is the
+ // only one that gets a note on the end of it.
+ FX.sfx("deal", { delay: (at + MOVE_MS) / 1000 });
+ FX.sfx("blip", { delay: (at + MOVE_MS + 60) / 1000 });
+ at += STEP_MS;
+ break;
+ }
+ });
+ }
+
// ---- the money -------------------------------------------------------------
// bank moves the spot to what the server says the board is worth. Up is chips
@@ -548,6 +578,9 @@
// Clearing 52 cards out of a Vegas deal is the rarest thing that happens in
// this room, so it's the one that gets the confetti.
if (v.outcome === "cleared") FX.burst(verdictEl, { count: 40 });
+ else if (v.net > 0) FX.sfx("win");
+ else if (v.net < 0) FX.sfx("lose");
+ else FX.sfx("push");
}
// play walks a server response onto the felt: the board is re-rendered, the
@@ -570,6 +603,7 @@
render(v);
var plan = planOf(events);
flashHome(events);
+ noises(events);
return animate(before, plan);
})
.then(function () {
diff --git a/internal/web/static/js/trivia.js b/internal/web/static/js/trivia.js
index 1ccdb40..7e0c454 100644
--- a/internal/web/static/js/trivia.js
+++ b/internal/web/static/js/trivia.js
@@ -372,10 +372,16 @@
renderQuestion(final);
renderLadder(final);
if (final && final.phase === "playing") startClock(final.left, final.limit);
+ FX.sfx("blip");
return;
case "right":
reveal(e.choice, e.correct);
+ // Three notes up, one per rung climbed — the ladder, in sound.
+ [0, 0.09, 0.18].forEach(function (d, i) {
+ FX.sfx("tick", { delay: d, v: i });
+ });
+ FX.sfx("win", { delay: 0.1 });
if (final) {
// The rung lighting and the multiple climbing are one event,
// because they are one event: this is what the answer was worth.
@@ -387,10 +393,13 @@
case "wrong":
reveal(e.choice, e.correct);
+ FX.sfx("bad");
return wait(1100);
case "timeout":
+ // The clock beat you to it, which is a different kind of bad.
reveal(-1, e.correct);
+ FX.sfx("lose");
return wait(1100);
case "settle":
diff --git a/internal/web/static/js/uno.js b/internal/web/static/js/uno.js
index 63430eb..0427d0e 100644
--- a/internal/web/static/js/uno.js
+++ b/internal/web/static/js/uno.js
@@ -39,6 +39,9 @@
var tableEl = root.querySelector("[data-table-name]");
var wildEl = root.querySelector("[data-wild]");
+ var gateEl = root.querySelector("[data-unogate]");
+ var callBtn = root.querySelector("[data-uno-call]");
+ var timerEl = root.querySelector("[data-uno-timer]");
// Not [data-pending]: that is the chip bar's, it lives inside this root, and it
// comes first in the document. See the note in uno.html.
var billEl = root.querySelector("[data-bill]");
@@ -72,6 +75,14 @@
var played = -1; // the card you just played, so the script lifts that one out
// of the hand and not merely the first one that lit up
+ // The UNO call. CALL_MS is how long you get to say it once the card is on its
+ // way down — long enough that nobody playing the game misses it, short enough
+ // that it is still a thing you have to *do*. The engine has the other half of
+ // this rule (see call.go); this is only the window.
+ var CALL_MS = 2600;
+ var gate = null; // the play waiting on a call: {index, color}
+ var gateTimer = 0;
+
var reduced = FX.reduced;
function pace(ms) { return reduced ? 0 : ms; }
function wait(ms) { return new Promise(function (r) { setTimeout(r, pace(ms)); }); }
@@ -207,6 +218,9 @@
function renderSeats(v) {
seatsEl.innerHTML = "";
if (!v) return;
+ var catchable = {};
+ (v.catchable || []).forEach(function (i) { catchable[i] = true; });
+
v.seats.forEach(function (s, i) {
if (s.you) return; // you are the hand at the bottom, not a seat up here
var el = document.createElement("div");
@@ -216,6 +230,24 @@
if (s.uno) el.dataset.uno = "1";
if (s.out) el.dataset.out = "1"; // No Mercy buried them; they are not coming back
+ // One card, and it never said so. This button is the only thing on the felt
+ // that gives it away — a bot that forgets to call is silent by definition,
+ // and the tell was always meant to be the count beside its fan. The button
+ // is the table doing you the courtesy of putting a target on it.
+ if (catchable[i]) {
+ var got = document.createElement("button");
+ got.type = "button";
+ got.className = "pete-uno-catch";
+ got.dataset.catch = String(i);
+ got.textContent = "Catch!";
+ got.setAttribute("aria-label", "Catch " + s.name + " — one card, never called");
+ got.addEventListener("click", function (e) {
+ e.stopPropagation();
+ catchSeat(i);
+ });
+ el.appendChild(got);
+ }
+
var fan = document.createElement("div");
fan.className = "pete-uno-fan";
var show = Math.min(s.cards, FAN);
@@ -252,23 +284,65 @@
return el ? el.querySelector(".pete-uno-fan") : deckEl;
}
- function renderHand(v) {
- handEl.innerHTML = "";
- if (!v || !v.hand) return;
- var playable = {};
- (v.playable || []).forEach(function (i) { playable[i] = true; });
- var yours = v.turn === 0 && v.phase !== "done";
+ // shownHand is the faces currently on screen. paintHand diffs against it so that
+ // only cards that are actually new fan in — without it, every redraw re-deals
+ // the whole hand in front of you, and a hand gets redrawn several times a lap.
+ var shownHand = [];
- v.hand.forEach(function (c, i) {
+ function key(c) { return c.color + "/" + c.value; }
+
+ // paintHand draws your fan. `live` is whether the cards are yours to click,
+ // which mid-script they are not: the game on screen is one the server has
+ // already moved past, and a card clicked during the lap would be a move against
+ // a board that no longer exists.
+ function paintHand(cards, playable, live) {
+ cards = cards || [];
+ // What was already on the table stays put. Matched by face and consumed, so a
+ // hand holding two red fives doesn't count one of them twice.
+ var pool = shownHand.map(key);
+ handEl.innerHTML = "";
+ var fresh = 0;
+
+ cards.forEach(function (c, i) {
var el = card(c, { button: true });
- el.style.setProperty("--i", i);
el.dataset.at = String(i);
- var ok = yours && playable[i];
+ var ok = !!live && !!playable[i];
el.dataset.on = ok ? "1" : "0";
el.disabled = !ok || busy;
if (ok) el.addEventListener("click", function () { pick(i, c); });
+ var was = pool.indexOf(key(c));
+ if (was >= 0) {
+ pool.splice(was, 1);
+ el.dataset.settled = "1"; // it was already in your hand: don't deal it again
+ } else {
+ el.style.setProperty("--i", fresh++);
+ }
handEl.appendChild(el);
});
+ shownHand = cards.slice();
+ }
+
+ function renderHand(v) {
+ if (!v || !v.hand) { handEl.innerHTML = ""; shownHand = []; return; }
+ var playable = {};
+ (v.playable || []).forEach(function (i) { playable[i] = true; });
+ paintHand(v.hand, playable, v.turn === 0 && v.phase !== "done");
+ }
+
+ // showHand redraws your fan from the hand the server stamped on the event that
+ // just changed it — see Event.Hand in the engine.
+ //
+ // This is the fix for a hand that used to sit there stale for the whole lap.
+ // bump() keeps the *bots'* fans honest and has always refused seat zero, so
+ // when a +4 landed on you at the top of a lap you watched four backs fly into
+ // your hand and then nothing: the cards themselves only turned up seconds
+ // later, when the script ended and paint() finally ran. You were looking at a
+ // hand you no longer held.
+ function showHand(cards) {
+ if (!cards) return;
+ paintHand(cards, {}, false);
+ // And the line above the fan counts what's in it, so it moves too.
+ countEl.textContent = cards.length + (cards.length === 1 ? " card — UNO!" : " cards");
}
function renderPiles(v) {
@@ -357,7 +431,10 @@
else if (v.net < 0) text += " " + v.net.toLocaleString();
verdictEl.textContent = text;
verdictEl.classList.remove("hidden");
+ // burst() is the room's win sound as well as its confetti; anything else is
+ // a night that didn't go your way.
if (v.outcome === "won") FX.burst(verdictEl, { count: 34 });
+ else FX.sfx("lose");
}
// controls is the one place that decides what you can click: whose turn the
@@ -373,6 +450,11 @@
handEl.querySelectorAll(".pete-uno-card").forEach(function (b) {
b.disabled = busy || !yours || b.dataset.on !== "1";
});
+ // Catching is free, but it is still a move: it can't go up while one of yours
+ // is in flight, and it can't go up on somebody else's turn.
+ seatsEl.querySelectorAll("[data-catch]").forEach(function (b) {
+ b.disabled = busy || !yours;
+ });
// Under a stack you cannot draw. The deck is not a way out of a bill: the two
// moves that exist are answering it with a draw card or taking the lot.
if (drawBtn) drawBtn.disabled = busy || !yours || drawn || stack;
@@ -398,6 +480,7 @@
betting.classList.toggle("hidden", live);
playing.classList.toggle("hidden", !live);
hideWild();
+ hideGate();
if (drawBtn) drawBtn.classList.toggle("hidden", drawn || stack);
if (takeBtn) takeBtn.classList.toggle("hidden", !stack);
@@ -406,7 +489,13 @@
// this is the table not offering a button that cannot work.
var canPass = drawn && !(v && v.tier && v.tier.no_mercy);
if (passBtn) passBtn.classList.toggle("hidden", !canPass);
- if (hintEl && live) hintEl.textContent = HINTS[v.phase] || HINTS.play;
+ if (hintEl && live) {
+ // A seat sitting on one card it never called is the most valuable thing on
+ // the table, and it is worth more than whatever the phase was going to say.
+ hintEl.textContent = (v.catchable || []).length
+ ? "Somebody's down to one card and never said so. Catch them before you play."
+ : (HINTS[v.phase] || HINTS.play);
+ }
if (!live && v) rulesFor(v); // it's over: leave the panel where the game was
controls();
if (!v || !v.outcome) verdictEl.classList.add("hidden");
@@ -426,7 +515,9 @@
// ---- the script ------------------------------------------------------------
- // throwCard sends a card from one place to another across the felt.
+ // throwCard sends a card from one place to another across the felt. It lands
+ // with a slap — "card" for one going down on the pile, "deal" (softer, lower)
+ // for one coming off the deck into a hand, which is a card being *placed*.
function throwCard(node, from, to, opts) {
opts = opts || {};
return FX.flyNode(node, from, to, {
@@ -435,12 +526,17 @@
spin: opts.spin == null ? FX.jitter((opts.index || 0) + 3, 14) : opts.spin,
fromScale: opts.fromScale == null ? 0.9 : opts.fromScale,
delay: opts.delay || 0,
+ index: opts.index || 0,
+ sound: opts.sound === undefined ? "card" : opts.sound,
});
}
// bump keeps a bot's fan honest during the script: the server's count is the
// truth, but between events the fan has to grow and shrink as cards move, or
// the flight lands on a pile that hasn't changed.
+ //
+ // Seat zero is not a bot and is not bumped — a fan of backs is no use to you,
+ // you want the faces. showHand does yours, from the hand the event carries.
function bump(seat, left) {
if (seat === 0 || left == null) return;
var el = seatEl(seat);
@@ -499,6 +595,10 @@
// The deal isn't animated card by card: seven cards to each of four
// seats is 28 flights and a player who wants to play. The hand fans in
// on its own (CSS), which reads as being dealt without taking as long.
+ // The sound does the same job: a riffle, then cards landing, rather
+ // than twenty-eight separate slaps.
+ FX.sfx("shuffle");
+ for (var c = 0; c < 5; c++) FX.sfx("deal", { delay: 0.28 + c * 0.06, v: c });
paint(final);
return wait(320);
@@ -525,6 +625,7 @@
colourEl.dataset.c = e.color;
feltEl.dataset.c = e.color;
}
+ showHand(e.hand); // the card has landed; close the gap it left
return wait(e.seat === 0 ? 120 : 300);
});
}
@@ -535,14 +636,16 @@
var to = seatAnchor(e.seat);
var backs = [];
for (var i = 0; i < Math.min(e.n, 4); i++) {
- backs.push(throwCard(back(), deckEl, to, { index: i, delay: i * 90 }));
+ backs.push(throwCard(back(), deckEl, to, { index: i, delay: i * 90, sound: "deal" }));
}
var punished = e.kind === "forced";
+ if (punished) FX.sfx("bad");
// A forced draw is also how a stack ends: somebody stopped answering
// and paid the bill, so the bill is gone.
if (punished) { pending(0); badge(e.seat, "+" + e.n, "bad"); }
return Promise.all(backs).then(function () {
bump(e.seat, e.left);
+ showHand(e.hand); // your cards, face up, as they land
deckCntEl.textContent = String(Math.max(0, parseInt(deckCntEl.textContent, 10) - e.n));
// Your own drawn card comes face up — it's yours, and the server
// sent its face for exactly this.
@@ -551,12 +654,58 @@
});
}
+ // ---- the call, and the catch ---------------------------------------
+ //
+ // Somebody went down to one card without saying so, and somebody else
+ // noticed. Two cards off the deck, and the seat that took them wears the
+ // name of whoever spotted it — because "caught" with nobody attached is
+ // just a mystery.
+ case "caught": {
+ spotlight(e.seat);
+ var mine = e.seat === 0;
+ var caughtBy = (game && game.seats && game.seats[e.by]) || {};
+ var whom = mine
+ ? "Caught! " + (caughtBy.name || "They") + " saw it"
+ : "Caught! +" + e.n;
+ // Catching a bot is a pointed finger; being caught is a thud.
+ FX.sfx(mine ? "bad" : "catch");
+ var lands = [];
+ var at = seatAnchor(e.seat);
+ for (var k = 0; k < e.n; k++) {
+ lands.push(throwCard(back(), deckEl, at, { index: k, delay: k * 100, sound: "deal" }));
+ }
+ badge(e.seat, whom, "bad");
+ return Promise.all(lands).then(function () {
+ bump(e.seat, e.left);
+ showHand(e.hand);
+ deckCntEl.textContent = String(Math.max(0, parseInt(deckCntEl.textContent, 10) - e.n));
+ return wait(520);
+ });
+ }
+
+ // You called a seat that had nothing to hide. Two cards, and they're
+ // yours: the price of the catch button not being a thing you just mash.
+ case "miscall": {
+ FX.sfx("bad");
+ var wrong = [];
+ for (var w = 0; w < e.n; w++) {
+ wrong.push(throwCard(back(), deckEl, handEl, { index: w, delay: w * 100, sound: "deal" }));
+ }
+ badge(0, "They called it. +" + e.n, "bad");
+ return Promise.all(wrong).then(function () {
+ showHand(e.hand);
+ deckCntEl.textContent = String(Math.max(0, parseInt(deckCntEl.textContent, 10) - e.n));
+ return wait(520);
+ });
+ }
+
case "stack":
// The bill has moved on to somebody, and N is what it stands at now —
// not what was just added to it. A +2 answered with a +10 is a seat
// looking at twelve cards.
pending(e.n);
spotlight(e.seat);
+ FX.sfx("bad");
return badge(e.seat, "+" + e.n, "bad").then(function () { return wait(140); });
case "skipall":
@@ -580,7 +729,10 @@
dumped.push(throwCard(back(), dumpFrom, discardEl, { index: d, delay: d * 70 }));
}
badge(e.seat, "−" + e.n + " " + (e.color || ""));
- return Promise.all(dumped).then(function () { return wait(220); });
+ return Promise.all(dumped).then(function () {
+ showHand(e.hand);
+ return wait(220);
+ });
}
case "roulette": {
@@ -595,6 +747,7 @@
badge(e.seat, "Roulette +" + e.n, "bad");
return Promise.all(spun).then(function () {
bump(e.seat, e.left);
+ showHand(e.hand);
deckCntEl.textContent = String(Math.max(0, parseInt(deckCntEl.textContent, 10) - e.n));
return wait(400);
});
@@ -606,7 +759,9 @@
var grave = seatEl(e.seat);
if (grave) grave.dataset.out = "1";
bump(e.seat, 0);
+ showHand(e.hand); // if it was you, the hand goes with you
pending(0); // a dead seat pays no bill and leaves none behind
+ FX.sfx("bad");
return badge(e.seat, "Buried on " + e.n, "bad").then(function () { return wait(460); });
}
@@ -618,6 +773,7 @@
return badge(e.seat, "Reverse").then(function () { return wait(60); });
case "uno":
+ FX.sfx("uno");
return badge(e.seat, "UNO!", "uno");
case "reshuffle":
@@ -626,6 +782,7 @@
// and counting down from a deck that still reads empty gets nowhere.
deckCntEl.textContent = String(e.n);
deckEl.classList.add("pete-uno-shuffle");
+ FX.sfx("shuffle");
return wait(420).then(function () {
deckEl.classList.remove("pete-uno-shuffle");
});
@@ -699,8 +856,72 @@
function pick(i, c) {
if (busy || !game || game.phase === "done" || game.turn !== 0) return;
- if (c.wild) { askColour(i); return; }
- move({ kind: "play", index: i });
+ if (c.wild) { askColour(i); return; } // the colour first; the call comes after
+ offer(i, 0);
+ }
+
+ // offer is the last thing between a card and the pile. If playing it leaves you
+ // holding one card, the table wants a word out of you first.
+ //
+ // Which cards those are is the server's answer, not a count of your hand: No
+ // Mercy's "discard all" takes every card of its colour with it, so a six-card
+ // hand can land on one, and a browser subtracting one from six would let you
+ // walk into a catch it never warned you about.
+ function offer(i, color) {
+ if (game && (game.uno_at || []).indexOf(i) >= 0) {
+ openGate(i, color);
+ return;
+ }
+ move({ kind: "play", index: i, color: color });
+ }
+
+ function openGate(i, color) {
+ gate = { index: i, color: color };
+ gateEl.classList.remove("hidden");
+ if (timerEl) {
+ // Restart the drain. The bar is a CSS animation and this element gets
+ // reused, so without kicking the browser between the two assignments it
+ // stays where the last one finished — a full bar that never moves, which
+ // is worse than no bar at all.
+ timerEl.style.animation = "none";
+ void timerEl.offsetWidth;
+ timerEl.style.animation = "";
+ timerEl.style.animationDuration = CALL_MS + "ms";
+ }
+ if (callBtn) callBtn.focus();
+ gateTimer = setTimeout(function () { shut(false); }, CALL_MS);
+ }
+
+ // shut plays the card the gate was holding, with or without the word. Letting
+ // the clock run out is a real answer — it is the answer that gets you caught.
+ function shut(called) {
+ if (!gate) return;
+ var g = gate;
+ gate = null;
+ clearTimeout(gateTimer);
+ gateTimer = 0;
+ gateEl.classList.add("hidden");
+ move({ kind: "play", index: g.index, color: g.color, uno: called });
+ }
+
+ // hideGate drops the gate without playing anything. Only for the paths that tear
+ // the board down under it (an error, a resume) — a gate that outlived its game
+ // would play a card into the next one.
+ function hideGate() {
+ if (gateTimer) clearTimeout(gateTimer);
+ gateTimer = 0;
+ gate = null;
+ if (gateEl) gateEl.classList.add("hidden");
+ }
+
+ if (callBtn) callBtn.addEventListener("click", function () { shut(true); });
+
+ // Catching a seat that went quiet. It is not a turn — right or wrong, the turn
+ // stays with you — but it is a move, so it goes through the same door as one.
+ function catchSeat(seat) {
+ if (busy || !game || game.phase === "done" || game.turn !== 0) return;
+ if ((game.catchable || []).indexOf(seat) < 0) return;
+ move({ kind: "catch", seat: seat });
}
function askColour(i) {
@@ -724,7 +945,7 @@
var i = pendingWild;
var c = COLOURS[b.dataset.colourPick];
hideWild();
- move({ kind: "play", index: i, color: c });
+ offer(i, c); // named the colour; now, is it your last but one?
});
});
diff --git a/internal/web/templates/_chipbar.html b/internal/web/templates/_chipbar.html
index 54774ba..62eb655 100644
--- a/internal/web/templates/_chipbar.html
+++ b/internal/web/templates/_chipbar.html
@@ -37,6 +37,25 @@
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
Cash out
+
+
+
diff --git a/internal/web/templates/games_layout.html b/internal/web/templates/games_layout.html
index 0be30f0..097cfaf 100644
--- a/internal/web/templates/games_layout.html
+++ b/internal/web/templates/games_layout.html
@@ -73,6 +73,11 @@
Play for what you can lose. Cash out whenever you like.
+
+
{{block "scripts" .}}{{end}}