anvilsign in

collin/mahjong

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