anvilsign in

collin/mahjong

1@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;600;900&display=swap');
2
3:root {
4 /* Depth of each player's strip. `--h-len` is floored so a window too narrow
5 to hold two strips shrinks the tiles rather than going negative.
6
7 Shallower than it looks like it should be, on purpose: discards live in the
8 middle of the table now rather than in a row inside each strip, and the
9 depth that freed up was given to the centre instead of to the tiles. The
10 wall square and the pile inside it are what wanted the room. */
11 --depth: clamp(226px, 30vh, 307px);
12 --h-len: max(160px, calc(100vw - 2 * var(--depth)));
13 --v-len: 100vh;
14 /* How tall an action button is: a tile, because that is what they are now.
15 They sit in the row your hand is in, in the room it already keeps above the
16 tiles, and they are the same tile as the ones under them — 打出 and 碰 are
17 things you do with a tile, so they are drawn as one. */
18 --bar-h: calc(var(--tile-w) * 1.375);
19 /* The daylight between two tiles standing side by side in a row, and between
20 two buttons — they are tiles too. */
21 --hand-gap: 2px;
22 /* The wider daylight: what a tile pushed out of the row gets. The tile just
23 drawn is set this far from the rest of the hand, and the buttons are set
24 this far from *it* — because that tile is the one they are usually about
25 to act on, and it is the one wearing a 摸 tag wider than itself and a gold
26 ring when it is lifted. Anything tighter and the two touch. */
27 --drawn-gap: calc(var(--tile-w) * 0.45);
28 /* Largest tile a strip can hold. Edge inwards it has to fit the melds (a
29 full tile, 1.375 tall, since they are the same tiles as the ones in the
30 hand), the button bar, the hand (1.71 + 6px) and the nameplate (28px), plus
31 its own padding, gaps and margins — so 3.085 tiles plus 122px.
32
33 The melds used to be drawn at 0.62 of a tile and budgeted at 0.86, which is
34 where the old 2.57 came from; `--depth` grew by the difference rather than
35 the tiles shrinking, for the same reason it shrank when the discards moved
36 out to the middle. The strip is deeper, but the felt is *bigger* than it
37 was for any seat that has not claimed anything — see geometry.ts.
38
39 It used to be 3.45 tiles: the missing 0.88 is the row of discards, which is
40 in the middle of the table now. `--depth` came down by the same amount
41 rather than the tiles growing, so a tile is the size it always was and the
42 centre is bigger.
43
44 And it used to be 122px rather than 70px: the missing 52 is the action bar,
45 which had a row of its own and had to be budgeted for whether or not
46 anything was on it. It has no row now — it hangs at the end of the row the
47 hand is in, out of the flow — so the strip stopped paying for one.
48
49 Tile size otherwise follows the strip's *length*, which is why a window
50 with less height than F11 kept full-size tiles and left the strip's
51 contents to be cut off. */
52 --tile-fit: calc((var(--depth) - 70px) / 3.085);
53
54 /* One size for every tile on the table — in a hand, in the wall, and lying in
55 the middle. They are the same tiles, so drawing them at different sizes was
56 always a lie, and a visible one: a tile that shrank as it was thrown read as
57 being dropped away from you rather than skidded across a flat table.
58
59 Taken from whichever way round is tighter, so all four hands match rather
60 than the side seats getting their own size off `--v-len`.
61
62 What a strip has to fit along its length, written out: sixteen tiles and
63 fifteen gaps of hand, the slot it keeps for the tile it is about to draw
64 (a gap, a `--drawn-gap` and a tile), the `--drawn-gap` on either side of
65 the hand, a tile of button at the end of the row — and, because the hand
66 is centred rather than pushed along, that same button's width again on the
67 other side. That comes to 20.35 tiles and 48px, and the row is 16px inside
68 the strip's own padding.
69
70 Sized off 18 tiles it was not enough: a hand down the side of a 765px
71 window ran off both ends of its own strip, and 碰 and 看牌 stood in felt
72 that was not on the screen. Nothing about the numbers below changes when
73 the buttons wrap — the room for the first one is always there. */
74 --tile-w: min(
75 calc((var(--h-len) - 48px) / 20.35),
76 calc((var(--v-len) - 48px) / 20.35),
77 var(--tile-fit),
78 72px
79 );
80
81 --felt: #0f4534;
82 --felt-2: #0a3126;
83 --ivory: #f6f0e2;
84 --ivory-edge: #d9cfb6;
85 --ink: #21262b;
86 --gold: #d9b45c;
87 --red: #b3261e;
88 --green: #1c7a4b;
89 --blue: #1d5aa8;
90}
91
92* {
93 box-sizing: border-box;
94 -webkit-tap-highlight-color: transparent;
95}
96
97html,
98body,
99#root {
100 height: 100%;
101 margin: 0;
102 overflow: hidden;
103 overscroll-behavior: none;
104 touch-action: manipulation;
105 user-select: none;
106}
107
108/* Phone browsers hand out a `100%`/`100vh` viewport that includes the address
109 bar they are about to slide away over the top of it; `dvh` is the height the
110 table can actually have. */
111@supports (height: 100dvh) {
112 html,
113 body,
114 #root {
115 height: 100dvh;
116 }
117}
118
119body {
120 font-family: 'Noto Sans TC', system-ui, sans-serif;
121 color: var(--ivory);
122 background: var(--felt-2);
123}
124
125/* ---------- lobby ---------- */
126.lobby {
127 height: 100%;
128 display: flex;
129 flex-direction: column;
130 align-items: center;
131 /* Centred while the whole lobby fits, and pinned to the top the moment it
132 does not. Plain `center` spills equally out of both ends of a short window,
133 and the half that goes off the top is cut away with no scroll that reaches
134 it: the title was losing its lower stroke and the settings row was falling
135 off the bottom on any laptop window under about 800px. `safe` keeps the
136 overflow on the end you can actually scroll to. */
137 justify-content: safe center;
138 overflow-y: auto;
139 padding: 16px 16px calc(16px + env(safe-area-inset-bottom));
140 gap: 0.5rem;
141 background: radial-gradient(ellipse at center, var(--felt) 0%, var(--felt-2) 75%);
142}
143/* Nothing in here gives up height to fit. A squeezed button is a button with
144 its own two lines of text crushed inside it, which is the cut-off text the
145 scroll is here to avoid. */
146.lobby > * {
147 flex: none;
148}
149.lobby h1 {
150 font-size: clamp(3rem, 9vw, 7rem);
151 font-weight: 900;
152 letter-spacing: 0.1em;
153 margin: 0 0 0.5rem;
154 color: var(--gold);
155}
156.lobby p {
157 margin: 0;
158 opacity: 0.75;
159 letter-spacing: 0.15em;
160}
161.lobby .hint {
162 opacity: 0.45;
163 font-size: 0.9rem;
164 margin-bottom: 1.5rem;
165}
166
167/* ---------- table ---------- */
168.table {
169 position: relative;
170 height: 100%;
171 width: 100%;
172 background: radial-gradient(ellipse at center, var(--felt) 0%, var(--felt-2) 80%);
173}
174
175.slot {
176 position: absolute;
177 overflow: hidden;
178}
179.slot-bottom {
180 bottom: 0;
181 left: var(--depth);
182 right: var(--depth);
183 height: var(--depth);
184}
185.slot-top {
186 top: 0;
187 left: var(--depth);
188 right: var(--depth);
189 height: var(--depth);
190}
191.slot-left {
192 left: 0;
193 top: 0;
194 width: var(--depth);
195 height: 100%;
196}
197.slot-right {
198 right: 0;
199 top: 0;
200 width: var(--depth);
201 height: 100%;
202}
203
204.seat {
205 position: absolute;
206 top: 50%;
207 left: 50%;
208 height: var(--depth);
209 display: flex;
210 flex-direction: column;
211 justify-content: flex-end;
212 align-items: center;
213 gap: 2px;
214 padding: 2px 8px 4px;
215 border-radius: 10px;
216 overflow: hidden;
217 transition: background 0.2s;
218}
219/* `--tile-w` is the table's, set once in :root — a seat only says how long its
220 own strip is. */
221.slot-bottom .seat,
222.slot-top .seat {
223 width: var(--h-len);
224}
225.slot-left .seat,
226.slot-right .seat {
227 width: var(--v-len);
228}
229.seat.active {
230 background: linear-gradient(to top, rgba(217, 180, 92, 0.22), transparent 70%);
231}
232/* Whose go it is, on a screen with four people round it. `active` is the same
233 thing said to a player who is already looking at their own tiles; this is the
234 version the *room* can read — it goes on a covered hand too, since whose turn
235 it is was never one of the things the card over a hand is hiding, and the
236 seat that has not noticed is exactly the one it is for.
237
238 Twice the gold `active` has and up twice as far into the strip: at a party
239 table the person being waited on may be looking at their phone, across the
240 room, or at somebody else's joke. */
241.seat.awaited {
242 background: linear-gradient(to top, rgba(217, 180, 92, 0.34), transparent 78%);
243 animation: spotlight 2.6s ease-in-out infinite;
244}
245/* And it breathes. A gold wash that simply sits there is read once and then
246 stops being read — it becomes part of the felt, which is exactly what this
247 must not be, because the person it is for is the one who has not looked up.
248 Movement is the thing peripheral vision answers to.
249
250 Slow, and never all the way off: two and a half seconds is a breath rather
251 than a blink, and the low end still says clearly whose go it is to anybody
252 who glances at that moment. The red on `claiming` runs at half this and is
253 meant to: a claim window closes and this does not. */
254@keyframes spotlight {
255 50% {
256 background: linear-gradient(to top, rgba(217, 180, 92, 0.62), transparent 86%);
257 }
258}
259/* And the name at the outer end of the strip, since that is what somebody
260 glancing up reads to find out who — the felt says *where*, the plate says
261 *who*, and at a table of people who have known each other five minutes those
262 are not the same question. */
263.seat.awaited .nameplate {
264 opacity: 1;
265}
266.seat.awaited .nameplate .pname {
267 color: var(--gold);
268 text-shadow: 0 0 10px rgba(217, 180, 92, 0.55);
269}
270.seat.claiming {
271 background: linear-gradient(to top, rgba(179, 38, 30, 0.25), transparent 70%);
272 animation: pulse 1.1s ease-in-out infinite;
273}
274@keyframes pulse {
275 50% {
276 background: linear-gradient(to top, rgba(179, 38, 30, 0.45), transparent 70%);
277 }
278}
279
280/* ---------- the seven seconds ---------- */
281/* A bar of gold along the inner edge of the strip — the edge that faces the
282 middle of the table, which is the one everybody at the screen is looking
283 over. Every seat is rotated so that its own top is the inner edge, so `top`
284 is the right answer at all four of them without asking which.
285
286 It drains from both ends towards the middle rather than from one end to the
287 other, because two of the four strips are upside down from wherever you are
288 sitting and a bar that empties leftwards for you empties rightwards for the
289 person opposite. Draining symmetrically reads the same from every chair.
290
291 Nothing happens when it reaches zero. The bar is gone and the line it leaves
292 breathes, and the table goes on waiting — see ui/TurnClock. */
293.turn-clock {
294 position: absolute;
295 top: 0;
296 left: 8px;
297 right: 8px;
298 height: 5px;
299 border-radius: 999px;
300 overflow: hidden;
301 pointer-events: none;
302 z-index: 3;
303}
304.turn-clock i {
305 display: block;
306 height: 100%;
307 border-radius: inherit;
308 background: linear-gradient(
309 90deg,
310 rgba(217, 180, 92, 0.45),
311 var(--gold) 28%,
312 var(--gold) 72%,
313 rgba(217, 180, 92, 0.45)
314 );
315 box-shadow: 0 0 9px rgba(217, 180, 92, 0.65);
316 animation: turn-drain var(--turn-secs, 7s) linear forwards;
317}
318@keyframes turn-drain {
319 from {
320 transform: scaleX(1);
321 }
322 to {
323 transform: scaleX(0);
324 }
325}
326/* What is left at zero: the same line, dim, breathing. Delayed by exactly the
327 seconds the bar took, so it starts as the gold runs out. */
328.turn-clock::after {
329 content: '';
330 position: absolute;
331 inset: 1px 0;
332 border-radius: inherit;
333 background: rgba(217, 180, 92, 0.5);
334 opacity: 0;
335 animation: turn-empty 1.5s ease-in-out var(--turn-secs, 7s) infinite;
336}
337@keyframes turn-empty {
338 0%,
339 100% {
340 opacity: 0.12;
341 }
342 50% {
343 opacity: 0.55;
344 }
345}
346
347/* ---------- tiles ---------- */
348/* Faces come from a 43x2 sprite sheet; the second row is the highlighted
349 variant, which we use for the tile a player has lifted to discard. */
350.tile {
351 position: relative;
352 flex: none;
353 --tw: var(--tile-w);
354 width: var(--tw);
355 height: calc(var(--tw) * 1.375);
356 border-radius: calc(var(--tw) * 0.1);
357 transition: transform 0.12s ease, filter 0.12s ease;
358}
359/* A melded tile is the same tile it was in the hand it came out of, so it is
360 the same size. It used to be drawn at 0.62 and read as a different, smaller
361 set of tiles sitting beside the real ones. */
362.tile.t-meld {
363 --tw: var(--tile-w);
364}
365.tile.t-small {
366 --tw: calc(var(--tile-w) * 0.64);
367}
368.tile.face {
369 background-image: url('./assets/tiles.svg');
370 background-repeat: no-repeat;
371 background-size: calc(var(--tw) * 43) calc(var(--tw) * 2.75);
372 background-position: calc(var(--col) * var(--tw) * -1) 0;
373 filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45));
374}
375.tile.back {
376 background-image: url('./assets/back.png');
377 background-size: 100% 100%;
378 filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45));
379}
380.tile.tappable {
381 cursor: pointer;
382}
383.tile.raised {
384 transform: translateY(calc(var(--tw) * -0.28));
385 filter: drop-shadow(0 10px 12px rgba(0, 0, 0, 0.55));
386}
387/* Lifted tiles switch to the sheet's highlighted row. */
388.tile.face.raised {
389 background-position: calc(var(--col) * var(--tw) * -1) calc(var(--tw) * -1.375);
390}
391.tile.hl {
392 outline: 2px solid var(--gold);
393 outline-offset: 1px;
394 border-radius: calc(var(--tw) * 0.1);
395}
396.tile.dim {
397 opacity: 0.45;
398}
399
400/* ---------- seat sections ---------- */
401.hand {
402 display: flex;
403 gap: var(--hand-gap);
404 /* the extra top room is where the 摸 tag sits */
405 height: calc(var(--tile-w) * 1.71 + 6px);
406 padding-top: calc(var(--tile-w) * 0.34);
407 align-items: flex-end;
408 flex: none;
409}
410/* A hand keeps the room for the tile it is about to draw, whether or not it is
411 holding it yet.
412
413 The row is centred in the strip, so without this every draw slid all sixteen
414 tiles half a tile along and every discard slid them back — the tile you were
415 looking at moved out from under you between one glance and the next. The slot
416 is the same width the drawn tile takes when it arrives (its own gap, the
417 daylight it is set apart by, and the tile), so a draw fills a space that was
418 already there and nothing else moves at all.
419
420 Every hand, not only your own: the four strips are the same strip, and each
421 of them is one draw away from needing it. Not on a phone, where the row wraps
422 to as many lines as it needs and a draw re-flows it anyway — there the slot
423 would only push the whole hand off its own middle.
424
425 A face-down hand is the reason for the seventeenth-tile test. Its tiles are
426 all one tile — which one was drawn is exactly what a covered hand is not
427 saying — so it has no `.drawn-slot` to go by, and a dealt seventeen was
428 keeping room for an eighteenth tile that cannot exist. Seventeen is the most
429 a hand ever holds (a kong takes three out and gives one back), so a row that
430 long is a row already holding its draw, and it is the same width either way:
431 sixteen tiles and the slot, or seventeen tiles and none. */
432:root:not(.compact) .hand:not(:has(.drawn-slot)):not(:has(.hand-cell:nth-child(17))) {
433 padding-right: calc(var(--hand-gap) + var(--drawn-gap) + var(--tile-w));
434}
435/* And the seventeenth tile of a covered hand still stands the daylight away
436 from the rest that a drawn tile stands, so that the sixteen under it are at
437 the same sixteen places they were before the draw. It is at the end of the
438 row rather than beside the tile, since a covered hand is not saying which of
439 them arrived last. */
440:root:not(.compact) .hand:not(:has(.drawn-slot)):has(.hand-cell:nth-child(17)) {
441 padding-right: var(--drawn-gap);
442}
443/* Your own hand, face down at a shared screen until you tap it. The backs are
444 already the picture; all this adds is that it is *askable* — a hand that
445 lifts a little under the finger, the way a card laid over a strip does. The
446 three other hands, which are face down because they are not yours, get none
447 of it and stay flat. See the `shared` prop in ui/Seat. */
448.hand.covered {
449 cursor: pointer;
450 transition: transform 0.12s ease-out, filter 0.12s ease-out;
451}
452.hand.covered:hover,
453.hand.covered:focus-visible {
454 transform: translateY(-3px);
455 filter: brightness(1.12);
456 outline: none;
457}
458.hand.covered:active {
459 transform: translateY(-1px);
460}
461
462/* Somebody has thrown a tile hard enough to come off the pool and into this
463 row. It takes the knock and rocks back — the sound is `barge` in game/sound,
464 and what decides it happened is the physics, in ui/Pool. Inside the seat's
465 own rotation, so it is always knocked *away from* the middle of the table. */
466.hand.shoved {
467 animation: shoved 0.34s ease-out;
468}
469@keyframes shoved {
470 0% {
471 transform: none;
472 }
473 22% {
474 transform: translateY(5px) rotate(0.5deg);
475 }
476 55% {
477 transform: translateY(-2px) rotate(-0.3deg);
478 }
479 80% {
480 transform: translateY(1px);
481 }
482 100% {
483 transform: none;
484 }
485}
486/* Every hand tile sits in a draggable cell. */
487.hand-cell {
488 position: relative;
489 display: flex;
490 align-items: flex-end;
491 touch-action: none;
492 cursor: grab;
493}
494.hand-cell.dragging {
495 z-index: 5;
496 cursor: grabbing;
497}
498.hand-cell.dragging .tile {
499 transform: translateY(calc(var(--tile-w) * -0.2)) scale(1.06);
500 filter: drop-shadow(0 14px 18px rgba(0, 0, 0, 0.65));
501}
502/* Lifted out of the row and being carried: the canvas is drawing it now, so the
503 element steps aside. Its slot stays, so the row does not shuffle around under
504 the tile you are holding and you can still drop it back where it came from. */
505.hand-cell.carrying .tile {
506 visibility: hidden;
507}
508.drawn-slot {
509 margin-left: var(--drawn-gap);
510}
511.drawn-slot .tile {
512 outline: 2px solid var(--gold);
513 outline-offset: 1px;
514 border-radius: calc(var(--tw) * 0.1);
515}
516.drawn-tag {
517 position: absolute;
518 top: calc(var(--tile-w) * -0.3);
519 left: 0;
520 right: 0;
521 text-align: center;
522 font-size: calc(var(--tile-w) * 0.24);
523 font-weight: 600;
524 letter-spacing: 0.1em;
525 color: var(--gold);
526 white-space: nowrap;
527 pointer-events: none;
528}
529
530/* No reservation. The strip is packed towards its outer edge and this is the
531 innermost thing in it, so a meld appearing grows inwards and moves nothing —
532 the height was only ever holding open a band of empty green between the seat
533 and the middle, which table/geometry.ts then had to hand back to the felt.
534 Room for one is still budgeted in `--tile-fit`, so a claim cannot overflow
535 the strip. */
536.melds {
537 display: flex;
538 gap: 10px;
539 flex-wrap: wrap;
540 justify-content: center;
541 flex: none;
542}
543.meld {
544 display: flex;
545 gap: 1px;
546}
547.meld.flowers {
548 margin-left: 12px;
549 padding-left: 12px;
550 border-left: 1px dashed rgba(246, 240, 226, 0.3);
551}
552/* Only the phone layout has these now — at full size a discard is out in the
553 middle with the others. Kept as the shared base the `.compact` rules below
554 build on, not as something the four strips render. */
555.discards {
556 display: flex;
557 flex-wrap: wrap;
558 gap: 2px;
559 justify-content: center;
560 align-content: flex-start;
561 max-width: 94%;
562 min-height: 0;
563 margin-bottom: 2px;
564 flex: 1 1 0;
565 overflow-y: auto;
566 scrollbar-width: none;
567}
568
569.nameplate {
570 display: flex;
571 align-items: center;
572 gap: 8px;
573 font-size: 13px;
574 letter-spacing: 0.08em;
575 opacity: 0.9;
576 padding-top: 2px;
577 flex: none;
578}
579.nameplate .wind {
580 display: flex;
581 flex-direction: column;
582 align-items: center;
583 line-height: 1;
584 font-size: 18px;
585 font-weight: 900;
586 color: var(--gold);
587}
588.nameplate .wind em {
589 font-style: normal;
590 font-size: 8px;
591 font-weight: 600;
592 letter-spacing: 0.1em;
593 text-transform: uppercase;
594 opacity: 0.6;
595}
596.nameplate .wind.dealer {
597 color: #ff7a68;
598}
599.nameplate .tag {
600 font-size: 11px;
601 padding: 1px 6px;
602 border-radius: 999px;
603 background: rgba(217, 180, 92, 0.2);
604 border: 1px solid rgba(217, 180, 92, 0.5);
605}
606/* Why a tile lobs over the square instead of sliding into it. */
607.nameplate .tag.walled {
608 background: rgba(246, 240, 226, 0.08);
609 border-color: rgba(246, 240, 226, 0.28);
610 opacity: 0.75;
611}
612.nameplate .tag.ting {
613 background: rgba(46, 170, 120, 0.2);
614 border-color: rgba(46, 170, 120, 0.6);
615}
616.nameplate .tag.sacred {
617 background: rgba(179, 38, 30, 0.22);
618 border-color: rgba(224, 108, 96, 0.6);
619 color: #ffc9c2;
620}
621.nameplate .chips {
622 margin-left: 4px;
623 font-weight: 900;
624 font-size: 16px;
625 color: var(--gold);
626}
627
628/* The tiles and the buttons that act on them, on one row. The hand stays in the
629 middle of the strip where it has always been, and the bar is taken out of the
630 flow and hung on the right-hand end — so a button arriving moves nothing at
631 all, neither the tiles under it nor the sets and flowers above. */
632/* Three columns: the room to the left of the hand, the hand, and the room to
633 the right of it. The two outside columns are the same `1fr`, so the hand is
634 centred in the strip and stays centred whether or not there is a button — and
635 the bar starts at the *start* of the right-hand column, which is exactly
636 where the tiles stop. A button is one more tile at the end of the row, one
637 tile-gap along, rather than something parked at the far edge of the strip.
638
639 `1fr` rather than `minmax(0, 1fr)`: a bar crowded with claim buttons is
640 allowed to push the hand along instead of being cut off by the strip. */
641.hand-row {
642 position: relative;
643 display: grid;
644 grid-template-columns: 1fr auto 1fr;
645 column-gap: var(--drawn-gap);
646 align-items: end;
647 align-self: stretch;
648 flex: none;
649}
650:root:not(.compact) .hand-row > .hand {
651 grid-column: 2;
652}
653/* Out of the flow, in the column the grid gave it: the grid decides *where* the
654 bar starts (the start of the right-hand column, which is where the tiles
655 stop) and the bar then costs the row nothing. So the hand keeps the middle of
656 the strip whatever is on the bar, and a crowded bar — 碰 槓 胡 過 all at once,
657 or a hand with two kongs to declare — stacks up the strip in tile-wide lines
658 instead of running off the end of it. The room for the first line is in
659 `--tile-w` itself; the lines above it are over the felt, which on the seat
660 being waited on is the emptiest part of the strip. */
661:root:not(.compact) .hand-row > .actionbar {
662 grid-column: 3;
663 position: absolute;
664 left: 0;
665 bottom: 0;
666 width: 100%;
667 /* Bottom line first, later ones above it: the buttons grow away from the
668 hand rather than down through it. */
669 flex-wrap: wrap-reverse;
670 align-content: flex-start;
671}
672.actionbar {
673 display: flex;
674 /* The gap the tiles keep between themselves, so a run of buttons reads as a
675 run of tiles rather than as a toolbar. */
676 gap: var(--hand-gap);
677 /* On the same line the tiles stand on, since they are tiles too. */
678 align-items: flex-end;
679 justify-content: flex-start;
680 flex-wrap: wrap;
681 flex: none;
682}
683/* A phone has no room beside a hand. There the wrapper steps out of the way
684 entirely and the bar is a row of its own again, where the grid and the
685 `order`s further down put it. */
686.compact .hand-row {
687 display: contents;
688}
689.compact .actionbar {
690 position: static;
691 height: auto;
692 min-height: var(--bar-h);
693 align-self: stretch;
694}
695/* Every button on the bar is the same height, so the row is the same height
696 whichever ones are up: without this a 打出 五萬 stands taller than a bare 過
697 and the strip shuffles itself every time you lift a tile. */
698.actionbar .btn {
699 height: var(--bar-h);
700 min-height: 0;
701 padding-top: 4px;
702 padding-bottom: 4px;
703}
704.waiting {
705 font-size: 12px;
706 opacity: 0.5;
707}
708.delta {
709 font-size: 20px;
710 font-weight: 900;
711}
712.delta.up {
713 color: #5fd39a;
714}
715.delta.down {
716 color: #ff8a80;
717}
718
719/* ---------- buttons ---------- */
720.btn {
721 font-family: inherit;
722 font-size: 17px;
723 font-weight: 400;
724 color: #1a1a1a;
725 background: linear-gradient(180deg, #f3e6c4, #d9b45c);
726 border: none;
727 border-radius: 8px;
728 padding: 6px 15px;
729 min-height: 46px;
730 box-shadow: 0 2px 0 #9a7d38;
731 cursor: pointer;
732 display: inline-flex;
733 align-items: center;
734 gap: 6px;
735}
736.btn:active {
737 transform: translateY(2px);
738 box-shadow: none;
739}
740.btn-sub {
741 font-size: 12px;
742 opacity: 0.7;
743}
744
745/* Bilingual label: the called term above, English cue below. */
746.bi {
747 display: flex;
748 flex-direction: column;
749 align-items: center;
750 justify-content: center;
751 line-height: 1.05;
752 gap: 1px;
753}
754.bi .zh {
755 font-size: 19px;
756 font-weight: 400;
757 letter-spacing: 0.06em;
758}
759.bi .en {
760 font-size: 9.5px;
761 font-weight: 600;
762 letter-spacing: 0.11em;
763 text-transform: uppercase;
764 opacity: 0.68;
765 white-space: nowrap;
766}
767.bi .bi-sub {
768 font-size: 11px;
769 opacity: 0.72;
770 white-space: nowrap;
771}
772.big-btn .bi .zh {
773 font-size: 24px;
774}
775.big-btn .bi .en {
776 font-size: 11px;
777}
778.btn.claim.hu {
779 background: linear-gradient(180deg, #ff8a7a, #c0392b);
780 color: #fff;
781 box-shadow: 0 2px 0 #7c231a;
782}
783.btn.claim.kong,
784.btn.claim.pung {
785 background: linear-gradient(180deg, #8fd3ff, #2b7ec0);
786 color: #fff;
787 box-shadow: 0 2px 0 #1c527d;
788}
789.btn.claim.chow {
790 background: linear-gradient(180deg, #9be3b7, #2e9e63);
791 color: #fff;
792 box-shadow: 0 2px 0 #1d6640;
793}
794.btn.sort {
795 min-height: 20px;
796 padding: 1px 8px;
797 font-size: 11px;
798 letter-spacing: 0.06em;
799 border-radius: 999px;
800}
801.btn.proceed {
802 background: linear-gradient(180deg, #f3e6c4, #d9b45c);
803}
804.btn.pass,
805.btn.ghost {
806 background: transparent;
807 color: rgba(246, 240, 226, 0.75);
808 border: 1px solid rgba(246, 240, 226, 0.35);
809 box-shadow: none;
810}
811.btn.discard {
812 background: linear-gradient(180deg, #fff2cf, #e0c070);
813}
814/* Somebody pulling up a chair at a hand the computer has been playing. It is
815 the only button on that strip, so it may as well be the warm one — and it is
816 the same gold as 摸牌, because both are the table's way of saying this edge
817 is yours now. */
818.btn.sit {
819 background: rgba(217, 180, 92, 0.18);
820 border-color: rgba(217, 180, 92, 0.55);
821 color: #f6f0e2;
822}
823
824/* ---------- the action buttons are tiles ---------- */
825/* 打出, 碰, 吃, 槓, 胡, 過 are all things you do *with* a tile, and they sit in
826 the row your tiles are in — so they are drawn as tiles. Exactly the size of
827 the ones under them, `--tile-w` by 1.375 of it, ivory face and all, in the
828 room the hand row already keeps above its tiles for the 摸 tag. The room is
829 kept whether or not anything is on the bar, which is why the strip does not
830 move when one arrives.
831
832 Ivory, not coloured: a mahjong tile is ivory and it is the *character* that
833 is red or green. So the call takes the colour and the face never does.
834
835 The phone gets them too. Its bar is a row of its own rather than the end of
836 the hand row, but a call is still a thing you do with a tile and it should
837 still look like one — so `.ctl-actions` is scoped in alongside, and sizes its
838 tiles off a `--tile-w` of its own that is what a thumb needs rather than what
839 seventeen across a hand allows. Not the compact *table*, where four hands
840 share one phone and a tile is too small to write a call on. */
841/* Not a white rectangle standing in for a tile: the tile itself. The sheet the
842 hand is drawn from has a forty-third cell with no character on it — a blank
843 tile — and that is what a button is drawn with, at the same size, off the
844 same file, through the same `background-size` arithmetic as `.tile.face`. So
845 a button has the bevel the tiles have, the two grey sides they stand on
846 (`sideLight` down the left, `sideDark` along the bottom), the black hairline
847 round the outside, and the shadow the drop-shadow filter takes from the art's
848 own alpha rather than from a rectangle. It cannot drift from the tiles beside
849 it, because it is one of them.
850
851 Pressed, it turns over to the sheet's second row — the highlighted variant,
852 which is what a tile lifted out of the hand switches to. The same gesture
853 lights the same way whether it is a tile or a call. */
854:is(:root:not(.compact) .actionbar, .ctl-actions) .btn {
855 position: relative;
856 flex-direction: column;
857 width: var(--tile-w);
858 height: var(--bar-h);
859 min-height: 0;
860 padding: 0;
861 gap: 0;
862 border: none;
863 border-radius: calc(var(--tile-w) * 0.1);
864 background: url('./assets/tiles.svg') no-repeat;
865 background-size: calc(var(--tile-w) * 43) calc(var(--tile-w) * 2.75);
866 background-position: calc(var(--tile-w) * -42) 0;
867 box-shadow: none;
868 filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45));
869 color: var(--ink);
870 transition: transform 0.12s ease, filter 0.12s ease;
871}
872/* Under the finger it lifts, the way a tile does — a good deal less far, since
873 a button that jumped a quarter of its own height would be a button that moved
874 out from under the click. */
875:is(:root:not(.compact) .actionbar, .ctl-actions) .btn:hover {
876 transform: translateY(calc(var(--tile-w) * -0.07));
877 filter: drop-shadow(0 5px 7px rgba(0, 0, 0, 0.5));
878}
879:is(:root:not(.compact) .actionbar, .ctl-actions) .btn:active {
880 transform: translateY(1px);
881 background-position: calc(var(--tile-w) * -42) calc(var(--tile-w) * -1.375);
882 filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.45));
883}
884/* The call is written down the tile, the way the character on a tile's face is
885 written: one on top of the next, in the middle of the face, with the English
886 along the bottom edge under it.
887
888 A row of these is a row of tiles, and a tile is taller than it is wide — so
889 two characters side by side had to be drawn small enough to sit across a
890 38px face, while the same two stacked are the size the tile's own character
891 is. Four (過.摸牌) run out of height and take a second column to the left of
892 the first, which is where the next column of vertical Chinese goes.
893
894 The grid is what puts them there: one row that takes all the height left over
895 and centres what is in it, one row that is exactly the English. The reading
896 (打出) is the right-hand column, and the tile it is about (五萬) the column to
897 its left, because vertical Chinese reads right to left. */
898/* Written on the face, which is not the middle of the tile: the art stands the
899 tile up on `LOOK.depth` of thickness towards the bottom left, so the face is
900 inset by the left side's width and the bottom side's height and hardly at all
901 on the other two. Measured off the sheet rather than guessed — the blank cell
902 at column 42 is white from 14% of its width to 98%, and from 1% of its height
903 to 91%. As padding rather than a nudge, so the grid inside centres on the
904 face and the English sits along the face's own bottom edge. */
905:is(:root:not(.compact) .actionbar, .ctl-actions) .btn .bi {
906 display: grid;
907 grid-template-columns: auto auto;
908 grid-template-rows: 1fr auto;
909 justify-content: center;
910 align-items: center;
911 gap: 0;
912 line-height: 1;
913 height: 100%;
914 width: 100%;
915 padding: calc(var(--tile-w) * 0.015) calc(var(--tile-w) * 0.02)
916 calc(var(--tile-w) * 0.125) calc(var(--tile-w) * 0.14);
917}
918:is(:root:not(.compact) .actionbar, .ctl-actions) .btn .bi .zh {
919 grid-row: 1;
920 grid-column: 2;
921 /* Its own width, centred in the column, rather than stretched to fill it —
922 a stretched box in `vertical-rl` puts its column of characters against the
923 right-hand edge, which is a second way of ending up off centre. */
924 justify-self: center;
925 writing-mode: vertical-rl;
926 /* The room above the English, so a long call wraps into a second column
927 rather than growing out through the top of the tile. */
928 max-height: calc(var(--bar-h) - var(--tile-w) * 0.36);
929 font-size: calc(var(--tile-w) * 0.44);
930 letter-spacing: 0;
931}
932/* Under the call, in the call's own column — not spanning both.
933 Spanning was what pushed 打出 off the middle of the face: the English is
934 wider than one column of characters, and a spanning item shares the width it
935 needs out across every track it covers, so the empty 五萬 column took half of
936 the difference and carried the reading right along with it. In one column it
937 sizes that column and nothing else, and the pair of them centre together. */
938:is(:root:not(.compact) .actionbar, .ctl-actions) .btn .bi .en {
939 grid-row: 2;
940 grid-column: 2;
941 justify-self: center;
942 text-align: center;
943 font-size: calc(var(--tile-w) * 0.185);
944 letter-spacing: 0.03em;
945 white-space: normal;
946 opacity: 0.6;
947}
948/* An English-only table has nothing standing above it, so the name is the face
949 rather than the caption under one. */
950:is(:root:not(.compact) .actionbar, .ctl-actions) .btn .bi .en:first-child {
951 grid-row: 1 / -1;
952 font-size: calc(var(--tile-w) * 0.22);
953 opacity: 0.85;
954}
955/* Which tile it is about — 打出 五萬, 吃 三萬 四萬. Small, because the tile it
956 names is lifted out of the row right beside it, and written the same way
957 down the face as the call it belongs to. */
958:is(:root:not(.compact) .actionbar, .ctl-actions) .btn .bi .bi-sub {
959 grid-row: 1;
960 grid-column: 1;
961 justify-self: center;
962 writing-mode: vertical-rl;
963 max-height: calc(var(--bar-h) - var(--tile-w) * 0.36);
964 font-size: calc(var(--tile-w) * 0.22);
965 white-space: normal;
966 opacity: 0.75;
967}
968/* The character carries the colour, the way the dragons do. */
969:is(:root:not(.compact) .actionbar, .ctl-actions) .btn.claim.hu {
970 color: #b3261e;
971}
972:is(:root:not(.compact) .actionbar, .ctl-actions) .btn.claim.pung,
973:is(:root:not(.compact) .actionbar, .ctl-actions) .btn.claim.kong {
974 color: #1c527d;
975}
976:is(:root:not(.compact) .actionbar, .ctl-actions) .btn.claim.chow {
977 color: #1d6640;
978}
979/* 看牌 is the only button a covered hand has, and it is the one button here
980 that is not done *with* a tile — it is done to the card lying over them. So
981 it wears the back rather than the face, and reads as the thing beside it. */
982:is(:root:not(.compact) .actionbar, .ctl-actions) .btn.reveal {
983 background: url('./assets/back.png') center / 100% 100%;
984 color: var(--ivory);
985 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
986}
987/* And the table is waiting on this one. Whose go it is was never a secret, so
988 the ring says so — which is what stops four face-down hands and no buttons
989 reading as a table that has stopped rather than one waiting for somebody to
990 look at their tiles. */
991:is(:root:not(.compact) .actionbar, .ctl-actions) .btn.reveal.hot {
992 outline: 2px solid var(--gold);
993 outline-offset: -2px;
994 /* Gold off the tile's own outline rather than off a rectangle behind it: the
995 shadow already comes from the art's alpha, and a second glow drawn as a box
996 would put a corner where the tile has none. */
997 filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.45)) drop-shadow(0 0 6px rgba(217, 180, 92, 0.75));
998}
999/* 過 and 摸牌 are not calls, so they are the plain face — and a pass reads as
1000 the quiet one it is. */
1001:is(:root:not(.compact) .actionbar, .ctl-actions) .btn.pass,
1002:is(:root:not(.compact) .actionbar, .ctl-actions) .btn.ghost {
1003 color: rgba(33, 38, 43, 0.65);
1004}
1005/* The one that plays a tile wore a gold rim drawn round the face. The face is
1006 also where the English is written, along its bottom edge, and a line ruled
1007 there is a line ruled through the reading — so the rim is gone and the call
1008 itself carries the weight, the way the coloured ones do. */
1009.big-btn {
1010 font-size: 22px;
1011 padding: 12px 34px;
1012 min-height: 56px;
1013}
1014
1015/* ---------- the tiles in the middle ---------- */
1016/* One canvas over the whole table, drawn by ui/Pool.tsx. It has to span the
1017 table and not just the centre: the strips clip their own contents, so a tile
1018 thrown out of a hand would be cut off at the edge of its seat if it were an
1019 element. On the canvas there is nothing to clip it.
1020
1021 Layering: above .wall-ring (0), since the tiles land inside the square and in
1022 front of it; level with .facing, which sits later in the DOM and so keeps the
1023 centre readout on top; below the panels, which raise themselves. */
1024.tile-pool {
1025 position: absolute;
1026 inset: 0;
1027 width: 100%;
1028 height: 100%;
1029 pointer-events: none;
1030 z-index: 1;
1031}
1032/* The pile is handled directly: a discard is picked up, carried over the top of
1033 the others and set down or flicked away. The tiles are on a canvas with no
1034 elements to hit, so the hit test is against the physics and the listener is a
1035 capturing one on the table itself — see `dragging` in ui/Pool.tsx. Nothing
1036 here needs a box; `touch-action` is what stops a drag scrolling the page. */
1037.table {
1038 touch-action: none;
1039}
1040
1041/* ---------- center ---------- */
1042.center {
1043 position: absolute;
1044 inset: var(--depth);
1045 display: flex;
1046 flex-direction: column;
1047 align-items: center;
1048 justify-content: center;
1049 gap: 10px;
1050 pointer-events: none;
1051}
1052.center > * {
1053 pointer-events: auto;
1054}
1055/* Everything in the centre reads on top of the wall square. */
1056.info,
1057.last-discard,
1058.prompt,
1059.result,
1060.info {
1061 display: flex;
1062 flex-direction: column;
1063 align-items: center;
1064 gap: 2px;
1065 opacity: 0.9;
1066 text-align: center;
1067}
1068.info .round,
1069.info .wall {
1070 display: flex;
1071 flex-direction: column;
1072 align-items: center;
1073 line-height: 1.05;
1074}
1075.info .wall {
1076 margin-top: 10px;
1077 padding-top: 10px;
1078 border-top: 1px solid rgba(246, 240, 226, 0.14);
1079}
1080.info .big {
1081 font-size: 40px;
1082 font-weight: 400;
1083 color: var(--gold);
1084}
1085.info .lbl {
1086 font-size: 11px;
1087 letter-spacing: 0.06em;
1088 opacity: 0.6;
1089}
1090.info .hand-no {
1091 margin-top: 8px;
1092 font-size: 11px;
1093 opacity: 0.5;
1094 letter-spacing: 0.1em;
1095}
1096/* The wall, built the way it is built on a table: four walls laid out like a #
1097 rather than mitred into a closed square, each one carrying on past the corner
1098 of the next so no corner is a joint anybody had to cut.
1099
1100 Nothing here is turned. A square wants no rotation to describe it: each wall
1101 is pinned to its own edge of the opening, the two flat ones run their stacks
1102 across and the two upright ones run them down, and that is the whole
1103 difference between them. How big the square comes out is `wallSquare` in
1104 `game/wall.ts`, measured and applied by `ui/WallRing`; the size below is only
1105 of one stack.
1106
1107 `.wall-ring` is the opening, not the outside: the walls hang off its edges,
1108 so the square encloses the pool the way it does on a table. */
1109.wall-ring {
1110 /* A stack's short side: the table's tile, and nothing else. Every tile on the
1111 table is one size — in a hand, in the wall, and lying in the middle — so
1112 this is not a size the wall gets to have an opinion about. ui/WallRing
1113 measures what it came out as and says how long a wall of eighteen of them
1114 is (`--wall-len`); it does not decide either of them. */
1115 --ws: var(--tile-w);
1116 --wd: calc(var(--ws) * 1.375);
1117 /* A wall, end to end: eighteen stacks, set from ui/WallRing. It is longer
1118 than the edge of the opening by one wall-depth, and that overhang is what
1119 covers the corner. */
1120 --wall-len: 0px;
1121 position: absolute;
1122 top: 50%;
1123 left: 50%;
1124 width: 0;
1125 height: 0;
1126 transform: translate(-50%, -50%);
1127 pointer-events: none;
1128 z-index: 0;
1129}
1130/* Never drawn. It is here to be measured: how big a stack came out is CSS's
1131 answer, and the layout needs it in stacks before it has placed any. */
1132.wall-probe {
1133 position: absolute;
1134 visibility: hidden;
1135 width: var(--ws);
1136 height: var(--wd);
1137}
1138/* Each wall sits outside its own edge of the opening, flush with the corner it
1139 is built from and running one wall-depth past the far one — the same way
1140 round each time, which is what makes four straight walls a pinwheel. Every
1141 corner of the square is covered by exactly one of them, and no two overlap.
1142
1143 Every place in the wall is here whether or not there is still a stack on it —
1144 see `ringLayout` — so the row never closes up and the gap stays where the
1145 drawing actually got to. */
1146.wall-side {
1147 position: absolute;
1148 display: flex;
1149 /* Where it has been pushed to, in the table's own axes — a square wall goes
1150 where the finger goes. */
1151 transform: translate(var(--dx, 0px), var(--dy, 0px));
1152 /* The ring lets everything through; a wall is the one thing you can take
1153 hold of. */
1154 pointer-events: auto;
1155 cursor: grab;
1156}
1157.wall-side.top,
1158.wall-side.bottom {
1159 width: var(--wall-len);
1160 height: var(--wd);
1161}
1162.wall-side.left,
1163.wall-side.right {
1164 width: var(--wd);
1165 height: var(--wall-len);
1166}
1167/* Built from the top left corner, running right over the head of the right. */
1168.wall-side.top {
1169 left: 0;
1170 bottom: 100%;
1171}
1172/* From the top right, running down. */
1173.wall-side.right {
1174 left: 100%;
1175 top: 0;
1176 flex-direction: column;
1177}
1178/* From the bottom right, running left. */
1179.wall-side.bottom {
1180 right: 0;
1181 top: 100%;
1182 flex-direction: row-reverse;
1183}
1184/* From the bottom left, running up. */
1185.wall-side.left {
1186 right: 100%;
1187 bottom: 0;
1188 flex-direction: column-reverse;
1189}
1190.wall-stack {
1191 position: relative;
1192 flex: none;
1193 width: var(--ws);
1194 height: var(--wd);
1195}
1196/* Lying the other way about, which is how the two upright walls are built. */
1197.wall-side.left > .wall-stack,
1198.wall-side.right > .wall-stack {
1199 width: var(--wd);
1200 height: var(--ws);
1201}
1202/* The tile itself fills its stack, whichever way round the stack is lying — so
1203 the two upright walls need nothing said about them here. */
1204.wall-stack i {
1205 position: absolute;
1206 inset: 0;
1207 background-image: url('./assets/back.png');
1208 background-size: 100% 100%;
1209 /* A box-shadow, not a drop-shadow. The square is seventy-two stacks now
1210 rather than the twenty-odd that were left after a deal: `filter` gives each
1211 its own layer to rasterise, and seventy-two of those being redrawn while a
1212 wall is pushed about is enough to make the page stop answering. The tile is
1213 a rectangle, so the cheap shadow is the same picture. */
1214 box-shadow: 0 1px 1px rgba(0, 0, 0, 0.45);
1215}
1216/* One tile left in the stack. */
1217.wall-stack.s1 i {
1218 opacity: 0.8;
1219 transform: scale(0.86);
1220}
1221.wall-stack.next::after {
1222 content: '';
1223 position: absolute;
1224 inset: -2px;
1225 border: 2px solid var(--gold);
1226 border-radius: 3px;
1227 box-shadow: 0 0 12px rgba(217, 180, 92, 0.7);
1228}
1229
1230/* Text only, and it sits over the pool — so it must not swallow a finger that
1231 is reaching in to wash the tiles about. */
1232.facing {
1233 pointer-events: none;
1234 position: relative;
1235 z-index: 1;
1236 display: flex;
1237 flex-direction: column;
1238 align-items: center;
1239 gap: 8px;
1240 transition: transform 0.35s ease;
1241}
1242
1243.last-discard {
1244 display: flex;
1245 flex-direction: column;
1246 align-items: center;
1247 gap: 6px;
1248 --tile-w: 64px;
1249}
1250.ld-label {
1251 font-size: 12px;
1252 opacity: 0.6;
1253}
1254.prompt {
1255 font-size: 15px;
1256 letter-spacing: 0.1em;
1257 opacity: 0.8;
1258}
1259.prompt.warn {
1260 color: #ffb4a2;
1261 font-weight: 600;
1262}
1263.result {
1264 /* Over the pile in the middle, not under it. */
1265 z-index: 2;
1266 background: rgba(10, 30, 24, 0.94);
1267 border: 1px solid rgba(217, 180, 92, 0.45);
1268 border-radius: 14px;
1269 padding: 18px 26px;
1270 display: flex;
1271 flex-direction: column;
1272 align-items: center;
1273 gap: 10px;
1274 max-height: 90%;
1275 overflow: auto;
1276}
1277.result h2 {
1278 margin: 0;
1279 font-size: 24px;
1280 color: var(--gold);
1281}
1282.result .wt {
1283 color: var(--ivory);
1284 opacity: 0.8;
1285}
1286.tai-list {
1287 display: grid;
1288 grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
1289 gap: 2px 18px;
1290 font-size: 14px;
1291 width: 100%;
1292}
1293.tai-item {
1294 display: flex;
1295 align-items: baseline;
1296 justify-content: space-between;
1297 gap: 12px;
1298 padding: 2px 0;
1299 border-bottom: 1px dotted rgba(246, 240, 226, 0.2);
1300}
1301.tai-name {
1302 display: flex;
1303 flex-direction: column;
1304 line-height: 1.15;
1305}
1306.tai-name em {
1307 font-style: normal;
1308 font-size: 10px;
1309 letter-spacing: 0.08em;
1310 text-transform: uppercase;
1311 opacity: 0.55;
1312}
1313.tai-val {
1314 font-weight: 700;
1315 color: var(--gold);
1316}
1317/* 胡牌 — the arrow that lands on the winner's edge of the table. Sits over the
1318 strip rather than in it, so the hand underneath keeps the height it had and
1319 nothing shifts when it appears. */
1320.win-arrow {
1321 position: absolute;
1322 top: 0;
1323 left: 50%;
1324 font-size: 52px;
1325 line-height: 0.9;
1326 color: var(--gold);
1327 text-shadow: 0 0 6px #0a3126, 0 0 20px rgba(217, 180, 92, 0.9);
1328 pointer-events: none;
1329 z-index: 4;
1330 animation: arrow-nudge 0.9s ease-in-out infinite;
1331}
1332@keyframes arrow-nudge {
1333 0%,
1334 100% {
1335 transform: translate(-50%, 0);
1336 }
1337 50% {
1338 transform: translate(-50%, 9px);
1339 }
1340}
1341
1342/* 流局 — who was there and what they were sitting on. */
1343.ready-list {
1344 display: flex;
1345 flex-direction: column;
1346 gap: 6px;
1347 margin-top: 2px;
1348}
1349.ready-row {
1350 display: flex;
1351 align-items: center;
1352 gap: 8px;
1353 font-size: 14px;
1354}
1355.ready-row.none {
1356 opacity: 0.45;
1357}
1358.ready-name {
1359 min-width: 6.5em;
1360 text-align: right;
1361}
1362.ready-row .tag.ting {
1363 font-size: 11px;
1364 padding: 1px 6px;
1365 border-radius: 999px;
1366 background: rgba(46, 170, 120, 0.2);
1367 border: 1px solid rgba(46, 170, 120, 0.6);
1368 white-space: nowrap;
1369}
1370.ready-tiles {
1371 display: flex;
1372 flex-wrap: wrap;
1373 gap: 2px;
1374 /* Tiles size themselves off their strip's --tile-w, and the centre is not in
1375 one — without a size of its own here they come out 0 x 0 and vanish. */
1376 --tile-w: 46px;
1377}
1378.ready-no {
1379 font-size: 12px;
1380 letter-spacing: 0.04em;
1381}
1382
1383.deltas {
1384 display: flex;
1385 gap: 16px;
1386 font-size: 14px;
1387 opacity: 0.9;
1388}
1389.deltas .up {
1390 color: #5fd39a;
1391}
1392.deltas .down {
1393 color: #ff8a80;
1394}
1395
1396/* ---------- help panel ---------- */
1397.helpbtn.gear {
1398 font-size: 13px;
1399}
1400.helpbtn {
1401 margin-left: 2px;
1402 width: 22px;
1403 height: 22px;
1404 border-radius: 50%;
1405 border: 1px solid rgba(246, 240, 226, 0.35);
1406 background: transparent;
1407 color: rgba(246, 240, 226, 0.75);
1408 font-family: inherit;
1409 font-size: 12px;
1410 line-height: 1;
1411 cursor: pointer;
1412}
1413/* The rules panel gets its own deeper slot so it is comfortable to read. */
1414.help-slot {
1415 position: absolute;
1416 inset: 0;
1417 z-index: 30;
1418 background: rgba(4, 18, 14, 0.55);
1419}
1420.help-slot.slot-bottom,
1421.help-slot.slot-top,
1422.help-slot.slot-left,
1423.help-slot.slot-right {
1424 inset: 0;
1425 width: auto;
1426 height: auto;
1427}
1428.help {
1429 position: absolute;
1430 top: 50%;
1431 left: 50%;
1432 height: var(--hd);
1433 z-index: 31;
1434 display: flex;
1435 flex-direction: column;
1436 background: rgba(8, 32, 25, 0.985);
1437 border: 1px solid rgba(217, 180, 92, 0.4);
1438 border-radius: 12px;
1439 padding: 10px 18px 14px;
1440 box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
1441}
1442/* Anchored to the asking player's own edge, and rotated to face them. The
1443 leading translate is applied last, so it shifts the box in screen space. */
1444.help-slot.slot-bottom .help,
1445.help-slot.slot-top .help {
1446 width: calc(100vw - 60px);
1447 --hd: min(52vh, 560px);
1448 --off: calc(50vh - var(--hd) / 2 - 14px);
1449}
1450.help-slot.slot-left .help,
1451.help-slot.slot-right .help {
1452 width: calc(100vh - 60px);
1453 --hd: min(52vw, 560px);
1454 --off: calc(50vw - var(--hd) / 2 - 14px);
1455}
1456.help-slot.slot-bottom .help {
1457 translate: 0 var(--off);
1458}
1459.help-slot.slot-top .help {
1460 translate: 0 calc(var(--off) * -1);
1461}
1462.help-slot.slot-left .help {
1463 translate: calc(var(--off) * -1) 0;
1464}
1465.help-slot.slot-right .help {
1466 translate: var(--off) 0;
1467}
1468.help-head {
1469 display: flex;
1470 align-items: center;
1471 justify-content: space-between;
1472 gap: 16px;
1473 flex: none;
1474}
1475.help-head h3 {
1476 margin: 0;
1477 font-size: 15px;
1478 font-weight: 400;
1479 letter-spacing: 0.08em;
1480 color: var(--gold);
1481}
1482.help-head .btn {
1483 min-height: 30px;
1484 padding: 2px 12px;
1485 font-size: 13px;
1486}
1487.help-body {
1488 flex: 1 1 0;
1489 min-height: 0;
1490 overflow-y: auto;
1491 columns: 4;
1492 column-gap: 26px;
1493 column-fill: auto;
1494 padding-top: 8px;
1495 overflow-x: auto;
1496 scrollbar-width: thin;
1497}
1498.help-body section {
1499 break-inside: avoid;
1500 margin-bottom: 10px;
1501}
1502.help-body h4 {
1503 margin: 0 0 2px;
1504 font-size: 13px;
1505 font-weight: 400;
1506 color: var(--gold);
1507 letter-spacing: 0.06em;
1508}
1509.help-body h4 em {
1510 font-style: normal;
1511 font-size: 9px;
1512 letter-spacing: 0.12em;
1513 text-transform: uppercase;
1514 opacity: 0.55;
1515 margin-left: 4px;
1516}
1517.help-body p {
1518 margin: 0 0 4px;
1519 display: flex;
1520 flex-direction: column;
1521 line-height: 1.35;
1522 font-size: 11.5px;
1523}
1524.help-body p em {
1525 font-style: normal;
1526 font-size: 10.5px;
1527 opacity: 0.6;
1528}
1529.help-body .credit {
1530 opacity: 0.6;
1531}
1532
1533/* ---------- lobby resume ---------- */
1534.lobby .sub {
1535 font-size: 0.95rem;
1536}
1537.resume {
1538 margin-top: 1.4rem;
1539 padding: 12px 26px;
1540 border: 1px solid rgba(217, 180, 92, 0.35);
1541 border-radius: 12px;
1542 background: rgba(0, 0, 0, 0.2);
1543 text-align: center;
1544 display: flex;
1545 flex-direction: column;
1546 gap: 6px;
1547}
1548.resume-line {
1549 font-size: 14px;
1550 color: var(--gold);
1551 letter-spacing: 0.06em;
1552}
1553.resume-scores {
1554 display: flex;
1555 gap: 18px;
1556 justify-content: center;
1557 font-size: 13px;
1558 opacity: 0.8;
1559}
1560.resume-scores b {
1561 color: var(--gold);
1562 font-weight: 600;
1563}
1564.resume-when {
1565 font-size: 11px;
1566 opacity: 0.45;
1567 letter-spacing: 0.08em;
1568}
1569.lobby-buttons {
1570 display: flex;
1571 flex-direction: column;
1572 gap: 12px;
1573 margin-top: 1.6rem;
1574 align-items: stretch;
1575}
1576/* The two ways to play carry a line of explanation under the name. */
1577.lobby-buttons .big-btn {
1578 display: flex;
1579 flex-direction: column;
1580 gap: 3px;
1581 min-width: 360px;
1582 padding: 12px 28px;
1583}
1584.lobby-buttons .big-btn em {
1585 font-style: normal;
1586 font-size: 12px;
1587 font-weight: 400;
1588 letter-spacing: 0.03em;
1589 opacity: 0.72;
1590}
1591
1592/* A window with the width for the table but not the height for the lobby. The
1593 compact layout does not start until 620px tall, so a 700px laptop window got
1594 the full-size lobby: a 112px title, a saved game, four ways to start and the
1595 settings row, which comes to more than the window has. Everything gives a
1596 little, the same way the compact rules give, so the last of it lands above
1597 the fold rather than behind a scroll nobody goes looking for.
1598
1599 The title is the first to go, because it is the one thing sized off width
1600 alone: 9vw of a wide short window is a title taller than a button. */
1601@media (max-height: 860px) {
1602 .lobby {
1603 gap: 0.25rem;
1604 }
1605 .lobby h1 {
1606 font-size: clamp(2.4rem, min(9vw, 10vh), 7rem);
1607 margin-bottom: 0.2rem;
1608 }
1609 .resume {
1610 margin-top: 0.8rem;
1611 padding: 8px 20px;
1612 }
1613 .lobby .hint {
1614 margin-bottom: 0.6rem;
1615 }
1616 .lobby-buttons {
1617 margin-top: 0.9rem;
1618 gap: 8px;
1619 }
1620 .lobby-buttons .big-btn {
1621 padding: 7px 28px;
1622 }
1623 .lobby-extras {
1624 margin-top: 0.8rem;
1625 }
1626}
1627
1628/* ---------- sound ---------- */
1629.lobby-extras {
1630 margin-top: 1.4rem;
1631 display: flex;
1632 gap: 12px;
1633 align-items: center;
1634}
1635/* Small rounded button for the things that sit under the main choices. */
1636.btn.pill {
1637 min-height: 34px;
1638 padding: 2px 14px;
1639 font-size: 13px;
1640 letter-spacing: 0.06em;
1641 border-radius: 999px;
1642 gap: 8px;
1643}
1644.sound-toggle.on {
1645 border-color: rgba(217, 180, 92, 0.7);
1646 color: var(--gold);
1647}
1648.sound-toggle.compact {
1649 min-height: 28px;
1650 padding: 2px 9px;
1651 font-size: 15px;
1652}
1653.sound-toggle .spk {
1654 font-size: 15px;
1655 line-height: 1;
1656}
1657
1658/* ---------- language ---------- */
1659/* Sized to stand beside the sound and fullscreen pills in both their forms. */
1660.lang-toggle {
1661 min-height: 34px;
1662 padding: 2px 14px;
1663}
1664.lang-toggle.compact {
1665 min-height: 28px;
1666 padding: 2px 9px;
1667}
1668/* A boxed glyph, so 雙 / 中 / A all take the same room and the row does not
1669 shuffle sideways as the mode steps round. */
1670.lang-toggle .lang-icon {
1671 display: inline-grid;
1672 place-items: center;
1673 width: 18px;
1674 height: 18px;
1675 flex: none;
1676 border: 1px solid rgba(230, 226, 214, 0.45);
1677 border-radius: 4px;
1678 font-size: 12px;
1679 line-height: 1;
1680}
1681
1682/* ---------- shared table controls ---------- */
1683/* Bottom-right of the centre square, opposite the log. Unrotated on purpose:
1684 it belongs to the table, not to a seat. */
1685/* Out of the way of a throw. The undo button is a wide target lying on the
1686 playing surface at the bottom-right of the middle, which is exactly where a
1687 tile leaves seat 0's hand — see the `carrying` class in ui/Pool.tsx. */
1688.table.carrying .table-controls {
1689 pointer-events: none;
1690}
1691.table-controls {
1692 position: absolute;
1693 /* Over the pile, same as .result. */
1694 z-index: 2;
1695 right: 10px;
1696 bottom: 8px;
1697 display: flex;
1698 align-items: center;
1699 gap: 8px;
1700}
1701.btn.undo {
1702 min-height: 34px;
1703 padding: 3px 14px;
1704 border-radius: 999px;
1705 gap: 9px;
1706 max-width: 40vh;
1707}
1708.undo-arrow {
1709 font-size: 17px;
1710 line-height: 1;
1711 color: var(--gold);
1712}
1713.undo-what {
1714 display: flex;
1715 flex-direction: column;
1716 align-items: flex-start;
1717 line-height: 1.15;
1718 min-width: 0;
1719}
1720.undo-what .zh {
1721 font-size: 13px;
1722 letter-spacing: 0.05em;
1723}
1724.undo-what .en {
1725 font-size: 9px;
1726 letter-spacing: 0.09em;
1727 text-transform: uppercase;
1728 opacity: 0.55;
1729 white-space: nowrap;
1730 overflow: hidden;
1731 text-overflow: ellipsis;
1732 max-width: 100%;
1733}
1734
1735/* ---------- result: one block per winner ---------- */
1736.result .win {
1737 display: flex;
1738 flex-direction: column;
1739 align-items: center;
1740 gap: 8px;
1741 width: 100%;
1742}
1743.result .win + .win {
1744 padding-top: 10px;
1745 border-top: 1px solid rgba(217, 180, 92, 0.3);
1746}
1747.result .liable {
1748 font-size: 12px;
1749 letter-spacing: 0.08em;
1750 color: #ffb4a2;
1751}
1752.result .multi {
1753 font-size: 12px;
1754 letter-spacing: 0.1em;
1755 color: var(--gold);
1756 opacity: 0.85;
1757}
1758
1759/* ---------- settings ---------- */
1760.over-buttons {
1761 display: flex;
1762 gap: 12px;
1763 align-items: center;
1764}
1765.settings-scrim {
1766 position: absolute;
1767 inset: 0;
1768 z-index: 40;
1769 background: rgba(4, 18, 14, 0.72);
1770 display: flex;
1771 align-items: center;
1772 justify-content: center;
1773 padding: 20px;
1774}
1775.settings {
1776 width: min(1180px, 100%);
1777 max-height: 100%;
1778 display: flex;
1779 flex-direction: column;
1780 background: rgba(8, 32, 25, 0.99);
1781 border: 1px solid rgba(217, 180, 92, 0.4);
1782 border-radius: 14px;
1783 box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
1784 padding: 16px 28px 22px;
1785}
1786.settings-head {
1787 display: flex;
1788 align-items: center;
1789 justify-content: space-between;
1790 gap: 16px;
1791 flex: none;
1792 padding-bottom: 12px;
1793 border-bottom: 1px solid rgba(246, 240, 226, 0.12);
1794}
1795.settings-head h3 {
1796 margin: 0;
1797 font-size: 22px;
1798 font-weight: 400;
1799 letter-spacing: 0.08em;
1800 color: var(--gold);
1801}
1802.settings-head-btns {
1803 display: flex;
1804 gap: 10px;
1805}
1806.settings-head .btn {
1807 min-height: 46px;
1808 padding: 2px 22px;
1809 font-size: 17px;
1810}
1811.settings-body {
1812 /* `1 1 auto`, not `1 1 0`: the panel's own height comes from this content,
1813 so a zero basis collapses the pair of them to nothing. `max-height` on the
1814 panel is what brings the scrollbar back at the other end. */
1815 flex: 1 1 auto;
1816 min-height: 0;
1817 overflow-y: auto;
1818 scrollbar-width: thin;
1819 padding-top: 14px;
1820 /* Columns rather than a grid: grid rows would size to the tallest section
1821 and leave the shorter one padded with air, which is what pushed the last
1822 house rule off the bottom. */
1823 columns: 2;
1824 column-gap: 44px;
1825}
1826.settings-body section {
1827 break-inside: avoid;
1828 margin-bottom: 22px;
1829}
1830.settings-body h4 {
1831 margin: 0 0 10px;
1832 font-size: 16px;
1833 font-weight: 400;
1834 color: var(--gold);
1835 letter-spacing: 0.06em;
1836}
1837.settings-body h4 em {
1838 font-style: normal;
1839 font-size: 11px;
1840 letter-spacing: 0.12em;
1841 text-transform: uppercase;
1842 opacity: 0.55;
1843 margin-left: 5px;
1844}
1845.settings .field {
1846 display: flex;
1847 align-items: center;
1848 gap: 14px;
1849 padding: 5px 0;
1850}
1851.settings .f-label {
1852 display: flex;
1853 align-items: baseline;
1854 gap: 6px;
1855 font-size: 16px;
1856 /* Wide enough for 起始籌碼 STARTING CHIPS, so every input in a section
1857 starts at the same place. */
1858 min-width: 196px;
1859 opacity: 0.85;
1860}
1861/* ...except the player rows, which are labelled 1–4 and need no such room. */
1862.settings .names .f-label {
1863 min-width: 26px;
1864}
1865.settings .f-label em {
1866 font-style: normal;
1867 font-size: 11px;
1868 letter-spacing: 0.1em;
1869 text-transform: uppercase;
1870 opacity: 0.55;
1871}
1872.settings input[type='text'],
1873.settings input:not([type]),
1874.settings input[type='number'] {
1875 flex: 1 1 0;
1876 min-width: 0;
1877 font-family: inherit;
1878 font-size: 18px;
1879 color: var(--ivory);
1880 background: rgba(0, 0, 0, 0.28);
1881 border: 1px solid rgba(246, 240, 226, 0.25);
1882 border-radius: 8px;
1883 padding: 11px 14px;
1884 min-height: 48px;
1885 user-select: text;
1886}
1887.settings input:focus {
1888 outline: none;
1889 border-color: rgba(217, 180, 92, 0.8);
1890}
1891.settings input[type='number'] {
1892 max-width: 150px;
1893}
1894.settings input[type='range'] {
1895 flex: 1 1 0;
1896 height: 34px;
1897 accent-color: var(--gold);
1898}
1899.settings input:disabled {
1900 opacity: 0.4;
1901}
1902.settings .switch {
1903 display: grid;
1904 grid-template-columns: auto 1fr;
1905 gap: 3px 14px;
1906 align-items: center;
1907 padding: 9px 0;
1908 cursor: pointer;
1909}
1910.settings .switch input {
1911 width: 26px;
1912 height: 26px;
1913 accent-color: var(--gold);
1914 cursor: pointer;
1915}
1916.settings .sw-name {
1917 display: flex;
1918 align-items: baseline;
1919 gap: 8px;
1920 font-size: 17px;
1921}
1922.settings .sw-name em {
1923 font-style: normal;
1924 font-size: 11px;
1925 letter-spacing: 0.1em;
1926 text-transform: uppercase;
1927 opacity: 0.55;
1928}
1929.settings .note {
1930 grid-column: 2;
1931 font-size: 13px;
1932 line-height: 1.4;
1933 opacity: 0.55;
1934}
1935.settings .switch.dead {
1936 opacity: 0.4;
1937 cursor: default;
1938}
1939.settings .note.lock {
1940 grid-column: auto;
1941 margin: 4px 0 0;
1942 color: #ffb4a2;
1943 opacity: 0.75;
1944}
1945
1946/* ============================================================================
1947 compact — a phone
1948
1949 The table layout is four strips of --depth arranged around a centre square,
1950 which wants about 700px each way; a phone has that in neither, and in
1951 landscape the top and bottom strips simply overlap. It also assumes four
1952 people sitting around a screen lying flat, which is not what a phone is.
1953
1954 Compact throws both assumptions out. Nothing is rotated — every label reads
1955 upright for the one person holding the phone — the table becomes a grid, the
1956 three other seats shrink to cards showing what is public about them, and the
1957 depth that frees up goes to the hand, which wraps when it has to.
1958
1959 The `compact` class is put on <html> by useCompact(); the breakpoint itself
1960 lives in ui/compact.ts, so it is written down once.
1961 ============================================================================ */
1962
1963.compact .table {
1964 display: grid;
1965 gap: 2px;
1966 /* Landscape is where a notch eats into the sides. */
1967 padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
1968 env(safe-area-inset-left);
1969 grid-template-columns: 1fr 1fr 1fr;
1970 grid-template-rows: minmax(0, 1fr) auto auto;
1971 grid-template-areas:
1972 'left across right'
1973 'bar bar bar'
1974 'mine mine mine';
1975}
1976.compact .slot {
1977 position: relative;
1978 inset: auto;
1979 width: auto;
1980 height: auto;
1981 min-width: 0;
1982 min-height: 0;
1983}
1984/* `.slot` and not the edge class alone: the rules sheet carries an edge class
1985 of its own, and would otherwise be parked in the grid area named here. */
1986.compact .slot.slot-left {
1987 grid-area: left;
1988}
1989.compact .slot.slot-top {
1990 grid-area: across;
1991}
1992.compact .slot.slot-right {
1993 grid-area: right;
1994}
1995.compact .slot.slot-bottom {
1996 grid-area: mine;
1997}
1998.compact .center {
1999 grid-area: bar;
2000}
2001/* The strip no longer floats in the middle of a slot — it is the slot. */
2002.compact .seat {
2003 position: relative;
2004 top: auto;
2005 left: auto;
2006 width: auto;
2007 height: 100%;
2008 padding: 2px 4px 3px;
2009}
2010/* Off to one side rather than dead centre: a card is narrow enough that the
2011 arrow would otherwise land on the name it is pointing out. */
2012.compact .win-arrow {
2013 font-size: 24px;
2014 left: 14px;
2015}
2016
2017/* ---------- the other three seats ---------- */
2018/* Read top-down: who they are, how many tiles they hold, what they have laid
2019 down, what they have thrown. The hand is the least interesting of those and
2020 goes smallest; the discards get whatever room is left, since reading them is
2021 the whole of playing against somebody. */
2022.compact .opp .seat {
2023 --tile-w: clamp(13px, calc((100vw / 3 - 20px) / 8.6), 32px);
2024 justify-content: flex-start;
2025 gap: 2px;
2026 border: 1px solid rgba(246, 240, 226, 0.09);
2027 border-radius: 8px;
2028 background: rgba(0, 0, 0, 0.14);
2029}
2030.compact .opp .nameplate {
2031 order: 0;
2032}
2033.compact .opp .hand {
2034 order: 1;
2035}
2036.compact .opp .melds {
2037 order: 2;
2038}
2039.compact .opp .discards {
2040 order: 3;
2041}
2042.compact .opp .actionbar {
2043 order: 4;
2044}
2045
2046.compact .opp .nameplate {
2047 flex-wrap: wrap;
2048 justify-content: center;
2049 gap: 3px 5px;
2050 font-size: 10px;
2051 padding-top: 0;
2052 row-gap: 1px;
2053}
2054.compact .opp .nameplate .wind {
2055 font-size: 13px;
2056}
2057.compact .opp .nameplate .wind em,
2058.compact .opp .nameplate .helpbtn,
2059.compact .opp .nameplate .sort {
2060 display: none;
2061}
2062.compact .opp .nameplate .chips {
2063 font-size: 13px;
2064 margin-left: 0;
2065}
2066.compact .opp .nameplate .tag {
2067 font-size: 9px;
2068 padding: 0 4px;
2069}
2070/* Face-down backs: a count you can see at a glance, nothing more. */
2071.compact .opp .hand {
2072 flex-wrap: wrap;
2073 justify-content: center;
2074 height: auto;
2075 padding-top: 0;
2076 gap: 1px;
2077 max-width: 100%;
2078}
2079.compact .opp .hand .tile {
2080 --tw: calc(var(--tile-w) * 0.52);
2081}
2082.compact .opp .melds {
2083 gap: 5px;
2084 min-height: 0;
2085 max-width: 100%;
2086}
2087/* Same size as the hand they were laid down from — which on an opponent's card
2088 is that card's own smaller tile, not the table's. */
2089.compact .opp .melds .tile {
2090 --tw: calc(var(--tile-w) * 0.52);
2091}
2092.compact .opp .meld.flowers {
2093 margin-left: 5px;
2094 padding-left: 5px;
2095}
2096.compact .opp .discards {
2097 flex: 1 1 auto;
2098 max-width: 100%;
2099 min-height: 0;
2100 overflow-y: auto;
2101 align-content: flex-start;
2102}
2103/* No height is reserved here: an opponent's bar is empty except in hotseat,
2104 and a card that stays still matters less than the room it costs. */
2105.compact .opp .actionbar {
2106 min-height: 0;
2107 padding-right: 0;
2108 justify-content: center;
2109 gap: 4px;
2110}
2111.compact .opp .actionbar .btn {
2112 height: auto;
2113 min-height: 34px;
2114 padding: 2px 8px;
2115}
2116.compact .opp .actionbar .bi .zh {
2117 font-size: 14px;
2118}
2119.compact .opp .delta {
2120 font-size: 15px;
2121}
2122
2123/* ---------- your own seat ---------- */
2124/* A grid rather than the stack the strips use, so the melds and the discards
2125 share one line and the hand gets the depth that saves. */
2126.compact .mine .seat {
2127 --tile-w: min(calc((100vw - 24px) / 18.3), 54px);
2128 display: grid;
2129 /* `minmax(0, auto)`, so a full set of melds wraps inside its column instead
2130 of shouldering the discards off the end of the strip. */
2131 grid-template-columns: minmax(0, auto) minmax(0, 1fr);
2132 grid-template-areas:
2133 'melds discards'
2134 'bar bar'
2135 'hand hand'
2136 'name name';
2137 align-content: end;
2138 align-items: end;
2139 justify-items: center;
2140 gap: 2px 8px;
2141 padding: 2px 6px 3px;
2142}
2143.compact .mine .melds {
2144 grid-area: melds;
2145 justify-content: flex-start;
2146 min-height: 0;
2147 gap: 6px;
2148}
2149/* Full size, like the hand under it — the discards beside them stay at half,
2150 which is now the whole of what tells the two rows apart. */
2151.compact .mine .melds .tile {
2152 --tw: calc(var(--tile-w) * 0.72);
2153}
2154.compact .mine .discards {
2155 grid-area: discards;
2156 /* One line that scrolls, kept pinned to its end by Seat.tsx — wrapping would
2157 cost a second row of depth the hand wants more. Reserved even while empty,
2158 since this row and the melds beside it share it: without that the strip
2159 would jump the first time either one appeared. */
2160 flex: none;
2161 flex-wrap: nowrap;
2162 justify-content: flex-start;
2163 align-self: end;
2164 max-width: 100%;
2165 width: 100%;
2166 min-height: calc(var(--tile-w) * 0.9);
2167 margin-bottom: 0;
2168 overflow-x: auto;
2169 overflow-y: hidden;
2170 /* Melds and discards end up shoulder to shoulder here, and both are rows of
2171 tiles — the rule and the smaller face are what tell them apart. */
2172 border-left: 1px solid rgba(246, 240, 226, 0.16);
2173 padding-left: 8px;
2174 opacity: 0.85;
2175}
2176.compact .mine .discards .tile {
2177 --tw: calc(var(--tile-w) * 0.5);
2178}
2179.compact .mine .actionbar {
2180 grid-area: bar;
2181 --bar-h: 44px;
2182 justify-content: center;
2183 padding-right: 0;
2184 gap: 6px;
2185}
2186.compact .mine .hand {
2187 grid-area: hand;
2188 flex-wrap: wrap;
2189 align-content: flex-end;
2190 justify-content: center;
2191 height: auto;
2192 max-width: 100%;
2193 /* The row gap matches the padding for the same reason: the 摸 tag hangs above
2194 its tile, and on a wrapped hand that is somebody else's row. */
2195 gap: calc(var(--tile-w) * 0.34) 2px;
2196 padding-top: calc(var(--tile-w) * 0.34);
2197}
2198.compact .mine .nameplate {
2199 grid-area: name;
2200 flex-wrap: wrap;
2201 justify-content: center;
2202 gap: 3px 7px;
2203 font-size: 11px;
2204 padding-top: 0;
2205}
2206.compact .mine .nameplate .wind {
2207 font-size: 15px;
2208}
2209.compact .mine .nameplate .wind em {
2210 display: none;
2211}
2212.compact .mine .nameplate .chips {
2213 font-size: 14px;
2214}
2215.compact .mine .nameplate .btn.sort {
2216 min-height: 26px;
2217 padding: 1px 9px;
2218 font-size: 12px;
2219}
2220.compact .mine .nameplate .helpbtn {
2221 width: 26px;
2222 height: 26px;
2223 font-size: 13px;
2224}
2225
2226/* ---------- the middle ---------- */
2227/* One bar across the table rather than a square in the middle of it. */
2228.compact .center {
2229 position: relative;
2230 inset: auto;
2231 flex-direction: row;
2232 align-items: center;
2233 justify-content: space-between;
2234 gap: 8px;
2235 padding: 1px 6px;
2236 min-height: 44px;
2237}
2238.compact .facing {
2239 flex: 1 1 auto;
2240 flex-direction: row;
2241 align-items: center;
2242 justify-content: center;
2243 gap: 10px;
2244 min-width: 0;
2245}
2246.compact .last-discard {
2247 --tile-w: 30px;
2248 flex-direction: row;
2249 align-items: center;
2250 gap: 6px;
2251}
2252.compact .ld-label {
2253 font-size: 10px;
2254}
2255.compact .prompt {
2256 font-size: 12px;
2257 text-align: center;
2258}
2259.compact .table-controls {
2260 position: static;
2261 flex: none;
2262}
2263.compact .btn.undo {
2264 max-width: 34vw;
2265 min-height: 30px;
2266 padding: 2px 10px;
2267}
2268.compact .undo-what .zh {
2269 font-size: 11px;
2270}
2271.compact .undo-what .en {
2272 display: none;
2273}
2274/* What the wall square was telling you, in the room a bar has. */
2275.wall-pill {
2276 display: flex;
2277 align-items: baseline;
2278 gap: 5px;
2279 flex: none;
2280 letter-spacing: 0.06em;
2281 opacity: 0.75;
2282}
2283.wall-pill b {
2284 font-size: 18px;
2285 font-weight: 900;
2286 color: var(--gold);
2287}
2288.wall-pill em {
2289 font-style: normal;
2290 font-size: 9px;
2291 text-transform: uppercase;
2292 opacity: 0.7;
2293}
2294
2295/* The hand result covers the table instead of sitting in a square that is no
2296 longer there. */
2297.compact .result {
2298 position: fixed;
2299 top: 50%;
2300 left: 50%;
2301 transform: translate(-50%, -50%);
2302 z-index: 25;
2303 max-width: min(560px, 94vw);
2304 max-height: 92dvh;
2305 padding: 12px 16px;
2306 gap: 8px;
2307}
2308.compact .result h2 {
2309 font-size: 18px;
2310}
2311.compact .tai-list {
2312 grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
2313 gap: 1px 12px;
2314 font-size: 12px;
2315}
2316.compact .ready-row {
2317 font-size: 12px;
2318}
2319.compact .ready-name {
2320 min-width: 4.5em;
2321}
2322.compact .ready-tiles {
2323 --tile-w: 26px;
2324}
2325.compact .deltas {
2326 flex-wrap: wrap;
2327 justify-content: center;
2328 gap: 6px 14px;
2329 font-size: 12px;
2330}
2331.compact .big-btn {
2332 font-size: 18px;
2333 padding: 8px 22px;
2334 min-height: 44px;
2335}
2336
2337/* ---------- portrait ---------- */
2338/* Held upright there is no room for three cards side by side, so they stack in
2339 the order play reaches them, and the hand wraps to two rows. */
2340@media (orientation: portrait) {
2341 .compact .table {
2342 grid-template-columns: 1fr;
2343 grid-template-rows: repeat(3, minmax(0, 1fr)) auto auto;
2344 grid-template-areas:
2345 'right'
2346 'across'
2347 'left'
2348 'bar'
2349 'mine';
2350 }
2351 /* A card gets the whole width here, so the tiles can be read rather than
2352 merely counted; the discards wrap to a second line when they need it. */
2353 .compact .opp .seat {
2354 --tile-w: clamp(18px, calc((100vw - 26px) / 13), 34px);
2355 }
2356 .compact .mine .seat {
2357 --tile-w: min(calc((100vw - 26px) / 9.4), 54px);
2358 }
2359 /* A phone's width will not hold the wall count, who discarded what, the claim
2360 prompt and the table's own buttons on one line. The readout — the part that
2361 changes — takes a line of its own underneath. The room comes out of the
2362 other seats, which are the flexible rows, so the hand never moves. */
2363 .compact .center {
2364 flex-wrap: wrap;
2365 row-gap: 2px;
2366 }
2367 .compact .wall-pill {
2368 order: 0;
2369 }
2370 .compact .table-controls {
2371 order: 1;
2372 }
2373 .compact .facing {
2374 order: 2;
2375 flex-basis: 100%;
2376 }
2377}
2378
2379/* ---------- the panels ---------- */
2380.compact .lobby {
2381 justify-content: flex-start;
2382 overflow-y: auto;
2383 gap: 0.25rem;
2384 padding: 12px 14px calc(14px + env(safe-area-inset-bottom));
2385}
2386.compact .lobby > * {
2387 flex: none;
2388}
2389/* Sized off both axes: a phone held sideways has width to spare and no height
2390 at all, and the title was the first thing to push the buttons off. */
2391.compact .lobby h1 {
2392 font-size: clamp(1.9rem, min(9vw, 11vh), 3.2rem);
2393 margin-bottom: 0.2rem;
2394}
2395.compact .lobby .sub {
2396 font-size: 0.78rem;
2397}
2398/* The screen dividers this asks for are a four-round-a-table idea, and nobody
2399 props cards against a phone. */
2400.compact .lobby .hint {
2401 display: none;
2402}
2403.compact .resume {
2404 margin-top: 0.8rem;
2405 padding: 8px 16px;
2406}
2407.compact .resume-scores {
2408 flex-wrap: wrap;
2409 gap: 4px 12px;
2410 font-size: 12px;
2411}
2412.compact .lobby-buttons {
2413 margin-top: 0.9rem;
2414 gap: 8px;
2415 width: min(460px, 100%);
2416}
2417.compact .lobby-buttons .big-btn {
2418 min-width: 0;
2419 width: 100%;
2420 padding: 8px 16px;
2421}
2422.compact .lobby-extras {
2423 margin-top: 0.9rem;
2424}
2425/* Sideways there is barely 390px of height for a title, a saved game, three
2426 ways to start and the settings link. Everything gives a little so the last of
2427 them lands above the fold, rather than behind a scroll nobody looks for. */
2428@media (orientation: landscape) {
2429 .compact .lobby h1 {
2430 font-size: clamp(1.6rem, 9vh, 2.6rem);
2431 }
2432 .compact .lobby .sub,
2433 .compact .resume-when {
2434 display: none;
2435 }
2436 .compact .resume {
2437 margin-top: 0.4rem;
2438 padding: 6px 14px;
2439 }
2440 .compact .resume-line {
2441 font-size: 12px;
2442 }
2443 .compact .lobby-buttons {
2444 margin-top: 0.5rem;
2445 }
2446 .compact .lobby-buttons .big-btn {
2447 min-height: 40px;
2448 padding: 5px 16px;
2449 font-size: 19px;
2450 }
2451 .compact .lobby-extras {
2452 margin-top: 0.5rem;
2453 }
2454}
2455
2456.compact .help-slot .help {
2457 width: calc(100vw - 16px);
2458 --hd: calc(100dvh - 16px);
2459 translate: none;
2460 padding: 8px 12px 10px;
2461}
2462.compact .help-head h3 {
2463 font-size: 13px;
2464}
2465/* The strip version fills four columns and scrolls sideways, which reads fine
2466 across a laptop's width and not at all on a phone. One column, scrolled the
2467 way every other sheet on the device is. */
2468.compact .help-body {
2469 columns: auto;
2470 column-gap: normal;
2471 overflow-x: hidden;
2472}
2473
2474.compact .settings-scrim {
2475 padding: 6px;
2476}
2477.compact .settings {
2478 padding: 10px 14px 14px;
2479}
2480.compact .settings-head h3 {
2481 font-size: 16px;
2482}
2483.compact .settings-head .btn {
2484 min-height: 38px;
2485 padding: 2px 14px;
2486 font-size: 14px;
2487}
2488/* Two columns sideways, where there is width and no height; one held upright,
2489 where it is the other way round. */
2490.compact .settings-body {
2491 columns: 2;
2492 column-gap: 26px;
2493 padding-top: 10px;
2494}
2495@media (orientation: portrait) {
2496 .compact .settings-body {
2497 columns: 1;
2498 }
2499}
2500.compact .settings-body section {
2501 margin-bottom: 14px;
2502}
2503.compact .settings-body h4 {
2504 margin-bottom: 6px;
2505 font-size: 14px;
2506}
2507.compact .settings .f-label {
2508 min-width: 116px;
2509 font-size: 14px;
2510}
2511.compact .settings input[type='text'],
2512.compact .settings input:not([type]),
2513.compact .settings input[type='number'] {
2514 font-size: 15px;
2515 min-height: 40px;
2516 padding: 8px 10px;
2517}
2518.compact .settings .sw-name {
2519 font-size: 14px;
2520}
2521.compact .settings .note {
2522 font-size: 11px;
2523}
2524
2525/* ---------- fullscreen ---------- */
2526/* Sized to match the sound toggle it stands beside, in both its forms. */
2527.fs-toggle {
2528 min-height: 34px;
2529 padding: 2px 14px;
2530}
2531.fs-toggle.compact {
2532 min-height: 28px;
2533 padding: 3px 9px;
2534}
2535.fs-toggle.on {
2536 border-color: rgba(217, 180, 92, 0.7);
2537 color: var(--gold);
2538}
2539.fs-toggle .fs-icon {
2540 display: block;
2541 flex: none;
2542}
2543
2544/* ---------- the party table's QR ---------- */
2545/* One join code for the whole table, lying in the middle of the felt like a
2546 tile someone left face up. It may cover the pool while it shows — it steps
2547 aside the moment every hand is held, and comes back while a 📱 tag is held
2548 down. Below the result panel (z-index 2, later in the DOM), above the pool
2549 and the centre readout (1): scannable while it matters, never over the
2550 scores. */
2551/* The QR the seat's gear offers, wearing the face of a tile: the same ivory,
2552 the same rounded corners, the same resting shadow. The table's own join code
2553 is a real tile out on the felt (see ui/Pool) and this is its twin in a panel,
2554 so a code to scan looks the same wherever it turns up. */
2555.seat-qr {
2556 --qw: 92px;
2557 width: var(--qw);
2558 height: calc(var(--qw) * 1.2);
2559 display: grid;
2560 place-items: center;
2561 padding: calc(var(--qw) * 0.09);
2562 border-radius: calc(var(--qw) * 0.1);
2563 background: linear-gradient(165deg, #fffdf3, #f4edd9 58%, #e6dcbf);
2564 box-shadow:
2565 inset 0 2px 1px rgba(255, 255, 255, 0.85),
2566 inset 0 calc(var(--qw) * -0.05) calc(var(--qw) * 0.04) rgba(148, 130, 90, 0.4),
2567 inset calc(var(--qw) * -0.02) 0 calc(var(--qw) * 0.03) rgba(148, 130, 90, 0.22),
2568 0 6px 12px rgba(0, 0, 0, 0.5);
2569}
2570.seat-qr svg {
2571 display: block;
2572 width: 100%;
2573 height: auto;
2574 aspect-ratio: 1;
2575}
2576.nameplate .tag.phone {
2577 background: rgba(29, 90, 168, 0.35);
2578 border-color: rgba(29, 90, 168, 0.7);
2579 cursor: pointer;
2580 user-select: none;
2581 touch-action: none;
2582}
2583/* The device holding this hand has gone quiet and the hand is being kept for
2584 it. Amber rather than red: nothing is lost, and in a few seconds the
2585 computer picks the tiles up so the table can carry on. */
2586.nameplate .tag.waiting {
2587 background: rgba(217, 180, 92, 0.28);
2588 border-color: rgba(217, 180, 92, 0.6);
2589 padding-inline: 5px;
2590}
2591/* This hand is calling for itself, out of the phone holding it. Sits next to
2592 the 📱 tag and reads as part of it, which is what it is. */
2593.nameplate .tag.voice {
2594 background: rgba(29, 90, 168, 0.2);
2595 border-color: rgba(29, 90, 168, 0.55);
2596 padding-inline: 5px;
2597}
2598
2599/* ---------- the phone controller ---------- */
2600/* A hand held in landscape: status, buttons, tiles, in that order down the
2601 screen, with the common screen doing everything else. Tiles get the room —
2602 they are the only thing this screen is for. */
2603.ctl {
2604 /* What a *button* is here: the calls are drawn as tiles, and a tile a thumb
2605 has to hit is not the same size as a tile in a row of seventeen. The hand
2606 sizes itself off how many tiles have to fit — see `.ctl-hand .hand` — so
2607 the two no longer have to be the same number. */
2608 --tile-w: clamp(46px, 13vmin, 74px);
2609 --bar-h: calc(var(--tile-w) * 1.375);
2610 position: fixed;
2611 inset: 0;
2612 display: flex;
2613 flex-direction: column;
2614 background: radial-gradient(ellipse at center, var(--felt) 0%, var(--felt-2) 80%);
2615 color: var(--ivory);
2616 overflow: hidden;
2617 touch-action: none;
2618}
2619.ctl-top {
2620 display: flex;
2621 align-items: center;
2622 gap: 8px;
2623 padding: 8px 12px 4px;
2624 flex-wrap: wrap;
2625 font-size: 13px;
2626}
2627.ctl-top .wind {
2628 font-weight: 900;
2629 font-size: 18px;
2630 color: var(--gold);
2631}
2632.ctl-top .wind em {
2633 font-style: normal;
2634 font-size: 10px;
2635 margin-left: 2px;
2636 opacity: 0.7;
2637}
2638.ctl-top .pname {
2639 font-weight: 600;
2640}
2641.ctl-top .tag {
2642 border: 1px solid rgba(246, 240, 226, 0.4);
2643 border-radius: 20px;
2644 padding: 1px 8px;
2645 font-size: 11px;
2646}
2647.ctl-top .tag.ting {
2648 border-color: var(--gold);
2649 color: var(--gold);
2650}
2651.ctl-status {
2652 opacity: 0.75;
2653}
2654.ctl-status em {
2655 font-style: normal;
2656 opacity: 0.6;
2657 margin-left: 4px;
2658}
2659.ctl-status.hot {
2660 opacity: 1;
2661 color: var(--gold);
2662 font-weight: 600;
2663}
2664/* The table has come round to this phone. Four people at one screen and a
2665 phone each is a game that stalls on whoever is not looking at either, so the
2666 whole screen answers rather than a line of text at the top of it: a gold rim
2667 edge to edge, breathing, with the seven seconds running along the top. It
2668 buzzes once as well — see `calling` in ui/Controller.
2669
2670 Red for a claim, because a claim window is somebody else's tile going past
2671 and it is the one thing here with a clock on it that is not yours. */
2672.ctl-turn::before,
2673.ctl-claim::before {
2674 content: '';
2675 position: fixed;
2676 inset: 0;
2677 pointer-events: none;
2678 z-index: 2;
2679 animation: ctl-breathe 2.1s ease-in-out infinite;
2680}
2681.ctl-turn::before {
2682 box-shadow: inset 0 0 0 3px var(--gold), inset 0 0 36px rgba(217, 180, 92, 0.3);
2683}
2684.ctl-claim::before {
2685 box-shadow: inset 0 0 0 3px #e0574a, inset 0 0 36px rgba(179, 38, 30, 0.32);
2686}
2687@keyframes ctl-breathe {
2688 50% {
2689 opacity: 0.45;
2690 }
2691}
2692.ctl-turn .ctl-status,
2693.ctl-claim .ctl-status {
2694 font-size: 16px;
2695}
2696/* Across the top of the phone, edge to edge — there is no strip to sit inside
2697 here, the screen is the strip. */
2698.ctl .turn-clock {
2699 position: fixed;
2700 left: 0;
2701 right: 0;
2702 height: 6px;
2703 border-radius: 0;
2704 z-index: 4;
2705}
2706.ctl-last {
2707 display: inline-flex;
2708 align-items: center;
2709 gap: 4px;
2710 margin-left: auto;
2711}
2712.ctl-top .chips {
2713 font-weight: 600;
2714 color: var(--gold);
2715}
2716/* A row of tiles, so it is laid out as one: they stand on a common line and
2717 the gap between them is the gap between tiles. */
2718.ctl-actions {
2719 display: flex;
2720 align-items: flex-end;
2721 justify-content: center;
2722 gap: var(--hand-gap);
2723 flex-wrap: wrap;
2724 row-gap: 6px;
2725 min-height: var(--bar-h);
2726 padding: 4px 10px;
2727}
2728.ctl-hint {
2729 font-size: 11px;
2730 opacity: 0.55;
2731}
2732/* The tiles get the middle of the screen. This is the one thing the phone is
2733 for, and a row pinned to the bottom edge left the hand sitting under the
2734 thumb holding the phone with the whole screen empty above it. */
2735.ctl-hand {
2736 flex: 1;
2737 display: flex;
2738 align-items: center;
2739 justify-content: center;
2740 min-height: 0;
2741 padding: 0 8px calc(10px + env(safe-area-inset-bottom, 0px));
2742}
2743.ctl-hand .hand {
2744 /* The whole hand in one row: 17 tiles and their gaps must fit the width. */
2745 --tile-w: min(76px, calc((100vw - 56px) / 17.6));
2746 flex-wrap: nowrap;
2747}
2748/* Held upright, seventeen tiles will never make one row.
2749
2750 So the hand is laid out as a grid rather than left to wrap: wrapping fills
2751 the first line and drops whatever is left onto the second, which for a
2752 sixteen-tile hand on a narrow screen is nine and seven, or ten and six as
2753 the width changes under it. A grid of `--hand-cols` — half the hand, rounded
2754 up, counted in ui/Hand — is two lines of the same length whatever the screen
2755 is doing, and the tiles are then sized off that count rather than shrunk to
2756 fit a row that was never going to hold them.
2757
2758 The tile just drawn takes a line of its own above both, which is the same
2759 thing the daylight beside it says at a full-size table: this one is not part
2760 of the sorted hand yet. At the top rather than the bottom because that is
2761 where the eye starts, and because the 摸 tag over it needs air above it. */
2762@media (orientation: portrait) {
2763 .ctl-hand .hand {
2764 --tile-w: min(76px, calc((100vw - 40px) / (var(--hand-cols, 9) + 0.4)));
2765 display: grid;
2766 grid-template-columns: repeat(var(--hand-cols, 9), var(--tile-w));
2767 justify-content: center;
2768 gap: var(--hand-gap);
2769 row-gap: calc(var(--tile-w) * 0.5);
2770 width: 100%;
2771 height: auto;
2772 /* Room over the top line for the 摸 tag, which hangs above its tile. */
2773 padding-top: calc(var(--tile-w) * 0.42);
2774 }
2775 .ctl-hand .hand .drawn-slot {
2776 order: -1;
2777 grid-column: 1 / -1;
2778 justify-self: center;
2779 /* Its own line — the gap that sets it apart in a row is what the line is
2780 doing here, and left in it would push it off the middle. */
2781 margin-left: 0;
2782 }
2783}
2784/* The wire has gone, on a phone holding a live hand. A bar across the top,
2785 because what is underneath it has stopped working even though it still looks
2786 as though it has not: everything is dimmed a little and nothing takes a tap
2787 while this is up. */
2788.ctl-cut-bar {
2789 position: fixed;
2790 top: 0;
2791 left: 0;
2792 right: 0;
2793 z-index: 6;
2794 padding: 6px 12px calc(6px + env(safe-area-inset-top, 0px));
2795 text-align: center;
2796 font-size: 13px;
2797 background: rgba(120, 44, 34, 0.94);
2798 color: var(--ivory);
2799 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
2800}
2801.ctl-cut-bar em {
2802 font-style: normal;
2803 opacity: 0.7;
2804 margin-left: 6px;
2805}
2806.ctl-cut .ctl-actions,
2807.ctl-cut .ctl-hand {
2808 opacity: 0.45;
2809 pointer-events: none;
2810}
2811/* A phone in a room with no table in it. Says which of the two it is and then
2812 gets out of the way — there is nothing here to press. */
2813.ctl-adrift {
2814 align-items: center;
2815 justify-content: center;
2816 text-align: center;
2817 gap: 6px;
2818 padding: 24px;
2819}
2820.ctl-adrift h1 {
2821 font-size: 26px;
2822 margin: 0;
2823}
2824.ctl-adrift .sub {
2825 letter-spacing: 0.28em;
2826 color: var(--gold);
2827 margin: 0;
2828}
2829.ctl-adrift .adrift-line {
2830 font-size: 17px;
2831 font-weight: 600;
2832 margin: 10px 0 0;
2833}
2834.ctl-adrift .hint {
2835 max-width: 30ch;
2836 opacity: 0.7;
2837 font-size: 13px;
2838 line-height: 1.5;
2839 margin: 0;
2840}
2841
2842/* The tile in the fingers once it has left the row, and its exit: flung, it
2843 keeps going up off the screen — the common screen takes it from here. */
2844.ctl-held {
2845 position: fixed;
2846 z-index: 5;
2847 pointer-events: none;
2848 transform: translate(-50%, -50%);
2849}
2850.ctl-held .tile {
2851 width: 100%;
2852 height: 100%;
2853}
2854.ctl-held.flung {
2855 animation: fling-up 0.3s ease-in forwards;
2856}
2857@keyframes fling-up {
2858 to {
2859 transform: translate(-50%, -140vh) rotate(18deg);
2860 opacity: 0.9;
2861 }
2862}
2863.ctl-result {
2864 position: fixed;
2865 left: 50%;
2866 top: 44%;
2867 transform: translate(-50%, -50%);
2868 z-index: 6;
2869 background: rgba(10, 49, 38, 0.96);
2870 border: 1px solid rgba(217, 180, 92, 0.5);
2871 border-radius: 14px;
2872 padding: 16px 22px;
2873 text-align: center;
2874 max-width: min(92vw, 480px);
2875}
2876.ctl-result h2 {
2877 margin: 0 0 8px;
2878 font-size: 18px;
2879}
2880.ctl-result .deltas {
2881 display: flex;
2882 gap: 12px;
2883 flex-wrap: wrap;
2884 justify-content: center;
2885 margin-bottom: 12px;
2886 font-size: 13px;
2887}
2888.ctl-result .deltas .up {
2889 color: var(--gold);
2890}
2891.ctl-result .deltas .down {
2892 color: #e08a84;
2893}
2894
2895/* Seat picking, on the phone and in the online lobby. */
2896.ctl-pick {
2897 align-items: center;
2898 justify-content: center;
2899 text-align: center;
2900 overflow-y: auto;
2901 touch-action: pan-y;
2902}
2903.pick-grid {
2904 display: grid;
2905 grid-template-columns: repeat(2, minmax(150px, 220px));
2906 gap: 10px;
2907 justify-content: center;
2908 margin: 12px 0;
2909}
2910.pick-grid .btn em {
2911 display: block;
2912 font-style: normal;
2913 font-size: 11px;
2914 opacity: 0.65;
2915 margin-top: 2px;
2916}
2917/* Under the seats: the phone's own mute and the fullscreen button. Turning
2918 sound on here is also what wakes this phone's audio, which is what lets it
2919 do the calling for whichever seat it ends up on. */
2920.pick-extras {
2921 display: flex;
2922 gap: 12px;
2923 align-items: center;
2924 justify-content: center;
2925 flex-wrap: wrap;
2926}
2927
2928/* The gear's panel. Same shell as the rules sheet — anchored in the player's
2929 own strip and turned to face it — but it is four buttons, not a book, so it
2930 is only as tall as they are. */
2931/* `--hd` is how deep it stands, and `--off` — how far out of the middle it is
2932 pushed towards its own edge — is worked out from it, so the menu sets the
2933 depth rather than letting its content decide it. Four buttons' worth. */
2934.help-slot.slot-bottom .help.menu,
2935.help-slot.slot-top .help.menu,
2936.help-slot.slot-left .help.menu,
2937.help-slot.slot-right .help.menu {
2938 width: min(420px, calc(100vw - 60px));
2939 --hd: 268px;
2940}
2941.help.menu {
2942 padding: 10px 18px 16px;
2943}
2944.menu-body {
2945 display: flex;
2946 flex-direction: column;
2947 gap: 8px;
2948 margin-top: 10px;
2949 /* The panel is a fixed height at its player's edge, and the QR at the foot
2950 of this list is what first made the list taller than it. Scroll rather
2951 than spill: the panel is turned to face one player, and a menu that ran
2952 off the table would run off it upside down for two of them. */
2953 min-height: 0;
2954 overflow-y: auto;
2955}
2956.menu-body .btn {
2957 justify-content: flex-start;
2958 width: 100%;
2959}
2960.menu-body .btn:disabled {
2961 opacity: 0.4;
2962 cursor: default;
2963}
2964/* The way off this screen: the QR for taking *this* hand onto a phone, turned
2965 to face the player whose hand it is along with everything else in here.
2966 Smaller than the one that lies in the middle of the felt — a phone held over
2967 an open menu is a foot away, not across the table. */
2968.menu-qr {
2969 display: flex;
2970 align-items: center;
2971 gap: 10px;
2972 padding-top: 4px;
2973 border-top: 1px solid rgba(246, 240, 226, 0.14);
2974}
2975.menu-qr .seat-qr {
2976 flex: none;
2977}
2978.menu-qr-hint {
2979 font-size: 12px;
2980 line-height: 1.35;
2981 opacity: 0.75;
2982}
2983.menu-qr-hint em {
2984 display: block;
2985 font-style: normal;
2986 font-size: 11px;
2987 opacity: 0.75;
2988}
2989
2990/* How many other devices are in the room, in the top-left corner of the felt.
2991 Small and dim: it is a thing to glance at when you are wondering whether a
2992 scan worked, not a thing to read while playing. Out of the way of all four
2993 strips, which sit against their own edges. */
2994.net-count {
2995 position: absolute;
2996 top: 6px;
2997 left: 8px;
2998 z-index: 55;
2999 font-size: 11px;
3000 letter-spacing: 0.02em;
3001 color: rgba(246, 240, 226, 0.42);
3002 pointer-events: none;
3003 user-select: none;
3004}
3005
3006/* ---------- the wire, when it goes ---------- */
3007/* One line across the top of the felt. The room is already reconnecting on its
3008 own, so this says what is happening rather than asking for anything, and it
3009 goes away by itself when state starts flowing again. Above the tiles and out
3010 of the way of every strip, which all sit against their own edge. */
3011.net-warn {
3012 position: absolute;
3013 top: 6px;
3014 left: 50%;
3015 transform: translateX(-50%);
3016 z-index: 60;
3017 max-width: 70vw;
3018 padding: 4px 12px;
3019 border-radius: 999px;
3020 background: rgba(20, 40, 30, 0.82);
3021 border: 1px solid rgba(217, 180, 92, 0.5);
3022 color: rgba(246, 240, 226, 0.85);
3023 font-size: 12px;
3024 text-align: center;
3025 pointer-events: none;
3026}
3027
3028/* ---------- movement, for anyone who would rather not ---------- */
3029/* The table says things twice — the gold is also a name in gold, the clock is
3030 also a bar — so nothing here is the only way anything is said, and the
3031 breathing can simply stop. What is left is the bright end of each animation
3032 held still, which is the state they were all built around. */
3033@media (prefers-reduced-motion: reduce) {
3034 .seat.awaited,
3035 .seat.claiming,
3036 .ctl-turn::before,
3037 .ctl-claim::before {
3038 animation: none;
3039 }
3040}