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