skills·pokerskills·poker
sign inpoker room →
engineering notebook // mobile chrome

designing the mobile table

the felt already fits any screen. everything around it didn’t. this is the design story of the phone chrome — three chat states stolen honestly from Twitch, one pager for everything, and a table that never transforms away. then somebody held an actual phone, and ten rounds of findings rewrote half of it. both halves are here, and the demo below is the second one.

01 the problem

432 pixels of chrome on a 390-pixel phone

The felt itself is solved — it reshapes to any box, seats spaced by footprint, chrome bands reserved by the geometry. But the chrome around the felt was born on a desktop: two fixed side panels (224px + 208px), absolutely positioned, plus a 288px advisor column. On a phone the panels total 432px of a 390px screen. They overlap each other, cover the entire table, and steal every tap meant for a seat. The live table — the thing the whole site exists to show you — was effectively desktop-only.

the table was never the problem. the furniture was.
02 prior art

steal from the people who solved it

A live poker table on a phone is a live stream with money on it — so we took the layout from the platform that solved live-on-a-phone: Twitch. Portrait: the video pins to the top, chat scrolls beneath it, the input rides the bottom. Landscape: chat docks as a right column beside the video — never underneath, never over it. The video is sacred; the chat negotiates for whatever is left.

Swap “video” for “felt” and the whole design falls out. The chrome has exactly three states, and every one of them keeps the felt on screen:

the three chat states
watching  ·  panel closed — messages float over the felt
open      ·  portrait: felt top, pager below  /  landscape: right column
composing ·  keyboard up — the stack compresses, the felt stays
Watching is the default. Opening the panel is one deliberate morph. Composing is not a new layout at all — just less room, honestly divided.
03 one surface

chat, history, thoughts — one pager

The desktop page hangs chat on one rail and hand history on the other, with agent thoughts in a third place. A phone gets one bottom surface, so all three become views of the same pager: [ chat | history | thoughts ], slid by swipe or tab. Opening any view triggers the same felt morph — there is one boundary, not three. The input row exists only on the chat view; in landscape the very same pager docks as the right column.

And it is a panel, not a bottom sheet. Sheets are for transient reference — a thing you summon, read, dismiss. Live chat is extended engagement; it gets a resident surface, the way every live platform ships it.

One correction the phone made later: the history view is a pager pane, but tapping a hand in it is not. On a phone that opens a full-screen takeover, not a pane and not a centered card — for a reason that took us two rounds and a portal to find (§07, finding 03).

04 ambient chat

the last four messages, floating

While you’re just watching (panel closed, both orientations), the last ≤4 messages float bottom-left over the felt — transparent rows, high-contrast text, fading out on a timer, pointer-transparent so they can never steal a tap. TikTok-style ambient chat: you feel the room without giving it any real estate. The moment the full feed is visible the floats clear — the feed already shows them, and nothing on this table gets said twice.

05 composing

the keyboard is not an event

Typing summons the keyboard and the layout compresses naturally — portrait: the felt shrinks in place at the top; landscape: it fills the top-left quadrant beside the chat column. No modal, no overlay, no “minimized” state. The felt never transforms away — it fills whatever region the stack leaves it, and the hand keeps playing in plain sight while you type.

the felt never transforms away. it just gets an honest share.
06 the shape rule

one boundary, one morph

Portrait with the panel closed, the felt is the circle — its max-tallness shape. Open the panel and it morphs to the flat wide oval sliding into the top slot. That transition is the one deliberate, animated shape change in the whole system. Everything else — keyboard, rotation settling, overlays — is a transform of the same solved table. Shape-per-mode: within a mode, the geometry never re-decides.

And the mode itself is not a CSS breakpoint. The geometry engine already knows exactly how much room the chrome has — its band widths are the crowding signal — so the chrome mode derives from the same solve that places the seats. One spatial authority. A parallel breakpoint system would disagree with the felt at some width eventually; a derived one can’t.

07 the reality check

what the phone taught us

Everything above was designed in a mockup and approved in a browser window. Then a human held an actual phone, and ten rounds of findings came back. Every single one of them was ours — not a platform quirk, not bad luck. Here they are in the order we made them: the mistake, what it cost, and the rule we now hold.

finding 01

the reveal control sat in the dynamic island’s lane

the mistakeWe hid the site header on phones and gave it a control at the top center of the screen — left-1/2 -translate-x-1/2. In portrait that is exactly where the Dynamic Island lives: a ~125×37px hole, centered, at the top edge. And when the first report came back, we fixed the wrong axis — pushed the control 32px DOWN out of iOS’s top-edge swipe band and left it centered, still directly under the cutout.

what it costTwo rounds on one small button. First, dragging it summoned Notification Center instead of our header. Then it was clear of the OS gesture and still underneath a hole.

the ruleAt the top edge of a phone, a small control is side-anchored, never centered. A full-width bar is exempt as long as its content is inset. That rule now lives in exactly one file, with the inventory of every surface it governs — because we had made the same mistake in two other places and only found them by going to look. The drag-down gesture the original affordance shipped with is retired outright: it had become a second way to do one thing, and its only remaining effect was racing the OS for the same swipe. One control, one tap.

finding 02

the header was drawn under the status bar

the mistakeviewport-fit=cover plus black-translucent deliberately extends the page under the status bar. That is exactly right for a felt background and exactly wrong for controls — and we applied it to both. The revealed site header translated to y = 0, so its whole 56px bar landed inside the ~47–59px status-bar band.

what it costRevealing the header showed you the clock and the battery icon sitting on top of the wordmark, the balance and sign-out. The thing you revealed was the thing you couldn’t read.

the ruleCover is for backgrounds; content takes the inset. And the fix is padding on the transformed box, not a top offset — translateY(-100%) is a percentage of the box’s own height, so padding keeps the hidden state fully off-canvas, while top: env(safe-area-inset-top) would have left a sliver exactly the inset tall on screen forever.

finding 03

the history modal was trapped by a transform

the mistakeTapping a hand on a phone opened a detail you could barely see. The obvious diagnosis was layering — it sat at z-50 while the collapsed chrome stacks 95, 100, 101, 110 above it. We raised the z-index. It fixed nothing. The real cause was one layer down: the pager’s sliding track always carries a transform (that is how the panes slide), and a transformed ancestor becomes the containing block for every position: fixed descendant — with overflow: hidden above it. The modal was never viewport-positioned at all. It was being drawn inside the pane and clipped to it.

what it costA whole diagnosis round spent on the layer we could see. position: fixed reads like a promise about the viewport, and CSS revokes it silently.

the ruleEscaping a transformed ancestor takes a portal, not a bigger number. On a phone the hand detail is now a full-screen takeover portalled to the document body, opening straight onto the replay so the play button is where your thumb already is. Desktop keeps its centered card, portal-free, in the tree it has always been in — and the test pins both sides, so neither can drift into the other.

It had one more surprise in it. The replay inside the takeover mounted the felt with DESKTOP geometry — columnLeft 224 + columnRight 208, so 432px of side bands inside a ~360px phone box. The same 432 pixels from the top of this article, come back to haunt us inside a modal. The geometry engine degraded gracefully instead of clipping, which is precisely why nothing looked broken: it just quietly rendered a 28px seat plaque using 32% of the width. One forwarded prop later: 39px, 84%.

finding 04

32 hardcoded z-indexes across 23 files

the mistakeEvery overlay picked its own stacking number at the moment it was written. This one plan hit three layering collisions before we drew the conclusion: the install nudge versus the hiding header, the bottom sheets versus the claim bar, then the history modal above.

what it costThree separate debug sessions that each ended at the same question — “what is above this?” — which nothing in the codebase could answer.

the ruleOne ladder, declared ascending so reading the file is reading the stack. All 32 sites now take their tier from it, each keeping exactly the number it already shipped, so nothing moved in the stack at all. A grep test fails the build on the next ad-hoc value. One honest correction fell out of writing it down: the brief said the login modal was topmost. It never was — five surfaces deliberately sit above it, because their whole job is that nothing is usable until they are answered. The ladder documents that boundary rather than asserting a topmost that was never true.

finding 05

touch targets at half the platform minimum

the mistakeThe install banner shipped about 29px tall, with a 15×14px dismiss × and a 50×20px install button — every control at half the 44px platform minimum, at the very top edge of a phone screen.

what it costThe report was not “hard to tap.” It was “nothing in the banner is clickable.” At half the platform minimum a control stops reading as a control at all — the honest conclusion for the person holding the phone was that we had drawn a picture of a banner.

the rule44px is a real inline min-width/min-height on the painted element, from one exported constant. Two more causes were hiding behind that one: the off-canvas site header still hit-tested its translated rect, so for 300ms it swept back up across the top of the page eating taps; and the ambient-float pattern’s pointer-events: none could swallow a child that never declared its own. Both fixed, both pinned — and what we ruled out is written into the test, so the next person doesn’t rule it out again.

finding 06

the camera cutout moves

the mistakeWe treated the notch as a number — a top inset. It is not a number, it is a hole, and it relocates. Portrait puts it on safe-area-inset-top. Landscape moves it to a side inset of ~44–59px, where the top inset is 0. Our code consumed top and bottom in eight places and the sides in none.

what it costThis is the entire reason a top-anchored control was fine in portrait and wrong in landscape. The same 32px offset that seated it below the portrait status bar put it at y ≈ 32 in landscape — no inset to clear, straight back into iOS’s own swipe band, beside a hole.

the ruleContent insets; backgrounds do not. A backdrop that stops at the inset trades the cutout for a black bar and throws away exactly what viewport-fit=cover bought, so each surface pads only the viewport edges it actually touches. The felt needed no geometry change at all — it is container-driven, so padding an ancestor shrinks it and the seat ring re-solves. (Padding the measured box itself changes nothing, because getBoundingClientRect includes padding. Its own small lesson.)

And then the conclusion that ended the whole thread: nothing may render in a hole, and the band beside the hole in portrait is the iOS status bar — not ours either. So no position at the top of the screen was ever going to be right. The control stopped floating. It became a slim tab on the far left of the table strip’s bottom edge — a drawer pull on the actual drawer, the same one in both orientations. The tab painted 30×15 — small on purpose, because a 44px square in the chip row had read as a different species beside four text chips — and carried a transparent hit-slop at the platform minimum, so the ink and the touch target were deliberately two different sizes. (That shape was right for a year and wrong for a phone. See finding 08, where it moved onto the header itself and took a whole portal seam with it, and then finding 12, where the user put it in the chip row after all — because the problem was never the shape.)

Two more decisions came out of the same review, and both are subtractions. First: the header pushes content down instead of overlaying it — “which would shrink the table a tiny bit as opposed to covering something up”. A taste call that happens to delete a whole bug class: a floating bar is what ate the install banner’s taps mid-transition (finding 05) and what got an unexpected containing block inside a transformed ancestor (finding 03). A box that takes flow space covers nothing, so there is nothing left to defend. It does mean the top inset changes hands: while the header is open it holds the status-bar inset and the strip stops holding it, or a notched phone pays ~59px twice on the axis it has least of.

Second: “can the header go full width in landscape. no need to make that area off limits.” We had been treating the side band as a reserved column — space given up so a control could live there covering nothing. It worked, and it cost a band of screen plus a runtime measurement to use: CSS can add an inset but cannot branch on which of the two is the non-zero one, so a hidden probe element had to be measured through getComputedStyle on every rotation just to answer “left or right?”. Once the tab lived on the strip, nothing needed the answer — and nothing has needed it since, because the reveal stopped being anchored to a viewport edge at all (finding 12). So the band went back to being what the rule already said it was — full-width boxes whose content pads past the hole — and two modules were deleted rather than one being added. That is the better version of this finding: the layout got simpler by removing the special case, not by handling it better.

finding 07

a float baseline isn’t portable

the mistakeThe felt’s markup tripwire compared full-precision seat coordinates with strict equality. Green on the dev machine, red in CI, with no code change between them.

what it costA red build on a test that was right about everything it cared about. The two machines are different CPUs — arm64 here, x86-64 there — and libm is only specified to about 1 ULP, so their Math.cos, Math.sin and ** disagree in the last one: 321.46266989387493 against …48, 314.7904016935511 against …096. A disagreement in the fifteenth significant digit failed a test about where a seat plaque goes.

the ruleA float baseline normalizes to a precision the product actually cares about. Both sides round to six decimals now — roughly nine orders of magnitude finer than a pixel — and we checked the tripwire kept its teeth: a 2px placement perturbation still fails it. Guarding a claim you can’t state portably isn’t rigor, it’s a flake.

finding 08

the pull was attached to the wrong thing

the mistakeFinding 06 got the SHAPE right and the OWNER wrong. We hung the tab off the table page’s strip, because that is where there was room. But the tab opens the site header — it is the header’s pull — and the header lives a layer up, in the app shell.

what it costAn entire two-way portal seam, invented to paper over the mismatch. The reveal STATE belongs to the header; the STRIP belonged to the page. So each collapsed surface rendered a display: contents slot, published it to a module store, and the header createPortaled its one control into whichever slot was mounted — plus a RevealPlacement union, plus a fail-safe floating variant for any surface that offered no slot, plus position: relative on two unrelated strips that carried it for no other reason. The user needed one sentence to dissolve all of it: “the tab to pull down the header needs to be on the header itself not the sub header on the table.”

the ruleA control belongs to the thing it controls. The tab became absolute against the header’s own box at top: 100%, so it hung off whatever the header’s bottom edge currently was — the top of the screen while hidden, under the bar while open, riding the same height transition with nothing to keep in sync. Five things were deleted to get there and nothing was added. When a deletion removes five other things, the seam was carrying a design mistake, not a dependency. (Third consecutive phase where the user’s instruction subtracted machinery.)

Half of this held and half of it did not. The STATE does belong to the header, and that half is load-bearing to this day. But we read “a control belongs to the thing it controls” as a claim about where the control renders, and that is the sentence that put it in the corner of the screen — see finding 12. The rule that survives is narrower and better: the state belongs to the thing it controls; the affordance belongs wherever the hand already is.

finding 09

the dismiss layer ate the scroll

the mistakeClick-away was an element: a transparent <button class="fixed inset-0"> mounted under the revealed header, there since the very first version. A full-screen button is a full-screen touch target, and it sat directly over the app shell’s overflow-y: auto content div.

what it cost“After pulling the header down in the lobby you can’t scroll.” Every touch in the card list landed on the dismiss layer, so the lobby could be dismissed but not dragged. It shipped for twelve phases and the suite never saw it, for a reason worth writing down: the table page has nothing to scroll — that is the whole design — so every smoke and every test in this plan exercised the one surface the bug could not appear on. The defect was invisible because of what we had chosen to test, not because it was subtle.

the ruleA listener cannot eat a gesture it does not own; an element always can. No z-index or touch-action fixes this — inset: 0 on a fixed box contains the scroller by construction, and containment is the bug. So the element is deleted and click-away is a capture-phase pointerdown listener on the document (plus Escape, for the keyboard path the button had been providing for free). The scroll proceeds and the header closes — both. Accepted trade: an outside tap now also does whatever it was aimed at, instead of being swallowed.

finding 10

a falsy default beat a deliberate omission

the mistakeThe felt’s cursive “connect to play” arrow was gated on seatJoinHint(Boolean(onSitDown), Boolean(walletConnected)). Both props are optional. Boolean(undefined) is false, so “nobody told me about a wallet” and “the wallet is disconnected” became the same answer.

what it costEvery surface that deliberately omitted the prop drew a wallet nudge. The hand-history replay says so on itself — “every interactive prop is inert by construction” — and still asked you to connect a wallet to a hand that finished hours ago (“on the history view the click here to connect wallet arrow cursive thing is showing up, it should not”). The desktop history card had it too, unreported. And being position: fixed, on both of this site’s demo felts it escaped the simulated phone frame entirely and landed on the real viewport. One coercion, five broken surfaces.

the ruleRequire positive evidence; do not coerce an optional to a claim. The gate is three-valued now: false means measured-and-off (show it), true means connected, and undefined means this surface has no wallet concept — answered with silence. Stated once, inside the gate, so a third felt cannot reintroduce the coercion at a call site where the gate can’t see it.

The same change answered a fourth report — “connect to play, you can remove that from mobile size” — better than a position fix could have. The arrow’s entire job is to point at the site header’s connect button, and in collapsed chrome that button is off-screen. The hint is therefore inapplicable, not misplaced. So collapsed chrome withholds the answer rather than answering it, and the three-valued gate suppresses the nudge with no new prop — because a showConnectNudge flag would be this same bug in a new coat: one more thing every caller has to remember, defaulting to a lie.

finding 11

we took the header away from a page that never needed it

the mistakeHiding the site header was keyed off one flag: data-chrome-mode. The lobby writes that flag too — it is a phone, after all — so the lobby lost its header as well, and grew a slim strip to stand in for it: a second wordmark, a second balance pill, a second everything.

what it cost“I think remove the header hide at all in the lobby both vertical or horizontal.” The felt genuinely needs the pixels — it is container-driven, and a phone has none to spare. A scrolling card list gains nothing and loses the wallet, the profile and sign-out behind a control the user could not reliably hit. We had also written the duplication into a comment and not noticed what it was telling us: “the site header sits off-canvas … so this strip is the whole header”. A surface that has to impersonate the chrome above it is a smell, not a design.

the ruleA mode is not a policy. The mode answers “is this a phone?”. Whether the chrome may be taken away is a product question, and only the surface can answer it — so the table page declares that it needs the screen and nothing else does. It is a mounted-lifetime lease, cleared on unmount, because the failure it prevents is a navigation: leave the table with the flag still set and the lobby renders headerless with no affordance anywhere to get it back. With the real bar back, the strip gives up everything the bar already carries and keeps only what is the lobby’s own.

finding 12

a position chosen to avoid things ends up where nothing is

the mistakeFour homes for one small control, and every one of them was chosen by asking what must it not cover? — not the chips, not the felt, not the pager, not the cutout, not the status bar. Answer that question hard enough and you arrive at the top-left corner of the screen, which is the one place on a phone where the answer is nothing.

what it cost“The tab thing is now too far away. It’s in the corner. You can’t actually click it. I don’t know what the solution is. I like the way it looks, but it doesn’t seem as functional as a button or some sort. It’s really frustrating.” It was the most carefully placed element in the plan — cutout-aware, gesture-band-aware, safe-area-aware, hit-slopped to the platform minimum — and it was unusable. Three phases of geometry, a probe element measured through getComputedStyle, five retired constants, and a thumb cannot get there.

the rulePut the control where the user’s thumb already goes. A position chosen only to avoid covering things ends up where nothing else lives, which is exactly where no thumb goes either. The user’s own fix: “a button on the far left to the left of where it says chat. Same size as height as chat.” The table strip’s chip row is already a row of tap targets a hand visits, so a chip that matches it needs no hit-slop, no layer tier, no gesture-zone arithmetic and no cutout geometry — the row solved all four before the control arrived. Everything the careful placement needed was deleted rather than moved: the tab, its 44px slop, its wrapper box, its three geometry knobs, and the last consumer of its rung on the z-ladder — the number stays reserved there, because reusing 101 for something else would be a silent collision and the assertions that describe the old stack name it.

and the trap inside the fixThere are two chips that say “chat” — one in the strip, one in the pager’s pane tabs — and the first attempt picked the pane tabs. That row only exists while the pager is open, and the pager opens closed. So the fix obeyed the instruction to the letter and broke it in substance: in the state you actually watch from there was no reveal on screen at all, and the header cost two taps. A control behind a disclosure is less reachable than a corner. The lesson generalises past this control: when a complaint is about reach, the answer has to be judged in the state the user is in by default, not the state a test happens to set up.

And the factoring is the part worth stealing, because the obvious way to do this is the thing finding 08 deleted. Do not portal a control into a surface; do not hand the surface a copy of the boolean and a callback. The header owns the STATE, the surface owns the AFFORDANCE — the reveal lives in a small shared store, the header reads it and renders no control at all, and the pager renders an ordinary <button> that toggles it. One boolean, one owner, two readers, no portal, and the two are structurally incapable of disagreeing because there is no sync step to get wrong.

finding 13

the icon that is exactly right is the one that might not render

the mistakeThe sketch was a bar with an arrow leaving it — down to show, mirrored to hide. Unicode has precisely these: U+21A7 ↧ and U+21A5 ↥. Two characters, no drawing, done.

what it costNothing, this time, because finding 04’s neighbour already cost it: a ⛶ fullscreen glyph rendered as an empty square on the user’s own phone. These two live in Arrows, well outside what a mono UI font is guaranteed to cover, and the control paints at 9px — where tofu and a glyph are the same size and shape. You would ship it, look at it, and see a button.

the ruleIf a glyph carries meaning, draw it. Inline SVG, currentColor, sized in em so it tracks the row’s own type instead of competing with it. And one definition, mirrored: the two states are the same three strokes rotated a half turn about the viewBox centre, not two hand-authored paths — because two paths are two things to keep symmetrical, and the failure is silent. The symmetry is the message the affordance is sending.

twelve of these came from a human holding a phone. the other came from a machine that wasn’t ours.
08 living on the home screen

the browser bars were the last of the chrome

Safari’s own chrome eats the landscape viewport, which is a strange thing to discover after spending a plan reclaiming 432 pixels. Two answers shipped, and neither pretends to be more than it is. The table strip carries a ⛶ fullscreen chip — standard requestFullscreen with the webkit fallback, active state driven by the real fullscreenchange event, and if the API isn’t there the chip isn’t either. And a minimal web-app manifest (display: standalone, brat noir colors) plus the iOS meta means Add to Home Screen launches the table with no browser chrome at all. Deliberately no service worker — a real PWA is a different plan, and half a PWA is worse than none.

Getting people to do that is an install nudge: a lime banner offering to put us on your home screen, honest per platform because the platforms are not the same. iOS has no install API, so iOS gets an instruction (share → Add to Home Screen). Chrome does, so Android gets a real button wired to beforeinstallprompt. Nobody already running standalone ever sees it, and dismissing it is forever.

It is gated on the device, not the viewport — (pointer: coarse) and (hover: none), failing closed when matchMedia is missing or hostile. That distinction cost us a round too: collapsed chrome is a viewport signal, so a narrow desktop window engaged the whole phone layout — and on desktop Chrome beforeinstallprompt genuinely fires, so we were briefly showing a live install button for a completely meaningless action.

09 ★ play with it ★

every state, in your hands

Every state above, clickable — the choreography the design was approved on, plus the affordances the findings put there. Rotate the phone. Open chat and watch the one morph. Summon the keyboard and see the compression. Swipe the pager between chat, history and thoughts. The 2s ring on the input is the real talk rate limit, surfaced.

Then the parts §07 argued for. Look at the left end of the table strip’s chips — no need to open anything, which is the point: the first chip in that row, drawn as a bar with an arrow leaving it, is the site header’s reveal — the same box as the chat chip beside it, height for height, because that was the whole request. Tap it and the header pushes everything down — the felt re-solves a touch smaller and nothing is covered, in either orientation — and the chip flips to the mirrored icon, because it is also the thing that closes it. It sits there for a reason worth the whole plan (finding 12): it lived at the screen’s top-left corner first, carefully clear of the cutout, the gesture band and the status bar, and a thumb could not reach it. A row of tap targets is where a hand already is — and it has to be a row that is always there, which the pager’s pane tabs (the first place this chip went) are not. Before that it was docked in the table strip (finding 08), which is the only reason a portal seam ever existed. Tap anywhere outside the header to dismiss it: there is no full-screen layer catching that tap, which is finding 09. Rotate for the other half of §07’s decision: landscape is full width, with the cutout side carried as content padding rather than a reserved column — and the same chip is in the same strip row, because there is no orientation branch left. Nothing drags; that gesture is retired. The chip and the home screen nudge are the ones from §08. And guides draws what we were fighting: the cutout as the hole it actually is, the portrait band above it hatched because iOS owns that, the landscape band tinted because we own it — the background bleeds under it while the content clears it.

What this is, exactly. A chrome demo. The felt inside it is the real BratTableV3 — the same component the live table renders, running the geometry engine on fixture state with spectator-inert props, reshaping itself as the modes change. The chrome around it demonstrates the shipped chrome without being the shipped components: same rules, same placements, same numbers, re-implemented here so an article can hold it. Believe it about where things go; read the app for what runs.

interactive · click everything

mode: portrait · watching · circle felt + floating messages

★ this is a chrome demo. the felt inside is the real BratTableV3 — the same component the live table renders, on fixture state with spectator-inert props. the chrome around it demonstrates the shipped chrome without being the shipped components: same rules, same placements, same numbers, re-implemented so an article can hold it.

◼ the cutout — a hole. nothing renders here.▨ env(safe-area-inset-top) · iOS owns this
skills·poker◈ 12,940
Spade Dojo5/10 · #81
📲 add skills·poker to your home screen — full screen, no browser bars
skills·poker
340
pot
RAISE RITA
R
13913
CLOSER C…
C
28118
SB
BLUFF BO…
B
22943
BB
BALANCE…
B
41370
POSITION…
P
21157
LAG LARRY
L
6834
PATIENT…
P
29995
MIN-RAIS…
M
31742
BLIND-DE…
B
29516
#81 BLAKE raises 120 · RITA calls · flop A♦ 5♦ 9♣
#80 LARRY all-in 6,834 · fold · fold · LARRY wins 415
#79 split pot — RITA / CLOSER 207 each
#78 BALANCE… wins 1,120 (set of 9s)
#77 walk — blinds to BLIND-DE…
#76 RITA bluffs river, takes 640
#75 4-way flop · CLOSER c-bets · takes it
seat 6 · LAG LARRY — “pot odds 3.2:1, range is capped, shoving any two if action folds to me”

seat 2 · CLOSER — “board pairs = bet 60% pot; villain folds to pressure 71%”

seat 9 · BLIND-DE… — “defending wider vs min-raise; note RITA’s timing tell”
2s
open the full demo →