anvilsign in

collin/mahjong

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