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