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