/* Cinzel (SIL Open Font License 1.1) — the app title face: engraved Roman
 * capitals, the look of a club or parlor sign. Embedded as a base64 woff2 SUBSET
 * holding only the letters of 'Mahjong Parlor', which is why it costs ~2 KB.
 * Embedded rather than linked because the app ships as ONE self-contained file
 * with no network access — and it makes the title identical on iPhone, Android,
 * Windows and Mac instead of falling back to a different face per platform. */
@font-face { font-family: 'Parlor'; font-style: normal; font-weight: 600; font-display: block;
  src: url(data:font/woff2;base64,d09GMgABAAAAAAeoABAAAAAADRQAAAdOAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbgkAcKAZgP1NUQVQcAHQRCAqNMIsKCxoAATYCJAMwBCAFg2oHIAwHG8AKyMR/nt/W/zo3gLsvIE4A6gRjBkYWCGaA+UCdKPi/4VU686rbeNHp/eN+pZvWgDm+oKv2I99W110TR+F9ugqK1kiRBTxwv1W6rjsID05p+kIzGAAtPIazj+IhgkENAtt8M0I0rOCqapps0M8ML85BD2FP8o1RNz3umYOmx0qVAA4a32cggIsOyz+GayDt7lT+Un5WfkY2o/Kj8g00yvfKb6urvx/5P1hE3XDG+92l/MKC3yrfaYKAadNc9fmhPcofyt/K38o17qqvGHUe4PAB6Yy2jAoCAnjsAQaxxzf6NFC7IJ6g5CEwKsBJyHHYAaN7GAcVJDDwyvc1QUaJEItY6sRbUIukDxvj3Czn5DzEBTgngMZ2FzYAK0vGTV8E5W/LYGmcn9PS2lKNh2CBh39p80Sjj3+JLVqAM1iM/cTdx0wWhKDRxLTNbOb2Tk4tMm7E8KCbTfzugIChFgw4r7agGW0YxhTcmME8PBqTG4STgrlbKF8qtym3KJco5yvHlWPKinIurwWyRgamKAhh+HVizDCaC3B1APfsofg1Re8kEO/HsgweqmtwU0GQSBYFkeSdAhEJehL0qYIwYlmdTqMRBM3GfiKNRLTNUOB+Z/dB3f4RnaTda5/yX3eTmbpGQCwYZMEHI6p9PviulmUQG5TtfTTosrnn+6nH0S+fd7/sD3psV/hqcvBa5vcPysG1tT0Xr66urhcw/+V3Pvvg7nOOGdfXifwrK7JAYv9Z+fIMg+R/gAIBMspcrNMucDrPlfRCf5AF742YtvdR7znHLrzQ1A0tG5bPu1v2Bxm07m9OB9fWEux0yfo6BVZX4Vtbk4PPvpjk8F9DxGRQYKftqtVVCqyvM//Gxl6BRK1A4ji0zMYbVTcoyz5u55HIZPgDLHAXiPzXGKUAY7JLh6ZrI2T5vKsPEQ/yr+KtCPwfPSj4Vl9azWF3PrrBRBm0cg+RUQcK3GUmNiiQ6NqpZaM2SgtuPLp7aM8ZV/eKk6zcUx5csi87RZoZ1j8hNmDniPevs1nOd/F5jLp4+5GkCizISK7Bt4oYs3fa6A9ubDA/Pejlufz5h+FbfSnr8pfDzrr+/oSrr76dqP3WX0g8+4YbbjCu3BMLictNV9553UYvuOGeO2KvuI3OvnamPaTZI3pFYuoa+zX8dyNvpwwvxKpTIVb1ZyS/l4zNp9UDYQZsCVXRrpZ0uX6eWAgiTi1+kXGcwW0IkYrzw06lnV1lnrKRp/OzUy6PMs8+0vv0vzP/7Hy+2mds9kUghNH8dJ46v1gKCe0xcBy+vPdg60R6pl3DolvLstvMDvof4eI7hAuuKErTo7yypcSOigrdfQfu4zz1bcRkFl4lcQl8uC2cEZPavB5eqgqHllEbquYsU8FHXjr19GvvD9XQ4wQNtt/zDkvkJt4Sar74jX/250ck1wXRfPTO4P/euWcbNCANaULvx+JYqlNu3v6Q+hzN3yV1jt25deMV2YecNuvBgfLKqB9zdTi0ddSolz6XdClq+BptuYvRtWNnNLYHR4bbg2c1wOdwLjkTncvOqyKii6On/iqtIcjMtGn08ka9CTKoJqy0hiDDpDN5edPfJiaDai40l9YQZJi/ET28uGmCFlQLc2kNQQvT3yYvr/raDBlUgw8dlSnRDT+1hd1MzX+YGuMqhDJelRouS2rSmktUXBk+cDiXnC7rT+qY7H8lX33FT6lllns3jrvnWgg6mDbFE14vrSXIMOtMXk6lMzEZVIuh8VJRmilVqe4rbiAYYPrL6Ba12xEKaiiejHctf+v+PlMqR4fu68jqVkjug59PFBU3EkJh1Brd4l9GhIIaivHPsaajSf3tVk9dndXT3340qanpaFJfu9VTV2f19LUfTWpsLh+tzDtks+cdHK0sLx+tzD1ot+UeGq3Emw5PclPoFbrkmN013/3836j16JFdxvzNFCNV5NRvTWhxj+/LSvj6qEVi6e8Nfek4Ly5Oz/rCDhoeuklEhpWcy07OPNdEdu83InudU31t5Oxjm1xmTrVzO4hJze9ZYM7hePC7ipDBkKKfNTz/2br4swMXmNsD38y1CsonmyeKlfxLgH4jiRPwbw5nIVv5ZFMtVr7rn6JlP8HK36Io3DKsXED5insHcdzXSGANcA9uDxJ4GeG8BvHc/5CKP7TgpsUcDFiGgRfwMv0SPF8pmyCZoQvwxqhj2IX7dBz0uEHHIwsHVgu7dSLCMaRTYRcqtHrkQGtjHDsacGAcLkxiCotl7yIW4EUB0pyLvBiFBy4sYBFepELehRmkfiFfGZNIQytq0IRKuDCHAxjHDFLQiXHMwoUKzGMGY+jCODwnHTWPOVhgRSrSzalevxAtaEULqpfAid+x9ySWMINheA4zbGmsxDwWsF97HBsqGdJLbhbYMIVxWE6vbYMH83AzGD00UI4lSlC0a3thQTw9gUm4dl53CSNIxSjmMYs0tKAK9ehEJezehWp0IE0KJPyPIP56ynFYkdXfxZR/3yScBA==) format('woff2'); }
/* American Mahjong — table aesthetic: green felt, ivory tiles, brass accents. */

:root {
  --felt: var(--t-123f2b);
  --felt-2: var(--t-0d3122);
  --rail: #4a2f1c;
  --ivory: #f4ecdc;
  --ivory-edge: #d9cdb2;
  --ink: #22303a;
  --brass: #d9a441;
  --bam: #1f8a4c;
  --crak: #c0392b;
  --dot: #2b6fb0;
  --soap: #3a4750;
  --wind: #5a5f6a;
  --flower: #b06fa8;
  --joker: #7a4fd0;
  --sel: #ffd76a;
  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* TABLE COLOUR (owner, 9/21: "let people ... change the primary background
   color (from green) to different colors" - "and add pink"). Every green that
   IS the table reads one of these; greens that MEAN something (the Bam suit,
   a good call, X badges, levels, "new") stay green on purpose. Themes are the
   same greens hue-shifted with their darkness kept, so contrast with the ivory
   tiles and brass holds. js/tablecolor.js sets <html data-table="...">. */
:root {
  --t-082018: #082018;
  --t-0a231866: #0a231866;
  --t-0a2818: #0a2818;
  --t-0a2c1d: #0a2c1d;
  --t-0b241a: #0b241a;
  --t-0b241ae8: #0b241ae8;
  --t-0b2e1d: #0b2e1d;
  --t-0d3122: #0d3122;
  --t-0d312200: #0d312200;
  --t-0e2418f2: #0e2418f2;
  --t-0e2b1fd9: #0e2b1fd9;
  --t-0e2b1fef: #0e2b1fef;
  --t-0e3a26: #0e3a26;
  --t-0e3c28: #0e3c28;
  --t-0f2f21: #0f2f21;
  --t-0f2f21f6: #0f2f21f6;
  --t-0f3a25: #0f3a25;
  --t-102217: #102217;
  --t-10261b: #10261b;
  --t-10261bee: #10261bee;
  --t-10261bf2: #10261bf2;
  --t-10261bf5: #10261bf5;
  --t-10261bfa: #10261bfa;
  --t-10321fd9: #10321fd9;
  --t-10321ff0: #10321ff0;
  --t-10321ff2: #10321ff2;
  --t-12241b: #12241b;
  --t-123f2b: #123f2b;
  --t-14251b: #14251b;
  --t-14382a: #14382a;
  --t-16493200: #16493200;
  --t-16583a: #16583a;
  --t-17301f: #17301f;
  --t-17402f: #17402f;
  --t-17402ff6: #17402ff6;
  --t-175c3c: #175c3c;
  --t-1a5c3e55: #1a5c3e55;
  --t-1b3b2c: #1b3b2c;
  --t-1c2f23: #1c2f23;
  --t-1c6344: #1c6344;
  --t-1e6b46: #1e6b46;
  --t-23784f: #23784f;
  --t-2a7d55: #2a7d55;
  --t-4a8a7a: #4a8a7a;
  --t-6a7a70: #6a7a70;
}
:root[data-table="navy"] {
  --t-082018: #091123;
  --t-0a231866: #0b142666;
  --t-0a2818: #0b162b;
  --t-0a2c1d: #0b1730;
  --t-0b241a: #0c1527;
  --t-0b241ae8: #0c1527e8;
  --t-0b2e1d: #0c1832;
  --t-0d3122: #0e1b35;
  --t-0d312200: #0e1b3500;
  --t-0e2418f2: #0f1727f2;
  --t-0e2b1fd9: #0f1a2ed9;
  --t-0e2b1fef: #0f1a2eef;
  --t-0e3a26: #0f1f3f;
  --t-0e3c28: #0f2041;
  --t-0f2f21: #101c33;
  --t-0f2f21f6: #101c33f6;
  --t-0f3a25: #10203f;
  --t-102217: #111825;
  --t-10261b: #111929;
  --t-10261bee: #111929ee;
  --t-10261bf2: #111929f2;
  --t-10261bf5: #111929f5;
  --t-10261bfa: #111929fa;
  --t-10321fd9: #111e36d9;
  --t-10321ff0: #111e36f0;
  --t-10321ff2: #111e36f2;
  --t-12241b: #131a27;
  --t-123f2b: #132444;
  --t-14251b: #161c28;
  --t-14382a: #16233c;
  --t-16493200: #182a4f00;
  --t-16583a: #18305f;
  --t-17301f: #192234;
  --t-17402f: #192845;
  --t-17402ff6: #192845f6;
  --t-175c3c: #193263;
  --t-1a5c3e55: #1c346355;
  --t-1b3b2c: #1d2940;
  --t-1c2f23: #1e2533;
  --t-1c6344: #1e386b;
  --t-1e6b46: #203c74;
  --t-23784f: #264482;
  --t-2a7d55: #2d4b87;
  --t-4a8a7a: #506795;
  --t-6a7a70: #727884;
}
:root[data-table="burgundy"] {
  --t-082018: #21090e;
  --t-0a231866: #240b1066;
  --t-0a2818: #290b11;
  --t-0a2c1d: #2d0b12;
  --t-0b241a: #250c11;
  --t-0b241ae8: #250c11e8;
  --t-0b2e1d: #2f0c13;
  --t-0d3122: #330f16;
  --t-0d312200: #330f1600;
  --t-0e2418f2: #250f14f2;
  --t-0e2b1fd9: #2c0f15d9;
  --t-0e2b1fef: #2c0f15ef;
  --t-0e3a26: #3c1019;
  --t-0e3c28: #3e1019;
  --t-0f2f21: #311117;
  --t-0f2f21f6: #311117f6;
  --t-0f3a25: #3c1119;
  --t-102217: #231115;
  --t-10261b: #271116;
  --t-10261bee: #271116ee;
  --t-10261bf2: #271116f2;
  --t-10261bf5: #271116f5;
  --t-10261bfa: #271116fa;
  --t-10321fd9: #341218d9;
  --t-10321ff0: #341218f0;
  --t-10321ff2: #341218f2;
  --t-12241b: #251317;
  --t-123f2b: #41141d;
  --t-14251b: #261519;
  --t-14382a: #3a161d;
  --t-16493200: #4b182300;
  --t-16583a: #5b1926;
  --t-17301f: #32191e;
  --t-17402f: #421921;
  --t-17402ff6: #421921f6;
  --t-175c3c: #5f1a28;
  --t-1a5c3e55: #5f1d2a55;
  --t-1b3b2c: #3d1d24;
  --t-1c2f23: #311e22;
  --t-1c6344: #661f2d;
  --t-1e6b46: #6e2231;
  --t-23784f: #7c2738;
  --t-2a7d55: #812e3f;
  --t-4a8a7a: #8f4f5c;
  --t-6a7a70: #807073;
}
:root[data-table="charcoal"] {
  --t-082018: #131415;
  --t-0a231866: #15161866;
  --t-0a2818: #17191a;
  --t-0a2c1d: #191b1d;
  --t-0b241a: #161819;
  --t-0b241ae8: #161819e8;
  --t-0b2e1d: #1b1c1e;
  --t-0d3122: #1d1f21;
  --t-0d312200: #1d1f2100;
  --t-0e2418f2: #18191af2;
  --t-0e2b1fd9: #1b1c1ed9;
  --t-0e2b1fef: #1b1c1eef;
  --t-0e3a26: #222426;
  --t-0e3c28: #232527;
  --t-0f2f21: #1d1f21;
  --t-0f2f21f6: #1d1f21f6;
  --t-0f3a25: #222427;
  --t-102217: #18191a;
  --t-10261b: #1a1b1c;
  --t-10261bee: #1a1b1cee;
  --t-10261bf2: #1a1b1cf2;
  --t-10261bf5: #1a1b1cf5;
  --t-10261bfa: #1a1b1cfa;
  --t-10321fd9: #1f2123d9;
  --t-10321ff0: #1f2123f0;
  --t-10321ff2: #1f2123f2;
  --t-12241b: #1a1b1c;
  --t-123f2b: #26282b;
  --t-14251b: #1c1c1d;
  --t-14382a: #242628;
  --t-16493200: #2d303200;
  --t-16583a: #34373a;
  --t-17301f: #222425;
  --t-17402f: #292c2e;
  --t-17402ff6: #292c2ef6;
  --t-175c3c: #36393d;
  --t-1a5c3e55: #383b3e55;
  --t-1b3b2c: #292b2d;
  --t-1c2f23: #252626;
  --t-1c6344: #3c4043;
  --t-1e6b46: #414448;
  --t-23784f: #494e52;
  --t-2a7d55: #4f5458;
  --t-4a8a7a: #676a6d;
  --t-6a7a70: #717273;
}
:root[data-table="plum"] {
  --t-082018: #1c0b21;
  --t-0a231866: #1f0d2466;
  --t-0a2818: #230d2a;
  --t-0a2c1d: #260e2e;
  --t-0b241a: #200e26;
  --t-0b241ae8: #200e26e8;
  --t-0b2e1d: #280f30;
  --t-0d3122: #2b1133;
  --t-0d312200: #2b113300;
  --t-0e2418f2: #211126f2;
  --t-0e2b1fd9: #26122dd9;
  --t-0e2b1fef: #26122def;
  --t-0e3a26: #32133c;
  --t-0e3c28: #33133e;
  --t-0f2f21: #2a1331;
  --t-0f2f21f6: #2a1331f6;
  --t-0f3a25: #32143c;
  --t-102217: #201324;
  --t-10261b: #231328;
  --t-10261bee: #231328ee;
  --t-10261bf2: #231328f2;
  --t-10261bf5: #231328f5;
  --t-10261bfa: #231328fa;
  --t-10321fd9: #2c1434d9;
  --t-10321ff0: #2c1434f0;
  --t-10321ff2: #2c1434f2;
  --t-12241b: #221526;
  --t-123f2b: #371842;
  --t-14251b: #231727;
  --t-14382a: #32193b;
  --t-16493200: #401c4c00;
  --t-16583a: #4c1e5b;
  --t-17301f: #2d1b33;
  --t-17402f: #391d43;
  --t-17402ff6: #391d43f6;
  --t-175c3c: #4f1f60;
  --t-1a5c3e55: #50226055;
  --t-1b3b2c: #37203e;
  --t-1c2f23: #2e2032;
  --t-1c6344: #562567;
  --t-1e6b46: #5d276f;
  --t-23784f: #692e7d;
  --t-2a7d55: #6f3583;
  --t-4a8a7a: #845793;
  --t-6a7a70: #817685;
}
:root[data-table="teal"] {
  --t-082018: #081e20;
  --t-0a231866: #0a202366;
  --t-0a2818: #0a2528;
  --t-0a2c1d: #0a292c;
  --t-0b241a: #0b2124;
  --t-0b241ae8: #0b2124e8;
  --t-0b2e1d: #0b2a2e;
  --t-0d3122: #0d2d31;
  --t-0d312200: #0d2d3100;
  --t-0e2418f2: #0e2224f2;
  --t-0e2b1fd9: #0e282bd9;
  --t-0e2b1fef: #0e282bef;
  --t-0e3a26: #0e363a;
  --t-0e3c28: #0e373c;
  --t-0f2f21: #0f2c2f;
  --t-0f2f21f6: #0f2c2ff6;
  --t-0f3a25: #0f363a;
  --t-102217: #102022;
  --t-10261b: #102426;
  --t-10261bee: #102426ee;
  --t-10261bf2: #102426f2;
  --t-10261bf5: #102426f5;
  --t-10261bfa: #102426fa;
  --t-10321fd9: #102f32d9;
  --t-10321ff0: #102f32f0;
  --t-10321ff2: #102f32f2;
  --t-12241b: #122224;
  --t-123f2b: #123a3f;
  --t-14251b: #142325;
  --t-14382a: #143438;
  --t-16493200: #16444900;
  --t-16583a: #165158;
  --t-17301f: #172d30;
  --t-17402f: #173c40;
  --t-17402ff6: #173c40f6;
  --t-175c3c: #17555c;
  --t-1a5c3e55: #1a555c55;
  --t-1b3b2c: #1b383b;
  --t-1c2f23: #1c2d2f;
  --t-1c6344: #1c5c63;
  --t-1e6b46: #1e636b;
  --t-23784f: #236f78;
  --t-2a7d55: #2a757d;
  --t-4a8a7a: #4a848a;
  --t-6a7a70: #6a787a;
}
:root[data-table="pink"] {
  --t-082018: #32081a;
  --t-0a231866: #360b1e66;
  --t-0a2818: #3e0a21;
  --t-0a2c1d: #450a23;
  --t-0b241a: #380c1f;
  --t-0b241ae8: #380c1fe8;
  --t-0b2e1d: #480b25;
  --t-0d3122: #4c0e29;
  --t-0d312200: #4c0e2900;
  --t-0e2418f2: #371122f2;
  --t-0e2b1fd9: #431026d9;
  --t-0e2b1fef: #431026ef;
  --t-0e3a26: #5a0e2f;
  --t-0e3c28: #5e0e30;
  --t-0f2f21: #491129;
  --t-0f2f21f6: #491129f6;
  --t-0f3a25: #5a1030;
  --t-102217: #341522;
  --t-10261b: #3a1425;
  --t-10261bee: #3a1425ee;
  --t-10261bf2: #3a1425f2;
  --t-10261bf5: #3a1425f5;
  --t-10261bfa: #3a1425fa;
  --t-10321fd9: #4d122cd9;
  --t-10321ff0: #4d122cf0;
  --t-10321ff2: #4d122cf2;
  --t-12241b: #371725;
  --t-123f2b: #621436;
  --t-14251b: #381b27;
  --t-14382a: #561833;
  --t-16493200: #71193f00;
  --t-16583a: #891648;
  --t-17301f: #491e31;
  --t-17402f: #631b3a;
  --t-17402ff6: #631b3af6;
  --t-175c3c: #8f174b;
  --t-1a5c3e55: #8f1c4e55;
  --t-1b3b2c: #5a233b;
  --t-1c2f23: #472634;
  --t-1c6344: #9a1e54;
  --t-1e6b46: #a6205a;
  --t-23784f: #ba2667;
  --t-2a7d55: #c1316f;
  --t-4a8a7a: #be7595;
  --t-6a7a70: #a6969d;
}

* { box-sizing: border-box; }
/* min-height, NOT height. `height:100%` plus `overflow-x:hidden` makes the box a
   scroll container (a hidden axis forces the other to `auto`), so html and body
   both became fixed-height scrollers nested inside each other. Content taller than
   the screen then scrolled unreliably — on the phone you could not always reach the
   end-of-hand prompt at the top. Letting the document scroll normally fixes it. */
html, body { margin: 0; min-height: 100%; max-width: 100%; }
/* NO DOUBLE-TAP ZOOM ANYWHERE (owner, 9/11: "in solitaire when I do a double
   tap it auto zooms"). Two quick taps on a tile are two taps, not a zoom; the
   board alone had this and a second tap that landed on the felt beside it
   zoomed the page. Pinch zoom and scrolling are untouched - `manipulation`
   only drops the double-tap gesture. Both pages load this sheet. */
html, body { touch-action: manipulation; }
/* NO TEXT AUTOSIZING (owner, 9/11: "on live solitaire match every time score
   goes up the screen shifts", sideways). iOS Safari inflates text in
   landscape on its own judgement and re-judges a block whenever its text
   changes - a falling tile count or a ticking clock re-sized the header and
   everything under it hopped. Chrome never does this, which is why the
   harness saw nothing. 100% = the sizes the stylesheet asks for, always. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
/* The header's Solo / 4 players segment is gone EVERYWHERE, not just on
   phones (Axe, 9/11, PC: "the top buttons still don't work") - the title
   screen chooses the game, and `.seg { display:flex }` had been outranking
   the HTML's hidden attribute on wide screens, leaving two dead buttons. */
.topbar .seg, .seg[hidden] { display: none !important; }
/* Only HTML clips horizontally. Putting `overflow-x: hidden` on BODY as well makes
   body a scroll container, and a scroll container between a sticky child and the
   viewport stops that child ever sticking — which is what kept the header from
   staying put. html's clip already prevents any sideways bleed.
   (`clip` would be the tidier value but needs Safari 16; this ships to iOS 15.1.) */
html { overflow-x: hidden; }
body {
  font-family: var(--font);
  color: var(--ivory);
  background: radial-gradient(ellipse at center, var(--felt) 0%, var(--felt-2) 100%);
  min-height: 100vh;
}

.topbar {
  display: flex; align-items: center; gap: 8px;
  /* NEVER WRAPS (owner, 9/12, two screen recordings): "Katherine's turn" beside
     the wordmark and the Menu button did not fit a phone's width, so the bar
     wrapped to two rows and grew ~40px, shoving the whole board down - and
     back up when a shorter name's turn came. The status yields (ellipsis)
     and the bar's height is the Menu button's, hand after hand. */
  flex-wrap: nowrap;
  padding: 8px 12px; border-bottom: 2px solid #0006;
  padding-top: max(8px, env(safe-area-inset-top)); /* clear the phone notch */
  /* Sticky, and opaque so the board scrolls UNDER it rather than through it. On the
     phone the header used to scroll away and the top player's panel ended up jammed
     beneath the iOS clock; this also keeps Menu reachable at all times. */
  position: sticky; top: 0; z-index: 40;
  background: var(--felt-2);
  box-shadow: 0 6px 14px #00000038;
}
/* Engraved caps want to be tracked out — the wide letter-spacing is what makes it
   read as signage rather than ordinary type. Set in caps because Cinzel is designed
   as Roman capitals. */
.topbar h1 { font-family: 'Parlor', Copperplate, 'Times New Roman', serif; font-weight: 600;
  font-size: 18px; line-height: 1; margin: 0; letter-spacing: .13em; text-transform: uppercase;
  color: var(--brass); white-space: nowrap; }
.topbar button { white-space: nowrap; }
/* Title on the left, one Menu button pinned right; a compact status fills between
   (useful for the Tower level/lives). Deal again / New game live in the menu now,
   so the top bar is a single slim row and the game gets more space. */
.topbar .status { flex: 1 1 0; min-width: 0; text-align: right; margin-right: 8px;
  font-size: 12px; opacity: .8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.menu-wrap { margin-left: auto; }

/* Menu dropdown — the secondary controls live here so the top bar is one row. */
.menu-wrap { position: relative; }
.menu-drop {
  /* Anchor to the RIGHT edge of the (right-aligned) Menu button so the dropdown
     opens leftward and never runs off the right side of the screen. */
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 60; min-width: 180px;
  max-width: min(240px, 86vw); display: flex; flex-direction: column; gap: 2px; padding: 6px;
  background: var(--t-14251b); border: 1px solid #ffffff26; border-radius: 10px;
  box-shadow: 0 12px 30px #000a;
  /* A short laptop screen could not reach the bottom rows (Alex, 9/10). */
  max-height: calc(100vh - 70px); overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.menu-drop[hidden] { display: none; }
.menu-drop button {
  width: 100%; text-align: left; background: transparent; color: var(--ivory);
  border: 1px solid transparent; border-radius: 7px; padding: 8px 10px; font-size: 13px;
}
.menu-drop button:hover { background: #ffffff14; border-color: #ffffff22; }
.menu-sep { height: 1px; background: #ffffff1f; margin: 4px 2px; }
button, select {
  font-family: var(--font); font-size: 13px; color: var(--ink);
  background: var(--ivory); border: 1px solid var(--ivory-edge);
  border-radius: 7px; padding: 6px 12px; cursor: pointer;
}
/* Hover belongs to mice, not fingers. iOS leaves :hover STUCK on the last thing you
   tapped, so this rule repainted a hand chip you had touched minutes ago pure white
   and left it that way — reading as "this is the hand you're on" when it wasn't.
   The rack tiles were gated for exactly this reason (see the note further down);
   buttons never were. */
@media (hover: hover) and (pointer: fine) {
  button:hover { background: #fff; }
}
button:disabled { opacity: .45; cursor: not-allowed; }
/* Siamese: dragging a tile between your two racks. The source dims, the ghost
   follows the finger, and the receiving rack is outlined so the target is obvious. */
.si-rack .tile.si-dragging { opacity: .3; }
.tile.si-ghost { box-shadow: 0 12px 24px #0008; transform: scale(1.12); }
.si-myhand.si-droptarget { outline: 2px dashed var(--brass); outline-offset: 3px; border-radius: 12px;
  background: #d9a4410f; }

/* The card, inline under the Siamese hands (same reference as the 4-player table). */
.si-cardview { margin-top: 12px; padding-bottom: 72px; }  /* room under the floating chat buttons */
.si-cardhead { font-size: 12px; color: var(--brass); opacity: .9; margin-bottom: 6px; }

/* The player's zodiac emblem, beside their name at the table. */
.emblem { font-size: 1.15em; line-height: 1; }
/* Mascot picker: the 12 zodiac animals, each with its name and the years it governs.
   Three columns rather than four so the year list has room to breathe. */
.emblem-modal { max-width: 520px; }
.emblem-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 12px 0; }
.emblem-pick {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 9px 6px; border-radius: 10px; cursor: pointer;
  background: #ffffff0e; border: 1px solid #ffffff20; color: var(--ivory);
}
.emblem-pick:hover { border-color: var(--brass); background: #d9a44120; }
.emblem-pick.on { border-color: var(--brass); background: #d9a44130; }
.emblem-pick .ea { font-size: 26px; line-height: 1; }
.emblem-pick .en { font-size: 11.5px; font-weight: 600; }
/* The year list: small, tabular, and allowed to wrap onto a few lines. */
.emblem-pick .ey { font-size: 9px; line-height: 1.35; opacity: .62; text-align: center;
  font-variant-numeric: tabular-nums; }
@media (max-width: 460px) { .emblem-grid { grid-template-columns: repeat(2, 1fr); } }

/* Call-menu guidance. Every option offered is legal; the ✓ one is the one that
   actually advances your closest hand. The others stay fully clickable (your
   call, your hand) but read as secondary so "take every tile" stops looking
   like the obvious play. */
button.callgood { background: #2f7d4f; border-color: #266b43; color: #f2fff6; font-weight: 700; }
button.calliffy { background: #ffffff10; border-color: #ffffff26; color: #ffffffb0; font-weight: 500; }
button.primary { background: var(--brass); border-color: #b9862c; font-weight: 600; }
/* A disabled primary (e.g. "Declare Mahjong" before the hand is complete) must NOT
   look gold/tappable — grey it out so it clearly reads as unavailable. */
button.primary:disabled { background: #ffffff12; color: #ffffff66; border-color: #ffffff22; box-shadow: none; opacity: 1; }
button.swap { background: #7a4fd0; border-color: #5f34c0; color: #fff; font-weight: 600; }
button.swap:hover { background: #8a5fe0; }
.seg { display: inline-flex; border-radius: 7px; overflow: hidden; }
.seg button { border-radius: 0; }
.seg button.on { background: var(--brass); font-weight: 600; }

.table { display: grid; grid-template-columns: 1fr 300px; gap: 12px; padding: 14px; }
.board { display: flex; flex-direction: column; gap: 14px; }

/* --- Seated table view --- */
.playtable {
  display: grid;
  /* SIDE COLUMNS OF A FIXED WIDTH (owner, 9/11: "the table shifts - gets larger
     and smaller on the right side"). `auto` tracks resized with the seat's
     content - a meld appearing, a DEALER chip, "dead?" - and the felt slid
     every turn. The tracks hold still; the seat's rows wrap inside them. */
  grid-template-columns: clamp(104px, 18%, 190px) minmax(0, 1fr) clamp(104px, 18%, 190px);
  grid-template-rows: auto minmax(180px, 1fr) auto;
  grid-template-areas: ".  top  ." "left center right" "bottom bottom bottom";
  gap: 10px; padding: 12px;
  background: radial-gradient(ellipse at center, var(--t-1a5c3e55), var(--t-0d312200) 70%);
  border: 1px solid #0004; border-radius: 18px;
}
.playtable.players-1 { grid-template-rows: auto minmax(150px, 1fr) auto; }
.oseat {
  background: #00000030; border: 1px solid #0005; border-radius: 10px; padding: 7px 9px;
  display: flex; flex-direction: column; gap: 5px; align-self: center; justify-self: center; max-width: 100%;
  min-width: 0;
}
.oseat.pos-left, .oseat.pos-right { width: 100%; }   /* fill the fixed track, so nothing shifts */
.oseat.pos-left > *, .oseat.pos-right > * { min-width: 0; flex-wrap: wrap; }
.oseat.active { border-color: var(--brass); box-shadow: 0 0 0 2px #d9a44155; }
/* A dead hand sits out - the whole seat dims and its melds grey with it. */
.oseat.dead-hand { opacity: .55; filter: saturate(.4); }
.odead { color: #e08a8a; font-weight: 700; font-size: 10px; letter-spacing: .06em; }
.calldead-btn {
  font-size: 10px; padding: 2px 6px; margin-left: 4px; border-radius: 8px;
  background: #3a2626; color: #e8c9c9; border: 1px solid #7a4a4a; cursor: pointer;
}
.calldead-btn:hover { background: #4a2e2e; }
.calldead-btn.held { visibility: hidden; pointer-events: none; }   /* keeps its space while a discard is on offer */
/* The second-Charleston veto, said in the middle of the table for two seconds
   (owner, 9/11). Fixed and centred on what the person is looking at, never
   in the flow, never in the way of a tap. */
.veto-say {
  position: fixed; inset: 0; z-index: 9300; pointer-events: none;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  text-align: center; color: var(--ivory);
  background: radial-gradient(ellipse at center, #0a120de0 0%, #0a120d99 50%, #0a120d00 74%);
  animation: vetoIn .22s cubic-bezier(.18, .9, .32, 1.12);
  transition: opacity .6s ease;
}
.veto-say.gone { opacity: 0; }
.veto-hand { font-size: clamp(38px, 12vw, 80px); line-height: 1; }
.veto-who { font-family: Georgia, 'Times New Roman', serif; font-size: clamp(26px, 8vw, 54px); font-weight: 700; line-height: 1.1; color: var(--brass); text-shadow: 0 3px 18px #000c; }
.veto-what { font-size: clamp(15px, 4.2vw, 24px); font-weight: 700; letter-spacing: .04em; text-shadow: 0 2px 12px #000c; }
@keyframes vetoIn { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .veto-say { animation: none; } }
/* Marks the half of the dead-hand rule that is this house's, not the League's:
   the NMJL penalty for a wrong call is 50 cents, which means nothing in an app. */
.house-rule {
  display: inline-block; padding: 1px 6px; border-radius: 999px;
  background: #4a3a1e; color: #e8c97a; border: 1px solid #6b552c;
  font-size: 0.78em; letter-spacing: 0.04em; text-transform: uppercase;
  vertical-align: 1px;
}
.oseat.pos-top { grid-area: top; }
.oseat.pos-left { grid-area: left; }
.oseat.pos-right { grid-area: right; }
.onm { display: flex; align-items: center; gap: 6px; font-size: 12px; }
.onm .oname { font-weight: 600; }
.onm .owind { color: var(--brass); font-weight: 700; }
.onm .ocount { opacity: .7; margin-left: auto; white-space: nowrap; }
/* Running-score chip beside a player's name. */
.oscore {
  font-weight: 800; font-size: 11px; line-height: 1; padding: 2px 6px; border-radius: 999px;
  background: rgba(255, 255, 255, 0.08); color: var(--ink, #e8efe9); white-space: nowrap;
}
.oscore.pos { background: rgba(123, 224, 123, 0.16); color: #8fe08f; }
.oscore.neg { background: rgba(255, 120, 120, 0.16); color: #ff9a9a; }
.myarea .label .oscore { font-size: 12px; }
.backs { display: flex; gap: 2px; }
.backs.horiz { flex-wrap: wrap; max-width: 300px; }
.backs.vert { flex-direction: column; }
.backs.horiz .tileback { width: 14px; height: 20px; }
.backs.vert .tileback { width: 24px; height: 14px; }
.oseat .exposures { gap: 4px; }

/* The felt: a lit centre falling off to a dark rail, over a procedural weave. The
   two fine diagonal gratings cross to read as woven cloth instead of flat colour —
   cheap (no image, no bundle weight) and it gives the table a surface. */
.felt {
  grid-area: center;
  background:
    repeating-linear-gradient(45deg, #ffffff06 0 1px, transparent 1px 3px),
    repeating-linear-gradient(-45deg, #00000008 0 1px, transparent 1px 3px),
    radial-gradient(ellipse at center, var(--t-1c6344), var(--t-0f3a25));
  border: 1px solid #0006; border-radius: 14px; padding: 10px 12px;
  display: flex; flex-direction: column; min-height: 0;
  /* Rim shadow + a soft inner glow, so the table looks lit from above. */
  box-shadow: inset 0 0 40px #00000040, inset 0 1px 0 #ffffff14, inset 0 -18px 40px #00000030;
  position: relative; overflow: hidden;
}
/* Table watermark: centred, faint, behind the discards, non-interactive. */
.felt-wm { position: absolute; inset: 0; z-index: 0; display: flex; align-items: center; justify-content: center;
  opacity: .1; pointer-events: none; }
.felt-wm svg { width: min(52%, 230px); height: auto; }
/* Cream characters on the felt, spaced like a carved table inscription. */
.felt-kanji { font-size: clamp(30px, 11vw, 82px); line-height: 1; letter-spacing: .1em; text-indent: .1em;
  color: var(--ivory); font-weight: 700; white-space: nowrap; user-select: none; }
.felt > .feltinfo, .pile-wrap > .pile { position: relative; z-index: 1; }
/* The well: watermark above the baize, discards above the watermark. */
.pile-wrap { position: relative; }
.pile-wrap > .felt-wm { opacity: .14; }
/* Five characters must fit the well: on an iPad the felt is narrow beside the
   side racks, and 82px clipped the first and last (store shoot, 9/10). */
@media (min-width: 768px) { .pile-wrap > .felt-wm .felt-kanji { font-size: clamp(30px, 6.5vw, 82px); } }
.feltinfo { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: 12px; opacity: .82; margin-bottom: 8px;
  /* ONE LINE, FIXED: the ✋ veto line (an emoji's taller line box, and a long
     name wrapping) grew this header 5-19px mid-Charleston and the pile and
     rack under it rode along (layouttest, 9/12). */
  height: 18px; overflow: hidden; }
.feltinfo > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* The discard pile is capped at a few rows: when it would grow past the cap the
   oldest row is dropped (in JS) so the newest discards stay at the bottom and the
   pile never marches down the screen. */
/* EVERY DISCARD IS VISIBLE, AND THE ROWS DO NOT SHUFFLE. Two reports from Axe,
   one rule behind both.
   - "There needs to be a way to see all of the discards at once. This is the
     case in a real game." This was `overflow: hidden`, so past about six rows
     the OLDEST discards were clipped and simply unreachable - not merely hard
     to read. At a real table you can always count the pile, and reading it is
     most of the skill. It scrolls now.
   - "it goes back and forth between five and six horizontally." flex-wrap fits
     as many as happen to go, so the count changed whenever the column width did
     - a scrollbar appearing was enough - and the whole pile re-flowed under the
     eye. A fixed track pins it: the row count is now a property of the layout,
     not of how many tiles have been thrown. */
/* THE PILE HOLDS ITS SIZE FROM THE FIRST TILE. It used to grow with the
   discards - a new row every seven tiles pushed the whole rack down a step,
   and on the web every draw or pass read as the screen resizing (owner, 9/8:
   "the screen seems to be moving all the time"). Reserved at its maximum, so
   nothing below it moves for the whole hand; the felt is a table, empty felt
   is fine. */
.pile { display: grid; grid-template-columns: repeat(auto-fill, 46px);
  justify-content: start; align-content: start; gap: 4px;
  overflow-y: auto; overflow-x: hidden; max-height: 290px; min-height: 290px;
  overscroll-behavior: contain;
  /* The 4px breathing room keeps the claimable tile's brass ring (outline +
     glow, both painted OUTSIDE the box) from being shaved off at the edges. */
  padding: 4px; }
/* The claimable discard doesn't just sit there — it breathes, so your eye is
   pulled to the one tile you can actually act on. */
.pile .tile.open { outline: 3px solid var(--brass); outline-offset: 1px; animation: openPulse 1.35s ease-in-out infinite; }
@keyframes openPulse {
  0%, 100% { box-shadow: 0 2px 3px #0004, 0 0 0 0 #d9a44100, inset 0 2px 2px #ffffffcc; }
  50%      { box-shadow: 0 2px 3px #0004, 0 0 14px 3px #d9a44188, inset 0 2px 2px #ffffffcc; }
}
/* The impact when a thrown tile lands: a quick squash that settles. Applied by
   main.js at the end of the fly-from-seat animation, so the tile hits the table
   with some weight instead of just appearing. */
/* The discard pile stays a clean grid on purpose — it reads as a tidy table and
   makes the pile easy to scan. Only the landing squash animates. */
.pile .tile.landed { animation: tileLand .26s cubic-bezier(.2, 1.6, .4, 1); }
@keyframes tileLand {
  0%   { transform: scale(1.16); }
  55%  { transform: scale(.94); }
  100% { transform: scale(1); }
}
/* NOTE — do NOT put keyframe animations on the seats, melds, or anything else the
   board rebuilds. render() recreates that DOM after every move, which restarts the
   animation each time; a meld "slide in" therefore blinked on every turn and every
   sound, and a looping seat pulse never got to finish a cycle. Only elements that
   survive a render (or one-shot classes applied deliberately, like .landed on the
   newest discard) may animate. Static styling for the active seat instead: */
.oseat.active { box-shadow: 0 0 0 2px #d9a44199, 0 0 14px 2px #d9a4412e; }
/* Progress bars glide to their new length instead of jumping, so a draw that moves
   you from 9/14 to 10/14 registers as progress. */
.tchip .tbar span { transition: width .35s cubic-bezier(.2, .8, .3, 1); }
/* Every button takes the press. Cheap, and it makes the whole UI feel responsive. */
button:active:not(:disabled) { transform: scale(.96); }
button { transition: transform .07s ease, background-color .12s ease, border-color .12s ease; }

@media (prefers-reduced-motion: reduce) {
  .tchip .tbar span { transition: none; }
  button:active:not(:disabled) { transform: none; }
}
.myarea.you { grid-area: bottom; }

.opponents { display: flex; gap: 12px; flex-wrap: wrap; }
.seat {
  background: #0000002e; border: 1px solid #0005; border-radius: 10px;
  padding: 8px 12px; min-width: 130px;
}
.seat.active { border-color: var(--brass); box-shadow: 0 0 0 2px #d9a44155; }
.seat .nm { font-weight: 600; display: flex; justify-content: space-between; align-items: center; }
.seat .meta { font-size: 12px; opacity: .8; margin-top: 3px; }
.seat .backrow { display: flex; gap: 2px; margin-top: 6px; flex-wrap: wrap; }
.exposures { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.exgroup { display: flex; gap: 2px; padding: 3px; background: #ffffff10; border-radius: 6px; }
/* Side seats: stack melds (and the tiles within them) vertically so the column
   stays narrow, matching the vertical fan of hand backs. */
.exposures.vert { flex-direction: column; align-items: flex-start; gap: 5px; margin-top: 0; }
.exposures.vert .exgroup { flex-direction: column; gap: 1px; }
/* Hand backs and exposures sit side by side to save vertical space. */
.obody { display: flex; flex-wrap: wrap; gap: 6px; align-items: flex-start; }
.myarea .exposures { margin-bottom: 8px; }
.tileback {
  width: 16px; height: 22px; border-radius: 3px;
  background: linear-gradient(160deg, var(--t-1e6b46), var(--t-0f3a25)); border: 1px solid #0007;
}

.center {
  background: #00000022; border: 1px solid #0004; border-radius: 12px;
  padding: 12px; min-height: 120px;
}
.center h3 { margin: 0 0 8px; font-size: 13px; opacity: .8; font-weight: 600; }
.discardpile { display: flex; flex-wrap: wrap; gap: 5px; }
.discardpile .tile.open { outline: 3px solid var(--brass); outline-offset: 1px; }
.wallinfo { font-size: 12px; opacity: .75; margin-top: 8px; }

.myarea {
  background: #00000030; border: 1px solid #0005; border-radius: 12px; padding: 12px;
}
.myarea .label { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.myarea .label .nm { font-weight: 600; }
.sortbtn { margin-left: auto; font-size: 12px; padding: 4px 11px; background: #00000038;
  color: var(--ivory); border: 1px solid #ffffff2b; border-radius: 7px; }
.sortbtn:hover { border-color: var(--brass); color: var(--brass); }
.rack { display: flex; flex-wrap: wrap; gap: 6px; min-height: 74px; }
/* Four tiers of usefulness. Deadweight is darkest; an "in-between" tile (useful
   to another hand you're near) is only lightly dimmed; a spare (extra copy of a
   tile this hand uses) gets an amber ring. */
/* Dead-weight tiles are dimmed, not ghosted - .4 read as see-through on the
   owner's phone; .62 still clearly says "dead" while every face stays legible. */
.rack .tile.surplus { opacity: .62; filter: saturate(.55); }
.rack .tile.surplus:hover { opacity: 1; filter: none; }
/* COMMITTED TO THE PASS. Dimmer than surplus and it does NOT come back on hover,
   because unlike a surplus tile this one is not a suggestion you can argue with -
   it is already gone, you are only waiting for the others. Not tappable either:
   a tile that answers a tap looks like a tile you could still change your mind
   about. */
.rack .tile.passed,
.rack .tile.passed:hover { opacity: .34; filter: saturate(.25); cursor: default; pointer-events: none; }
/* Blue ring = this tile is part of the set you're building (a lone member, not yet
   a group of its own). */
.rack .tile.used { box-shadow: 0 0 0 3px #4d9bff, 0 0 9px 1px #4d9bffaa; }

/* A mini-set bracketed as ONE group. --gc is the bracket colour: green when a joker
   finishes the set, blue when it's all natural. Top and bottom bars run the length
   of the run; the two ends add a side cap and rounded outer corners, so a row of
   same-set tiles reads as a single boxed group that matches a row in the needs list.
   A set whose tiles are NOT adjacent shows each member as a full ring (ing-solo),
   a cue to slide them together. Live: rearranging re-brackets instantly. */
/* The colour carries a meaning, and it is one worth knowing:
     BLUE  — the set is made of real tiles. Nobody can take it apart.
     GREEN — a JOKER is standing in. That group can be broken up: an opponent may
             redeem the joker off your exposure and hand you back a gap.
   Border WEIGHT is the second signal: thin while you are still collecting, heavy
   and closed once the set is finished. */
.rack .tile.ing { --gc: #4d9bff; --gcg: rgba(77,155,255,.55); --gw: 3px;
  box-shadow: inset 0 var(--gw) 0 var(--gc), inset 0 calc(-1 * var(--gw)) 0 var(--gc); }
.rack .tile.ing-n { --gc: #4d9bff; --gcg: rgba(77,155,255,.55); }
.rack .tile.ing-j { --gc: #4bd07b; --gcg: rgba(75,208,123,.55); }
.rack .tile.ing-full { --gw: 5px; filter: brightness(1.06); }
.rack .tile.ing-mid { box-shadow: inset 0 var(--gw) 0 var(--gc), inset 0 calc(-1 * var(--gw)) 0 var(--gc); }
.rack .tile.ing-l { box-shadow: inset 0 var(--gw) 0 var(--gc), inset 0 calc(-1 * var(--gw)) 0 var(--gc), inset var(--gw) 0 0 var(--gc);
  border-top-left-radius: 11px; border-bottom-left-radius: 11px; }
.rack .tile.ing-r { box-shadow: inset 0 var(--gw) 0 var(--gc), inset 0 calc(-1 * var(--gw)) 0 var(--gc), inset calc(-1 * var(--gw)) 0 0 var(--gc);
  border-top-right-radius: 11px; border-bottom-right-radius: 11px; }
/* Attached: close the rack gap INSIDE a group so its tiles butt together and the
   border reads as one continuous box. The tile that STARTS a group keeps its gap —
   that gap is what separates set from set.
   The inner corners are squared off deliberately. Left rounded, the top and bottom
   bars followed each tile's curve and the border dipped into every crevice between
   tiles; square inner corners make one straight, clean line across the group. */
.rack .tile.ing-mid, .rack .tile.ing-r { margin-left: -4px; }
.rack .tile.ing-l { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.rack .tile.ing-mid { border-radius: 0; }
.rack .tile.ing-r { border-top-left-radius: 0; border-bottom-left-radius: 0; }
/* Drop the tile's own 1px edge where two tiles of a set meet. Left in, it put a
   pale seam through the coloured bar at every junction, so the outline read as
   separate boxes touching rather than one box around the set. */
.rack .tile.ing-l { border-right-color: transparent; }
.rack .tile.ing-mid { border-left-color: transparent; border-right-color: transparent; }
.rack .tile.ing-r { border-left-color: transparent; }
/* A soft halo in the group's own colour, so a live set lifts off the felt. The
   finished ones glow a little harder — that is the reward for closing the set. */
.rack .tile.ing { filter: drop-shadow(0 0 3px var(--gcg)); }
.rack .tile.ing-full { filter: brightness(1.05) drop-shadow(0 0 7px var(--gcg)); }
/* A scattered set still has to read as FINISHED. This ring was hardcoded at 3px
   and ignored --gw, so the moment you arranged your own hand — which is exactly
   when a set's tiles stop being adjacent — a COMPLETE mini-set became
   indistinguishable from one you were still collecting. Border weight is the whole
   "is it done" signal, and hand-sorting was quietly throwing it away. Honour --gw
   so a solo ring thickens to 5px on ing-full, same as a bracketed group. */
.rack .tile.ing-solo { box-shadow: 0 0 0 var(--gw) var(--gc), 0 0 9px 1px var(--gc); }
.coach { background: #d9a4410f; border: 1px solid #d9a44133; border-radius: 10px; padding: 10px 12px; margin-bottom: 10px; }
.coach-title { font-size: 13px; margin-bottom: 8px; opacity: .95; }
.coach-title b { color: var(--brass); }
.targets { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.tchip { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 5px 10px;
  background: #00000033; border: 1px solid #ffffff1a; border-radius: 8px; color: var(--ivory); cursor: pointer; }
.tchip:hover { border-color: var(--brass); }
.tchip.top { border-color: var(--brass); background: #d9a44122; }
.tchip .tname { font-weight: 600; }
.tchip .tbar { width: 54px; height: 6px; background: #ffffff1f; border-radius: 4px; overflow: hidden; }
.tchip .tbar span { display: block; height: 100%; background: var(--brass); }
.tchip .tnum { color: var(--brass); font-weight: 700; }
/* Classic's line picker on a named card: section, line number, Track. The
   selects carry explicit colors - a bare <select> in the WebView paints
   white-on-white in the dark theme. */
.cluepick { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 8px; }
.cluepick select { font: inherit; font-size: 12px; padding: 4px 6px; max-width: 100%; color: inherit;
  background: #00000033; border: 1px solid #ffffff33; border-radius: 8px; }
.cluepick select option { color: #1d1d1d; background: #f4efe4; }
/* The chosen-set breakdown: a separated, labelled sub-panel under the chips. */
.trackpanel { margin-top: 4px; padding-top: 9px; border-top: 1px dashed #d9a44140; }
.trackhead { display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  font-size: 12.5px; margin-bottom: 3px; }
.trackhead b { color: var(--brass); }
.trackhead .tmeta { font-size: 11px; color: var(--brass); opacity: .85; white-space: nowrap; }
.trackdesc { font-size: 11.5px; opacity: .7; margin-bottom: 7px; font-style: italic; }
.trackwarn { font-size: 11.5px; color: #ffb4a2; background: #c0392b22; border: 1px solid #c0392b66;
  border-radius: 6px; padding: 4px 8px; margin-bottom: 7px; }
.tracker { display: flex; flex-wrap: wrap; gap: 6px; }
.gchip { font-size: 11.5px; padding: 3px 9px; border-radius: 20px; color: var(--ivory);
  background: #00000038; border: 1px solid #ffffff1f; }
.gchip b { color: var(--brass); }
.gchip i { font-style: normal; opacity: .7; font-size: 10.5px; }
.gchip.done { background: #1f8a4c33; border-color: #1f8a4c88; }
.gchip.done b { color: #7fe0a2; }
/* Locked = melded on the table, can't change: gold. */
.gchip.locked { background: #d9a44130; border-color: var(--brass); color: #f4dfae; font-weight: 600; }
.gchip.locked b { color: var(--brass); }
.racklegend { margin-top: 8px; }
.racklegend b { color: var(--brass); }
/* Two lines reserved: the pass/call instruction grows and shrinks between one
   line and two, and that was the last thing still nudging the panels below. */
.actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; align-items: center; min-height: 58px; }
.hint { font-size: 12px; opacity: .8; }

/* Tiles */
.tile {
  position: relative; width: 46px; height: 64px; border-radius: 8px;
  background: linear-gradient(175deg, #fff, var(--ivory) 60%, var(--ivory-edge));
  border: 1px solid var(--ivory-edge);
  box-shadow: 0 2px 3px #0004, inset 0 2px 2px #ffffffcc, inset 0 -3px 5px #00000012;
  padding: 3px; overflow: hidden; user-select: none;
  -webkit-user-select: none; -webkit-touch-callout: none;   /* long-press = magnify, not the iOS callout */
}
.tile .face { display: block; width: 100%; height: 100%; }
.rack .tile { cursor: pointer; }
/* Lift a tile on hover ONLY for real mouse pointers. On a touchscreen iOS leaves
   :hover "stuck" on the last-touched tile; with the board re-rendering during bot
   turns, that one tile would bob up and down on its own. Gating on hover/pointer
   keeps the desktop lift and kills the phantom jump on the phone. */
@media (hover: hover) and (pointer: fine) {
  .rack .tile { transition: transform .08s; }
  .rack .tile:hover { transform: translateY(-6px); }
}
/* Press feedback: the tile takes your finger. Inline transforms during a drag
   override this, so pressing and dragging never fight each other. */
.rack .tile:active { transform: scale(.94); }
/* Tiles you can act on get a real contact shadow, so they sit ON the table
   rather than being pasted to it. */
.pile .tile, .exgroup .tile { box-shadow: 0 1px 2px #0006, 0 3px 6px #00000038, inset 0 2px 2px #ffffffcc; }

/* Every bit of motion above is decoration. Honour the OS setting and drop it. */
@media (prefers-reduced-motion: reduce) {
  .pile .tile.landed { animation: none; }
  .pile .tile.open { animation: none; }
  .rack .tile:active { transform: none; }
}
.tile.small { width: 30px; height: 42px; border-radius: 6px; padding: 2px; }
.tile.sel { outline: 3px solid var(--sel); outline-offset: 1px; transform: translateY(-8px); }

/* Side panel */
.side { display: flex; flex-direction: column; gap: 12px; }
.panel { background: #00000026; border: 1px solid #0004; border-radius: 10px; padding: 10px 12px; }
.panel h3 { margin: 0 0 8px; font-size: 13px; color: var(--brass); }
.card-hand { font-size: 12px; padding: 6px 0; border-bottom: 1px dashed #ffffff1a; }
.card-hand:last-child { border-bottom: none; }
.card-hand .cat { font-weight: 600; }
.card-hand .desc { opacity: .8; }
.card-hand .pts { float: right; color: var(--brass); }

/* --- Card view (real-card look): sections + colour-coded tile patterns --- */
.cardview { display: flex; flex-direction: column; gap: 10px; }
.cv-section { border: 1px solid #00000030; border-radius: 8px; overflow: hidden; background: #fdf6e6; }
.cardview.compact .cv-focus { box-shadow: 0 0 0 2px var(--brass); }
.cv-head { color: #fdf1d8; font-weight: 800; font-size: 12px; letter-spacing: .8px; text-transform: uppercase; padding: 5px 9px; }
.cv-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; padding: 6px 9px; border-top: 1px solid #0000000f; }
.cv-row:first-of-type { border-top: none; }
.cv-row.aim {
  background: #d9a4414d; outline: 2px solid var(--brass); outline-offset: -2px;
  border-radius: 6px; box-shadow: inset 5px 0 0 var(--brass);
}
.cv-row.cv-pick { cursor: pointer; }
.cv-row.cv-pick:hover { background: #d9a4411a; }
.cv-pat { font-family: 'Consolas', ui-monospace, monospace; font-weight: 800; font-size: 15px; letter-spacing: 1px; flex: 1; min-width: 140px; }
.cv-tok { white-space: nowrap; }
.cv-gap { padding: 0 2px; }
.cv-meta { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
.cv-pts { color: #7a5a1f; font-weight: 800; font-size: 14px; }
.cv-ce { display: inline-block; width: 17px; height: 17px; line-height: 17px; text-align: center; border-radius: 4px;
  background: #2f6b3a; color: #fff; font-size: 11px; font-weight: 700; }
.cv-desc { flex-basis: 100%; font-size: 11.5px; color: #6a5a3a; opacity: .9; }
/* The card's small print: what may slide, and how many suits. Set apart from
   the description the way a real card sets its notes apart from its hands. */
/* Somebody at the table said something. Ivory like a spoken card, tipped
   toward the seat that said it, and never wide enough to cover the tiles. */
.oseat { position: relative; }
.talk-bubble {
  position: absolute; z-index: 40; left: 50%; transform: translateX(-50%);
  top: calc(100% + 6px);
  max-width: 168px; width: max-content;
  background: var(--ivory, #f4ecd8); color: #2b2318;
  border: 1px solid #00000022; border-radius: 12px;
  padding: 6px 10px; font-size: 12.5px; line-height: 1.35;
  box-shadow: 0 6px 14px #0006; pointer-events: none;
  animation: talk-in .22s ease both;
}
.talk-bubble::after {
  content: ''; position: absolute; top: -5px; left: 50%; margin-left: -5px;
  border: 5px solid transparent; border-top: 0; border-bottom-color: var(--ivory, #f4ecd8);
}
/* The side seats speak INTO the table, never off the edge of the phone: the
   left seat's bubble opens rightward, the right seat's opens leftward. (Had
   these reversed once - the right seat's line ran off a 342px screen.) */
.oseat.pos-left .talk-bubble { left: 0; right: auto; transform: none; }
.oseat.pos-left .talk-bubble::after { left: 14px; right: auto; margin-left: 0; }
.oseat.pos-right .talk-bubble { left: auto; right: 0; transform: none; }
.oseat.pos-right .talk-bubble::after { left: auto; right: 14px; margin-left: 0; }
/* Last defence on a narrow phone: never wider than the screen allows. */
.talk-bubble { max-width: min(168px, 46vw); }
.talk-bubble.going { opacity: 0; transition: opacity .4s ease; }
@keyframes talk-in { from { opacity: 0; transform: translateX(-50%) translateY(-4px) scale(.96); } }
.oseat.pos-left .talk-bubble, .oseat.pos-right .talk-bubble { animation-name: talk-in-side; }
@keyframes talk-in-side { from { opacity: 0; transform: translateY(-4px) scale(.96); } }
@media (prefers-reduced-motion: reduce) { .talk-bubble { animation: none; } }

.cv-note { flex-basis: 100%; font-size: 10.5px; letter-spacing: .02em; color: #8a6a2a; font-style: italic; margin-top: 1px; }
.cardview.compact .cv-note { font-size: 9.5px; }
/* Compact side-panel variant sits on the dark felt, so lighten the chrome. */
.cardpanel h3 { margin-bottom: 8px; }
.cardview.compact .cv-section { background: #f4ecda; }
.cardview.compact .cv-pat { font-size: 13px; letter-spacing: .5px; min-width: 110px; }
.cardview.compact .cv-row { padding: 4px 8px; }
/* THE PRINTED LAYOUT (owner, 9/21: "a little more like the real card" -
   "keep in mind your spacing issues ... it still needs to look good"). The full
   card reads like the tri-fold: a masthead across the top, then the sections
   flowing down columns - three on a laptop, two on a tablet, one on a phone -
   and a section never splits across two columns. inline-block is what keeps a
   section whole on every engine (break-inside alone is not honoured by older
   Safari inside columns); width 100% makes it fill its column. */
.learn-modal.card-modal { width: min(1180px, 96vw); }
.cardview.printed { display: block; columns: 3 320px; column-gap: 14px; }
.cardview.printed .cv-section { display: inline-block; width: 100%; margin: 0 0 12px; break-inside: avoid;
  -webkit-column-break-inside: avoid; vertical-align: top; }
.cv-mast { column-span: all; -webkit-column-span: all; display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 4px 12px; margin: 4px 0 12px; padding: 8px 12px; border-radius: 8px;
  background: #fdf6e6; color: #1e4a96; border: 1px solid #00000030; }
.cv-mast-name { font-weight: 800; font-size: 15px; letter-spacing: .9px; text-transform: uppercase; }
.cv-mast-sub { font-size: 11.5px; color: #6a5a3a; }
.card-hand.aim { background: #d9a4411f; border-radius: 6px; padding-left: 6px; margin-left: -6px; box-shadow: inset 3px 0 0 var(--brass); }
.hint.target b { color: var(--brass); }
.log { font-size: 12px; max-height: 220px; overflow-y: auto; line-height: 1.5; }
.log div { padding: 1px 0; opacity: .85; }
.log div:last-child { opacity: 1; font-weight: 600; }

/* --- Deal / wall setup: a real table, seen from above --- */
.dealwrap { grid-column: 1 / -1; display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 8px; }
.dealcap {
  font-size: 15px; color: var(--ivory); line-height: 1.4;
  background: linear-gradient(#0000005c, #0000003a); border: 1px solid #ffffff22;
  padding: 12px 20px; border-radius: 12px; max-width: 640px; text-align: center; min-height: 22px;
  box-shadow: 0 6px 18px #0006;
}
/* The felt table: wood rail, vignette, faint felt weave. */
.dealstage {
  position: relative; width: min(600px, 92vw); aspect-ratio: 1 / 1; margin: 6px auto;
  border-radius: 20px; border: 12px solid #47301c;
  background: radial-gradient(ellipse at 50% 42%, var(--t-23784f) 0%, var(--t-16583a) 55%, var(--t-0e3c28) 100%);
  box-shadow: inset 0 0 0 2px #6b4a2e, inset 0 0 70px #00000075, 0 16px 44px #0009;
}
.dealstage::before {
  content: ''; position: absolute; inset: 0; border-radius: 9px; pointer-events: none;
  background-image: repeating-linear-gradient(45deg, #ffffff08 0 2px, transparent 2px 6px);
  opacity: .5;
}
.wall { position: absolute; display: flex; gap: 1px; z-index: 1; }
.wall.top { top: 0; left: 50%; transform: translateX(-50%); flex-direction: row; }
.wall.bottom { bottom: 0; left: 50%; transform: translateX(-50%); flex-direction: row; }
.wall.left { left: 0; top: 50%; transform: translateY(-50%); flex-direction: column; }
.wall.right { right: 0; top: 50%; transform: translateY(-50%); flex-direction: column; }
/* Ivory tile backs, stacked two high. */
.wtile {
  width: 13px; height: 18px; border-radius: 3px;
  background: linear-gradient(150deg, #f7f0de 0%, #ece0c2 60%, #dccfa6 100%);
  border: 1px solid #c7b78d; box-shadow: 0 2px 0 #cbbb8e, 0 3px 3px #00000055;
  transition: opacity .25s, transform .25s;
}
.wall.left .wtile, .wall.right .wtile { width: 18px; height: 13px; box-shadow: 2px 0 0 #cbbb8e, 3px 0 3px #00000055; }
.wtile.gone { opacity: 0; transform: scale(.4); }
.wtile.brk { outline: 2px solid var(--brass); outline-offset: 1px; box-shadow: 0 0 9px 2px #d9a44188, 0 3px 3px #0006; z-index: 2; }
.dealcenter { position: absolute; inset: 24% 20%; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 16px; }
.dealcenter::before {
  content: ''; position: absolute; width: 78%; aspect-ratio: 1; border-radius: 50%;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  border: 2px dashed #d9a44130; box-shadow: inset 0 0 40px #d9a4410f; pointer-events: none;
}
.dice { display: flex; gap: 30px; z-index: 1; perspective: 700px; padding: 26px 0 34px; }
.die {
  --dz: 29px;   /* half the cube size — keep in sync with width/height */
  position: relative; width: 58px; height: 58px; transform-style: preserve-3d;
  transform: rotateX(-28deg) rotateY(-34deg);
  /* NO filter here — a CSS filter flattens 3-D children and collapses the cube.
     Grounding comes from the ::after contact shadow instead. */
  animation: dieidle 5s ease-in-out infinite;
}
/* Soft contact shadow on the felt beneath each die. */
.die::after {
  content: ''; position: absolute; left: 50%; bottom: -24px; width: 60px; height: 16px;
  transform: translateX(-50%) rotateX(80deg); border-radius: 50%;
  background: radial-gradient(ellipse, #0009, transparent 70%); filter: blur(2px); z-index: -1;
}
.dface {
  position: absolute; inset: 0; border-radius: 12px; padding: 8px;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px;
  background: radial-gradient(circle at 32% 26%, #fffef9 0%, #f0e7d1 62%, #e1d4b4 100%);
  box-shadow: inset 0 0 0 1.5px #d0bf94, inset 0 4px 7px #ffffffcc, inset 0 -5px 10px #0000001f;
  backface-visibility: hidden;
}
.df-front  { transform: translateZ(var(--dz)); }
.df-back   { transform: rotateY(180deg) translateZ(var(--dz)); }
.df-right  { transform: rotateY(90deg)  translateZ(var(--dz)); filter: brightness(0.7); }
.df-left   { transform: rotateY(-90deg) translateZ(var(--dz)); filter: brightness(0.7); }
.df-top    { transform: rotateX(90deg)  translateZ(var(--dz)); filter: brightness(1.16); }
.df-bottom { transform: rotateX(-90deg) translateZ(var(--dz)); filter: brightness(0.5); }
.die .pip {
  width: 9px; height: 9px; border-radius: 50%; align-self: center; justify-self: center;
  background: radial-gradient(circle at 35% 30%, #555, #0e0e0e 75%);
  box-shadow: inset 0 1px 1px #000, inset 0 -1px 1px #fff2, 0 0 1px #0007;
}
.die .pip.red { background: radial-gradient(circle at 35% 30%, #e35761, #9c141f 75%); }
.die span { display: block; }
/* A dramatic tumble: two full flips, land with a bounce. The two dice are offset
   so they don't roll in lockstep. */
.dice.rolling .die { animation: diceroll 1.05s cubic-bezier(.16, .8, .28, 1) both; }
.dice.rolling .die:last-child { animation-delay: .1s; animation-duration: 1.18s; }
@keyframes diceroll {
  0%   { transform: translateY(-52px) rotateX(-880deg) rotateY(680deg) scale(.72); }
  50%  { transform: translateY(8px) rotateX(-90deg) rotateY(-110deg) scale(1.12); }
  68%  { transform: translateY(-16px) rotateX(-18deg) rotateY(-14deg) scale(1); }
  82%  { transform: translateY(5px) rotateX(-34deg) rotateY(-38deg); }
  92%  { transform: translateY(-4px) rotateX(-22deg) rotateY(-26deg); }
  100% { transform: translateY(0) rotateX(-26deg) rotateY(-30deg); }
}
/* Gentle idle wobble so the dice always read as solid 3-D objects. */
@keyframes dieidle {
  0%, 100% { transform: rotateX(-26deg) rotateY(-30deg); }
  50%      { transform: rotateX(-21deg) rotateY(-37deg); }
}
@media (prefers-reduced-motion: reduce) { .die { animation: none; } }
.dealtap { font-size: 13px; color: var(--brass); margin-top: 8px; font-weight: 700; letter-spacing: .3px; animation: tappulse 1.5s ease-in-out infinite; }
@keyframes tappulse { 0%, 100% { opacity: .5; } 50% { opacity: 1; } }
.piles { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; z-index: 1; }
.pilechip {
  font-size: 12px; background: linear-gradient(#00000055, #00000033); border: 1px solid #ffffff22;
  border-radius: 9px; padding: 6px 10px; display: flex; align-items: center; gap: 7px; color: var(--ivory);
  box-shadow: 0 3px 8px #0005;
}
.pilechip.hot { border-color: var(--brass); animation: pilepulse 1s ease-in-out infinite; }
@keyframes pilepulse {
  0%, 100% { box-shadow: 0 0 0 2px #d9a44144, 0 3px 8px #0006; }
  50% { box-shadow: 0 0 0 4px #d9a44177, 0 3px 12px #0007; }
}
.pilechip.dealer { background: linear-gradient(#d9a44133, #d9a44111); border-color: var(--brass); }
.pilechip .easttag { font-size: 10px; font-weight: 700; color: var(--ink); background: var(--brass);
  padding: 1px 6px; border-radius: 10px; }
.pilechip .cnt { margin-left: auto; font-weight: 800; color: var(--brass); font-size: 16px; }
.center-actions { justify-content: center; }

/* The deal played inline in the centre felt. */
.dealfelt { align-items: center; justify-content: center; gap: 12px; text-align: center; }
.dealcap-inline {
  font-size: 14px; line-height: 1.4; color: var(--ivory); max-width: 94%;
  background: #00000033; border: 1px solid #ffffff1c; border-radius: 10px; padding: 8px 14px;
}
.dealpiles { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.dealtap { margin-top: 0; }
.dealskip {
  font-size: 12px; padding: 5px 13px; background: #00000038; color: var(--ivory);
  border: 1px solid #ffffff2b; border-radius: 7px;
}
.dealskip:hover { border-color: var(--brass); color: var(--brass); }
/* The Siamese deal ritual: the same felt, standing alone in the siamese root
   (which has no board grid), so it sizes itself rather than filling a cell. */
.sia-dealwrap { display: flex; justify-content: center; padding: 18px 10px; }
.sia-dealwrap .dealfelt { grid-area: auto; width: min(440px, 94vw); min-height: 300px; margin: 0 auto; cursor: pointer; }
.sia-dealwrap .dealpiles { width: 100%; max-width: 360px; }

.banner {
  text-align: center; padding: 14px; margin: 10px 14px 0; border-radius: 12px;
  background: var(--brass); color: var(--ink);
}
.banner .btitle { font-weight: 800; font-size: 17px; }
.banner .bsub { font-size: 13px; opacity: .85; margin-top: 3px; }
.banner .bpay { display: flex; gap: 14px; justify-content: center; margin-top: 8px; font-weight: 700; font-size: 13px; }
.banner .bpay .pos { color: #0b5a2a; }
.banner .bpay .neg { color: #8f2018; }
.banner .btower { margin-top: 10px; padding-top: 8px; border-top: 1px solid #00000026; font-weight: 700; font-size: 14px; }
.banner-actions { display: flex; gap: 10px; justify-content: center; margin-top: 12px; }
.banner-actions button { background: #00000022; border: 1px solid #00000033; color: var(--ink); font-weight: 700; }
.banner-actions button.primary { background: var(--ink); color: var(--brass); border-color: var(--ink); }
.scorerow { display: flex; justify-content: space-between; font-size: 13px; padding: 3px 0; }
.scorerow .val.pos { color: #7fe0a2; font-weight: 700; }
.scorerow .val.neg { color: #e59; font-weight: 700; }
/* --- Title / mode-select screen --- */
.title-screen {
  /* SCROLL WHEN IT DOES NOT FIT (owner, 8/31: "still not scrolling ... it's
     cutting off"). This was overflow:hidden, which clipped 334px of the title
     screen on any window that is wide but SHORT - the mode cards, the name box
     and the zodiac picker could not be reached at all, and the wheel did
     nothing, because hidden disables user scrolling entirely. The narrow-screen
     rule further down already knew this; short screens were never covered.
     overflow-x stays hidden: the deco tiles drift sideways and must not make
     the page scroll across. */
  position: fixed; inset: 0; z-index: 40;
  overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain;
  background:
    radial-gradient(ellipse at 50% 0%, var(--t-1e6b46) 0%, var(--t-0d3122) 55%, var(--t-082018) 100%);
  /* NEVER align-items:center on a scrollable container. A centred flex item
     that overflows is clipped at its START, and scrollTop cannot go negative,
     so that content is unreachable for good - the owner could scroll, but the
     app title and the daily cards were 235px above the top of the scroll range
     (8/31). margin:auto on the panel does the centring instead: it takes the
     free space when there is some, and resolves to 0 rather than negative when
     there is not. No height threshold to guess at, which is what my first
     attempt got wrong.
  */
  display: flex; align-items: flex-start; justify-content: center; padding: 24px;
  /* CLEAR THE PILL. viewport-fit=cover means this screen runs under the Dynamic
     Island and the notch, and the title's own menu button already stepped around
     them - the CONTENT did not, so the tile and the wordmark sat behind the pill.
     Plain value first, then the inset: iOS 15.1 is the floor and a device with no
     inset resolves env() to 0, so max() keeps the original 24px there. */
  padding-top: 24px;
  padding-top: max(24px, calc(env(safe-area-inset-top) + 10px));
  padding-bottom: max(24px, env(safe-area-inset-bottom));
}
.title-screen[hidden] { display: none; }
/* THE MENU ON THE TITLE SCREEN. In the corner, and inside .title-screen: the
   topbar's own menu is sealed in that bar's stacking context (sticky + z-index
   40) and can never paint above this screen, whatever z-index it is given. So
   the button lives here and the dropdown is MOVED here while it is open. */
.title-menu-btn { position: absolute; z-index: 3;
  top: max(10px, env(safe-area-inset-top)); right: max(10px, env(safe-area-inset-right));
  padding: 9px 13px; font-size: 14px; }
.title-screen > #menu-drop { position: absolute; z-index: 4; right: max(10px, env(safe-area-inset-right));
  top: calc(max(10px, env(safe-area-inset-top)) + 44px);
  max-height: 70vh; max-height: 70dvh; overflow-y: auto; }
.title-deco { position: absolute; inset: 0; pointer-events: none; opacity: .16; overflow: hidden; }
.title-deco .tile { position: absolute; top: 100%; width: 54px; height: 74px; }
.title-deco .drift { animation: drift var(--dur, 22s) linear infinite; animation-delay: var(--delay, 0s); }
@keyframes drift {
  from { transform: translateY(14vh) rotate(var(--ra, -8deg)) scale(var(--s, 1)); }
  to   { transform: translateY(-124vh) rotate(var(--rb, 8deg)) scale(var(--s, 1)); }
}
.title-panel { position: relative; z-index: 1; width: min(920px, 100%); text-align: center; margin: auto; }
.title-head { display: flex; align-items: center; justify-content: center; gap: 18px; margin-bottom: 8px; flex-wrap: wrap; }
.title-mascot .tile.big { width: 92px; height: 124px; box-shadow: 0 10px 30px #0007; transform: rotate(-6deg); }
/* Title screen wordmark, same engraved caps as the top bar, tracked out wider still
   at display size. */
.title-heading h1 { margin: 0; font-family: 'Parlor', Copperplate, 'Times New Roman', serif; font-weight: 600;
  font-size: 50px; line-height: 1.08; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ivory); text-align: left; }
.title-heading h1 span { display: block; color: var(--brass); text-shadow: 0 2px 0 #00000040; }
.title-sub { margin: 8px 0 0; color: #ffffffb0; font-size: 14px; text-align: left; }
.title-modes { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin: 26px 0 18px; }
.mode-card.tower-card { background: linear-gradient(180deg, #d9a44122, #00000030); border-color: #d9a44155; }
.mode-card.tower-card:hover { border-color: var(--brass); }
.mode-card {
  background: linear-gradient(180deg, #ffffff0f, #00000030); border: 1px solid #ffffff26;
  border-radius: 16px; padding: 18px 16px 20px; cursor: pointer; color: var(--ivory);
  display: flex; flex-direction: column; align-items: center; gap: 10px; transition: transform .12s, border-color .12s, box-shadow .12s;
}
.mode-card:hover { transform: translateY(-6px); border-color: var(--brass); box-shadow: 0 14px 34px #0006; }
.mode-icons { display: flex; height: 64px; align-items: flex-end; justify-content: center; }
.mode-icons .tile { width: 40px; height: 54px; margin-left: -14px; box-shadow: 0 4px 10px #0006; transform: rotate(calc((var(--k) - 1) * 7deg)); }
.mode-icons .tile:first-child { margin-left: 0; }
.mode-tag { font-size: 11px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--brass); font-weight: 700; }
/* The title screen's "who's on?" pill, on the Play Online card. The dot goes green
   the moment anybody is at a table or browsing the lounge. */
.title-refer {
  display: block; margin: 18px auto 6px; padding: 9px 22px;
  font: 600 13.5px system-ui, sans-serif; letter-spacing: .3px;
  color: var(--ivory, #f5efdc); background: #ffffff10;
  border: 1px solid #c9a55866; border-radius: 99px; cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.title-refer:hover { border-color: var(--brass, #c9a558); background: #c9a55818; }
.online-now {
  margin-left: 8px; padding: 1px 8px; border-radius: 99px;
  font-size: 10px; letter-spacing: .4px; text-transform: none; font-weight: 600;
  background: #00000026; color: var(--ivory, #f5efdc); opacity: .85;
}
.online-now::before { content: '●'; margin-right: 5px; font-size: 8px; color: #8a8a8a; vertical-align: 1px; }
.online-now.lit::before { color: #47d764; }
/* "A table just opened" / "Greta is online" toast — shown over ANY offline mode,
   so it lives on body. Raised above the presence pill's row so the two never
   collide, and BELOW every modal overlay (learn 50, result 60, name 130): a
   notification must never sit on top of a dialog. */
.online-toast {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: 58px;   /* iOS 15.1: keep a plain fallback before env() */
  bottom: calc(58px + env(safe-area-inset-bottom, 0px));
  z-index: 46;
  display: flex; align-items: center; gap: 12px;
  width: max-content;
  max-width: min(92vw, 440px);
  background: linear-gradient(180deg, var(--t-17402ff6), var(--t-0f2f21f6));
  border: 1px solid var(--brass, #c9a558);
  border-radius: 14px; padding: 11px 12px 11px 11px;
  color: var(--ivory, #f5efdc); font-size: 14px;
  box-shadow: 0 10px 32px #000c, inset 0 1px 0 #ffffff14;
  animation: olt-in .28s cubic-bezier(.2, .9, .3, 1.2);
}
@keyframes olt-in { from { opacity: 0; transform: translate(-50%, 16px) scale(.97); } to { opacity: 1; transform: translate(-50%, 0) scale(1); } }
.online-toast.gone { opacity: 0; transition: opacity .35s ease; }
.online-toast .olt-ico {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 20px; background: var(--t-0b241a); border: 1px solid #ffffff1e;
  box-shadow: inset 0 2px 6px #0008;
}
.online-toast .olt-stack { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; line-height: 1.25; }
/* WRAPS, NEVER CLIPS (UI audit, 9/14: "Nothing … / Create an a…" on a phone, and
   169 of 617px on a desktop). A toast that explains why nothing was saved has to
   be readable.
   BREAK-WORD, NOT ANYWHERE (owner, 9/15: Axe's invite printed ONE LETTER PER
   LINE down the whole phone). The toast is fixed and centred, so it sizes to its
   content - and `overflow-wrap: anywhere` makes that content's narrowest size a
   single character, so the card shrank to a letter's width. `break-word` only
   breaks a word too long for the line and leaves the sizing alone. */
.online-toast .olt-stack b { font-size: 14px; white-space: normal; overflow-wrap: break-word; }
.online-toast .olt-sub { font-size: 12px; opacity: .75; white-space: normal; overflow-wrap: break-word; }
.online-toast .olt-go {
  font: inherit; font-weight: 700; font-size: 13px; padding: 8px 15px; border-radius: 99px;
  border: 1px solid #0006; background: var(--brass, #c9a558); color: #1c1508;
  cursor: pointer; flex: 0 0 auto; box-shadow: 0 2px 6px #0007;
}
.online-toast .olt-go:active { transform: translateY(1px); }
.online-toast .olt-x {
  font: inherit; background: none; border: none; color: var(--ivory, #f5efdc);
  opacity: .5; cursor: pointer; padding: 4px 6px; flex: 0 0 auto; font-size: 13px;
}
.online-toast .olt-x:hover { opacity: 1; }
/* The presence pill: "● N" others online. CENTERED along the bottom edge, in the
   safe-area gap below the rack (owner, 9/3: "just center it at the bottom of the
   screen") - the old bottom-LEFT corner sat on top of the rack. */
#presence-chip {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: 6px;   /* iOS 15.1: plain fallback before env() */
  bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  z-index: 45;
  font: 600 12px/1 system-ui, sans-serif;
  padding: 7px 11px; border-radius: 99px;
  background: var(--t-0e2b1fd9); color: #9aa79f;
  border: 1px solid #ffffff22; cursor: pointer;
  box-shadow: 0 3px 12px #0008;
}
#presence-chip.lit { color: var(--ivory, #f5efdc); border-color: var(--brass, #c9a558); }
/* Not on the title screen or the offline room. Both are fixed to the bottom-left
   corner, which on a 375px phone is squarely on top of the Play Online card's
   body copy — and that card already reports the headcount in words. The pill is
   for the modes, where nothing else does. */
body.mj-home #presence-chip { display: none; }
/* IN-GAME ITEMS BELONG IN A GAME. The title screen builds its own ☰ button that
   MOVES the one shared #menu-drop into itself, so the in-game list came with it -
   and syncMenuForMode(), which is what normally hides these, is bound only to the
   topbar button and never ran. So "Deal again" and "New game" sat on the title
   screen looking live, and pressing one dealt a whole hand BEHIND the title
   (.title-screen is fixed, inset 0, z-index 40) - a game the player could neither
   see nor reach, and no sign anything had happened. Home is meaningless here too:
   you are already home, and the offline room has its own Back. */
body.mj-home #deal-again,
body.mj-home #new-game,
body.mj-home #to-title { display: none; }
/* The parlor's answer card: what the one-line banner could not say, because any
   background request that succeeded blanked it within about 30ms. Brass edge so
   it reads as an answer to what you just did, not as another thing to start. */
.parlor-notice { border-color: var(--brass, #c9a558); }
.parlor-notice h3 { color: var(--brass, #c9a558); }
/* The parlor strip (title screen): one slim line above the mode cards, present
   ONLY while somebody is actually online. The layout beneath never moves —
   when the parlor is empty the strip does not exist. */
.parlor-strip {
  display: block; width: 100%; margin: 0 0 12px; padding: 9px 16px;
  font: 600 14px/1.3 system-ui, sans-serif; letter-spacing: .2px;
  text-align: center; cursor: pointer;
  color: var(--ivory, #f5efdc);
  background: linear-gradient(90deg, #c9a55822, #c9a5580d, #c9a55822);
  border: 1px solid var(--brass, #c9a558); border-radius: 99px;
  box-shadow: 0 3px 14px #0007, inset 0 1px 0 #ffffff14;
  animation: strip-in .3s ease;
}
.parlor-strip:hover { background: linear-gradient(90deg, #c9a55833, #c9a55814, #c9a55833); }
@keyframes strip-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
#presence-chip::before { content: '●'; margin-right: 6px; font-size: 9px; color: #8a8a8a; vertical-align: 1px; }
#presence-chip.lit::before { color: #47d764; }
.mode-body h2 { margin: 2px 0 4px; font-size: 22px; }
.mode-body p { margin: 0 0 10px; font-size: 13px; opacity: .82; min-height: 52px; }
.mode-play { color: var(--brass); font-weight: 700; font-size: 14px; }
/* Statistics sheet */
.statsview { display: flex; flex-direction: column; }
.stat-row { display: flex; justify-content: space-between; align-items: baseline; padding: 9px 4px; border-bottom: 1px dashed #ffffff14; }
.stat-row .stat-label { font-size: 14px; opacity: .9; }
.stat-row .stat-val { font-size: 17px; font-weight: 800; color: var(--brass); }
/* Tower trophy shelf in the stats sheet. */
.trophy-shelf { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; padding: 12px 4px 4px; gap: 12px; }
.trophy-shelf .stat-label { font-size: 14px; opacity: .9; flex: 1 1 140px; }
.trophy-row { display: flex; gap: 10px; }
.trophy { position: relative; font-size: 30px; line-height: 1; filter: grayscale(1) opacity(0.35); }
.trophy.won { filter: none; text-shadow: 0 0 12px rgba(255, 212, 71, 0.5); }
.trophy .trophy-x { position: absolute; right: -6px; bottom: -4px; font-size: 11px; font-weight: 800; color: var(--brass); text-shadow: 0 1px 2px #000; }
.stat-reset { margin-top: 16px; background: #ffffff10; border: 1px solid #ffffff2a; color: var(--ivory); }
.stat-reset:hover { border-color: #e57; }

.title-name { display: flex; justify-content: center; margin-bottom: 12px; }
.title-acct { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px 10px; margin: -4px 0 14px; font-size: 13px; color: var(--ivory); opacity: .92; text-align: center; }
.title-acct-say { max-width: 34ch; }
.title-name label { display: flex; align-items: center; gap: 8px; color: var(--ivory); font-size: 13px; opacity: .9; }
.title-name input { background: #00000033; border: 1px solid #ffffff33; color: var(--ivory); border-radius: 8px;
  padding: 7px 11px; font-size: 14px; font-family: var(--font); width: 160px; text-align: center; }
.title-name input:focus { outline: none; border-color: var(--brass); }
.title-name { flex-wrap: wrap; gap: 12px; }
.title-easy { background: #00000033; border: 1px solid #ffffff33; color: var(--ivory); border-radius: 8px; padding: 7px 12px; font-size: 13px; cursor: pointer; }
.title-easy.on { background: var(--brass); color: var(--ink); border-color: #b9862c; font-weight: 700; }
/* Card-year segmented picker. */
.title-year { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.title-year-label { color: var(--ivory); font-size: 13px; opacity: .9; }
.year-seg { display: inline-flex; border: 1px solid #ffffff33; border-radius: 8px; overflow: hidden; }
.year-seg button { background: #00000033; border: none; border-right: 1px solid #ffffff22; color: var(--ivory);
  padding: 7px 14px; font-size: 13px; cursor: pointer; }
.year-seg button:last-child { border-right: none; }
.year-seg button.on { background: var(--brass); color: var(--ink); font-weight: 700; }
.title-links { display: flex; gap: 12px; justify-content: center; }
.title-links button { background: transparent; border: 1px solid #ffffff33; color: var(--ivory); }
.title-links button:hover { border-color: var(--brass); background: #ffffff10; }
.title-links .title-tut { background: var(--brass); color: var(--ink); border-color: #b9862c; font-weight: 700; }
.title-links .title-tut:hover { background: #e8b451; }

/* Tutorial coach bubble */
.coach-bubble {
  position: fixed; right: 18px; bottom: 18px; z-index: 45; width: min(340px, 90vw);
  background: linear-gradient(180deg, var(--t-1e6b46), var(--t-123f2b)); color: var(--ivory);
  border: 1px solid var(--brass); border-radius: 14px; padding: 14px 16px;
  box-shadow: 0 12px 34px #0008;
}
.coach-bubble .cb-title { font-weight: 800; color: var(--brass); margin-bottom: 5px; }
.coach-bubble .cb-text { font-size: 13.5px; line-height: 1.5; opacity: .95; white-space: pre-line; }
.coach-bubble .cb-end { margin-top: 10px; background: #00000033; border: 1px solid #ffffff33; color: var(--ivory); font-size: 12px; }
.coach-bubble .cb-end:hover { border-color: var(--brass); }
@media (max-width: 720px) { .title-modes { grid-template-columns: 1fr; } .title-heading h1 { font-size: 40px; } }
@media (prefers-reduced-motion: reduce) { .title-deco .drift { animation: none; } }

/* --- Mahjong Solitaire --- */
/* Solitaire plays on the bare page gradient, which reads as empty next to the
   4-player table. Dress it as a real table instead, reusing the language already in
   the app: the same woven felt as the discard area, a warm overhead spotlight so the
   board looks lit from above, a deep vignette to pull the eye inward, and a rail
   around the edge. All procedural — no images, nothing added to the bundle. */
.table.solo-mode {
  display: block; position: relative; border-radius: 16px; overflow: hidden;
  padding: 14px 14px 18px;
  /* Fill the space rather than stopping under the board — a panel that ended
     mid-screen left a dead dark gap below it and undid the "real table" effect. */
  min-height: 72vh;
  background:
    /* the weave */
    repeating-linear-gradient(45deg, #ffffff07 0 1px, transparent 1px 3px),
    repeating-linear-gradient(-45deg, #00000009 0 1px, transparent 1px 3px),
    /* overhead lamp, hotter above the board than at the rails */
    radial-gradient(120% 80% at 50% 8%, var(--t-2a7d55) 0%, var(--t-175c3c) 42%, var(--t-0e3a26) 78%, var(--t-0a2c1d) 100%);
  box-shadow:
    inset 0 1px 0 #ffffff18,             /* lit top edge of the rail */
    inset 0 0 0 1px #00000055,           /* rail line */
    inset 0 -30px 60px #00000045,        /* falloff at the bottom */
    inset 0 0 120px #00000030;           /* vignette */
}
/* A ghosted dragon glyph, anchored off the bottom-right corner and deliberately
   bleeding past the edge so it reads as a decorative flourish. Centring it behind
   the board hid it completely except for a sliver below the tiles, which just looked
   like a smudge — the board always occupies the middle, so the ornament can't. */
.table.solo-mode::before {
  content: '龍';
  position: absolute; right: -.12em; bottom: -.24em; z-index: 0; pointer-events: none;
  font-size: min(52vh, 40vw); line-height: .8; color: var(--ivory);
  opacity: .05; user-select: none;
}
.table.solo-mode > * { position: relative; z-index: 1; }
.sol-bar { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.sol-info { font-size: 14px; } .sol-info b { color: var(--brass); }
.sol-controls { display: flex; gap: 8px; }
.sol-board { position: relative; }
.sol-scalewrap { margin: 8px auto; position: relative; }
.stile {
  position: absolute; width: 42px; height: 56px; border-radius: 6px; padding: 2px;
  background: linear-gradient(175deg, #fff, var(--ivory) 60%, var(--ivory-edge));
  border: 1px solid var(--ivory-edge);
  box-shadow: 2px 3px 5px #00000066, inset 0 2px 2px #ffffffcc;
}
.stile .face { display: block; width: 100%; height: 100%; }
.stile.free { cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  .stile.free:hover { background: #fff; transform: translate(-1px, -1px); }
}
.stile.blocked { filter: brightness(0.8) saturate(0.9); }
/* Selection and hint rings sit FLUSH against the tile. They used to be outlines
   with outline-offset: 1px, and that one pixel is a transparent gap - so picking a
   tile opened a hairline window straight through to whatever was stacked
   underneath it, which reads as the tile going see-through. A box-shadow ring
   starts at the border with no gap at all, and the tile stays solid. */
.stile.sel { box-shadow: 0 0 0 3px var(--sel), 2px 3px 5px #00000066, inset 0 2px 2px #ffffffcc; }
/* Online race: a removed tile keeps its element (patched board, not rebuilt) -
   hidden here, so a refused optimistic pick can reappear in place. */
.stile.gone { display: none; }
.stile.hint { box-shadow: 0 0 0 3px #6fd3ff, 2px 3px 5px #00000066, inset 0 2px 2px #ffffffcc; }
/* No z-index here on purpose: solitaire.js sets each tile's stacking inline so the
   upper layers cover the lower ones, and a class rule could never win against that
   anyway. The flush ring does not need it. */

/* --- How to play overlay --- */
.learn-overlay { position: fixed; inset: 0; background: #000000a8; z-index: 50;
  display: flex; align-items: center; justify-content: center; padding: 20px; }
.learn-overlay[hidden] { display: none; }
.learn-modal { background: linear-gradient(180deg, var(--t-16493200), var(--t-0d3122)), var(--felt-2);
  border: 1px solid #0007; border-radius: 14px; width: min(760px, 96vw); max-height: 90vh;
  display: flex; flex-direction: column; box-shadow: 0 20px 60px #000a; }
.learn-head { display: flex; align-items: center; padding: 14px 18px; border-bottom: 1px solid #ffffff14; }
.learn-head h2 { margin: 0; font-size: 18px; color: var(--brass); }
.learn-close { margin-left: auto; background: transparent; border: 1px solid #ffffff2a; color: var(--ivory);
  border-radius: 8px; min-width: 32px; height: 32px; padding: 0 12px; font-size: 14px; white-space: nowrap; cursor: pointer; }   /* reads '‹ Back' (owner, 9/11: back buttons throughout the menus) */
.learn-close:hover { background: #ffffff14; }
.learn-nav { display: flex; gap: 6px; flex-wrap: wrap; padding: 12px 18px; border-bottom: 1px solid #ffffff10; }
.learn-navchip { font-size: 12px; padding: 5px 11px; border-radius: 20px; cursor: pointer;
  background: #00000038; border: 1px solid #ffffff1f; color: var(--ivory); }
.learn-navchip:hover { border-color: var(--brass); }
/* SEE THE CARD, inside the rules now. A little larger and brass-bordered so it
   reads as an action, not a section jump-link. */
.learn-cardbtn { font-size: 14px; padding: 8px 15px; border-color: var(--brass);
  background: #00000050; margin-top: 4px; }
/* THE OVERLAY HAS TO SCROLL. overflow-y alone does not do it: this is a flex
   item in a column capped at 90vh, and a flex item defaults to min-height:auto,
   which refuses to shrink below its own content. So the body grew past the
   modal and was simply clipped - the card editor cut off with no way to reach
   the rest, which is most of a long card. flex:1 + min-height:0 gives it a
   real height to scroll inside. overscroll-behavior keeps a flick at the end
   from scrolling the board behind it. */
.learn-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch; padding: 6px 22px 22px; color: var(--ivory); }
.learn-sec { padding: 16px 0; border-bottom: 1px dashed #ffffff12; }
.learn-sec h3 { color: var(--brass); margin: 0 0 8px; font-size: 15px; }
.learn-sec p { margin: 8px 0; line-height: 1.55; font-size: 14px; opacity: .92; }
.learn-note { font-size: 13px; background: #d9a4411a; border-left: 3px solid var(--brass);
  padding: 8px 12px; border-radius: 0 8px 8px 0; opacity: 1; }
.learn-list { margin: 8px 0; padding-left: 20px; line-height: 1.7; font-size: 14px; }
.learn-tiles { display: flex; gap: 14px; flex-wrap: wrap; margin: 12px 0; }
.learn-tilecell { display: flex; flex-direction: column; align-items: center; gap: 5px; font-size: 11px; opacity: .8; }
.card-full { padding: 12px 0; border-bottom: 1px dashed #ffffff14; }
.card-full .cf-top { display: flex; justify-content: space-between; align-items: baseline; }
.card-full .cf-cat { font-weight: 700; color: var(--ivory); font-size: 15px; }
.card-full .cf-pts { color: var(--brass); font-weight: 700; }
.card-full .cf-desc { font-size: 14px; opacity: .85; margin-top: 3px; }
/* Card editor */
.ce-h { color: var(--brass); font-size: 14px; margin: 16px 0 8px; }
.ce-hand { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px dashed #ffffff14; }
.ce-hand > div { flex: 1; } .ce-hand .ce-pts { color: var(--brass); font-size: 12px; }
.ce-hand .ce-desc { font-size: 12px; opacity: .8; margin-top: 2px; }
.ce-del, .ce-rm, .ce-addg { background: #ffffff12; border: 1px solid #ffffff2a; color: var(--ivory); border-radius: 7px; padding: 5px 10px; cursor: pointer; font-size: 12px; }
.ce-del:hover, .ce-rm:hover { border-color: #e57; }
.ce-meta { display: flex; gap: 8px; margin-bottom: 8px; }
.ce-meta input, .ce-desc-in { background: #0003; border: 1px solid #ffffff2a; color: var(--ivory); border-radius: 7px; padding: 8px 10px; font-size: 13px; font-family: var(--font); }
.ce-meta input:first-child { flex: 1; } .ce-pts-in { width: 80px; } .ce-desc-in { width: 100%; margin-bottom: 10px; }
.ce-packbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 6px 0 10px; }
.ce-packbar button { background: #ffffff12; border: 1px solid #ffffff2a; color: var(--ivory); border-radius: 7px; padding: 5px 10px; cursor: pointer; font-size: 12px; }
/* The card shelf: which of your cards is on the table. The name field is saved
   as you type, and the delete button arms itself before it bites. */
.ce-packbar select { background: #ffffff12; border: 1px solid #ffffff2a; color: var(--ivory);
  border-radius: 7px; padding: 5px 8px; font-size: 12px; max-width: 100%; }
.ce-packname { background: #00000030; border: 1px solid #ffffff2a; color: var(--ivory);
  border-radius: 7px; padding: 5px 9px; font-size: 12px; min-width: 0; flex: 1 1 160px; }
.ce-packbar button.ce-armed { background: #7a1f1f; border-color: #c04a4a; color: #ffe9e9; }
.ce-replace { font-size: 12.5px; display: flex; align-items: center; gap: 6px; flex: 1; min-width: 220px; cursor: pointer; }
.ce-io { width: 100%; min-height: 72px; background: #0003; border: 1px solid #ffffff2a; color: var(--ivory); border-radius: 7px; padding: 8px 10px; font-size: 11px; font-family: Consolas, Menlo, monospace; margin-bottom: 8px; }
.ce-cat { color: var(--brass); font-size: 12px; letter-spacing: .08em; text-transform: uppercase; margin-top: 10px; }
.ce-hand.editing { outline: 1px solid var(--brass); border-radius: 6px; padding-left: 6px; }
.ce-hand .ce-line { font: 11px Consolas, Menlo, monospace; opacity: .55; margin-top: 2px; }
.ce-edit { background: #ffffff12; border: 1px solid #ffffff2a; color: var(--ivory); border-radius: 7px; padding: 5px 10px; cursor: pointer; font-size: 12px; }
.ce-conc { font-size: 12.5px; display: flex; align-items: center; gap: 5px; white-space: nowrap; cursor: pointer; }
.ce-quick { width: 100%; background: #0003; border: 1px solid var(--brass); color: var(--ivory); border-radius: 7px; padding: 9px 10px; font: 14px Consolas, Menlo, monospace; margin-bottom: 2px; }
.ce-quickerr { color: #ff9b8a; font-size: 12px; min-height: 16px; margin-bottom: 6px; }
.ce-groups { display: flex; flex-direction: column; gap: 6px; }
.ce-grow { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.ce-grow select { background: var(--ivory); color: var(--ink); border: 1px solid var(--ivory-edge); border-radius: 6px; padding: 5px 6px; font-size: 12px; }
.ce-params { display: flex; gap: 6px; }
.ce-addg { margin: 10px 0; }
.ce-foot { display: flex; align-items: center; gap: 14px; margin-top: 8px; }
.ce-total { font-weight: 700; opacity: .8; } .ce-total.ok { color: #7fe0a2; opacity: 1; }

/* Collapse the sidebar column on narrow OR short (landscape phone) viewports. */
@media (max-width: 860px), (max-height: 560px) { .table { grid-template-columns: 1fr; } }

/* ---------- Phone layout ---------- */
/* Also triggers on short viewports so landscape phones get the compact stacked
   layout (opponents row, felt, your hand) instead of the cramped desktop grid. */
@media (max-width: 560px), (max-height: 560px) {
  /* Clear the status bar / notch so the buttons are tappable, and declutter:
     the player-mode toggle, Edit card, and How to play live on the title/menu. */
  /* The Dynamic Island is not the old notch: it is a floating black pill sitting
     BELOW the top edge, so content placed at exactly the safe-area inset can still
     end up tucked under it. Sit the bar at the inset PLUS a margin, and floor it at
     56px for the case where the WebView reports no inset at all - which is what put
     the title under the island. */
  .topbar { gap: 6px; padding: max(56px, calc(env(safe-area-inset-top) + 12px)) max(9px, env(safe-area-inset-right)) 7px max(9px, env(safe-area-inset-left)); }
  .table { padding-left: max(8px, env(safe-area-inset-left)); padding-right: max(8px, env(safe-area-inset-right)); }
  .topbar h1 { font-size: 16px; }   /* shares the row with the Menu button now */
  .topbar .seg { display: none; }   /* mode chosen on the title screen on phones */
  .topbar button, .topbar select { font-size: 13px; padding: 7px 11px; border-radius: 6px; }
  /* On a phone the status may take TWO short lines inside the bar (a daily's
     streak, the Tower's hearts) - clamped, so the bar's height stays the Menu
     button's and nothing below it ever moves. */
  .topbar .status { margin-left: auto; font-size: 12px; white-space: normal; line-height: 1.2;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; max-height: 2.4em; overflow: hidden; }
  body { padding-bottom: env(safe-area-inset-bottom); }
  .table { padding: 8px; gap: 8px; }
  /* The fixed "● N" presence pill sat on the coaching line at the foot of the
     page (owner's 9/12 recording): the page ends below the pill now. */
  .table { padding-bottom: calc(44px + env(safe-area-inset-bottom, 0px)); }

  /* Stack the table: opponents as a compact row, big discard felt, your hand. */
  .playtable { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px; }
  .oseat { flex: 1 1 30%; min-width: 0; order: 1; padding: 6px; }
  /* The three-across row must read like the TABLE: your left-hand neighbour on
     the left, across in the middle, your right-hand neighbour on the right -
     so the turn visibly travels counter-clockwise. DOM order is screen-seat
     1(right),2(top),3(left), which unordered rendered MIRRORED (owner caught
     it watching a game: "Clyde should be on the left, Judy on the right"). */
  .oseat.pos-left { order: 1; }
  .oseat.pos-top { order: 2; }
  .oseat.pos-right { order: 3; }
  .oseat .backs { display: flex; flex-wrap: wrap; }
  .oseat .backs.vert { flex-direction: row; }   /* all opponents fan horizontally on a phone */
  .oseat .backs .tileback { width: 9px; height: 13px; }
  /* Exposures lie in a horizontal row (tiles side by side, melds wrapping) so an
     opponent's melds don't stack tall down the screen. */
  .oseat .exposures, .oseat .exposures.vert { flex-direction: row; flex-wrap: wrap; gap: 4px; align-items: flex-start; }
  .oseat .exposures.vert .exgroup { flex-direction: row; }
  .obody { flex-direction: column; }   /* name/backs above, exposures below, each horizontal */
  .onm { font-size: 11px; gap: 4px; flex-wrap: wrap; }
  .onm .ocount { flex-basis: 100%; margin-left: 0; }
  .felt { order: 4; flex: 1 1 100%; min-height: 168px; }
  .pile { max-height: 316px; min-height: 316px; }   /* four rows - iPads hold the tile-counters; reserved so the rack never moves */
  .myarea.you { order: 5; flex: 1 1 100%; }

  /* Deal: shrink the wall tiles so all 38 per side fit without overlapping. */
  .dealstage { border-width: 9px; border-radius: 15px; }
  .wtile { width: 7px; height: 10px; box-shadow: 0 1.5px 0 #cbbb8e, 0 2px 2px #00000055; }
  .wall.left .wtile, .wall.right .wtile { width: 10px; height: 7px; box-shadow: 1.5px 0 0 #cbbb8e, 2px 0 2px #00000055; }
  .dealcenter { inset: 28% 14%; gap: 10px; }
  .dice { gap: 20px; padding: 18px 0 24px; }
  .die { --dz: 23px; width: 46px; height: 46px; }
  .die::after { bottom: -18px; width: 48px; }
  .piles { gap: 5px; }
  .pilechip { font-size: 11px; padding: 5px 7px; }

  .rack { gap: 4px; }
  .tile { width: 40px; height: 56px; }
  .tile.small { width: 26px; height: 36px; }
  .coach-bubble { left: 8px; right: 8px; bottom: 8px; width: auto; }
  .targets, .tracker { gap: 5px; }
  .tchip { font-size: 11px; }

  /* Title screen: keep the header on screen and shrink the mode cards. */
  .title-screen { align-items: flex-start; overflow-y: auto; padding: 16px 14px 28px;
    /* The phone rule, where the pill actually is. Same shape as the base rule. */
    padding-top: 16px;
    padding-top: max(16px, calc(env(safe-area-inset-top) + 10px));
    padding-bottom: max(28px, env(safe-area-inset-bottom)); }
  .title-heading h1 { font-size: 34px; }
  .title-mascot .tile.big { width: 66px; height: 90px; }
  .title-modes { gap: 10px; margin: 18px 0 14px; }
  .mode-card { padding: 12px; }
  .mode-icons { height: 44px; }
  .mode-icons .tile { width: 32px; height: 44px; }
  .mode-body h2 { font-size: 19px; }
  .mode-body p { font-size: 12px; min-height: 0; }
}
@media (prefers-reduced-motion: reduce) { .rack .tile { transition: none; } }

/* Drag-to-rearrange the rack. touch-action:none stops the page from scrolling
   while you drag a tile sideways. */
.rack .tile { touch-action: none; cursor: grab; }
/* The dragged tile must follow the finger with ZERO transition — the .08s ease on
   .rack .tile above would otherwise lag every move and feel rubbery, not buttery. */
.rack .tile.dragging {
  cursor: grabbing; z-index: 6; opacity: 0.98; will-change: transform;
  transition: none !important; box-shadow: 0 14px 26px rgba(0, 0, 0, 0.45);
}
.rack .tile.dragging:hover { transform: none; }   /* let the JS transform win */

/* Change-name modal (from the in-game menu). */
.name-overlay { position: fixed; inset: 0; z-index: 130; display: flex; align-items: center; justify-content: center;
  background: rgba(6, 12, 9, 0.6); padding: 20px; }
/* A SHEET NEVER OUTGROWS THE SCREEN (UI audit, 9/14: the zodiac picker was 646px
   tall on a 390px sideways phone - heading and Done off-screen, the last row cut
   in half, no scroll). It scrolls inside itself instead. */
.name-modal { background: var(--t-14251b); border: 1px solid #ffffff26; border-radius: 14px; padding: 20px 22px; width: min(360px, 92vw);
  max-height: calc(100vh - 40px); max-height: calc(100dvh - 40px); overflow-y: auto; -webkit-overflow-scrolling: touch; }
.name-modal h3 { margin: 0 0 4px; color: var(--brass); }
.name-modal .name-sub { margin: 0 0 14px; font-size: 13px; opacity: .8; }
.name-modal input { width: 100%; box-sizing: border-box; background: #00000033; border: 1px solid #ffffff33;
  color: var(--ivory); border-radius: 8px; padding: 10px 12px; font-size: 16px; font-family: var(--font); }
.name-modal input:focus { outline: none; border-color: var(--brass); }
.name-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; }
.name-actions button { padding: 9px 18px; border-radius: 9px; border: 1px solid #ffffff2a; background: #ffffff10; color: var(--ivory); font-weight: 700; cursor: pointer; }
.name-actions .primary { background: var(--brass); color: var(--ink); border-color: transparent; }

/* ---------- Siamese (two-hands-each) screen ---------- */
.si-wrap { display: flex; flex-direction: column; gap: 10px; padding: 10px; max-width: 900px; margin: 0 auto; }
.si-opp { background: #00000030; border: 1px solid #0005; border-radius: 12px; padding: 7px 10px; }
.si-opp-name { font-weight: 600; font-size: 12px; margin-bottom: 5px; }
.si-opp-hands { display: flex; gap: 8px; flex-wrap: wrap; }
.si-opp-hand { flex: 1 1 40%; min-width: 0; background: #0000002e; border: 1px solid #0005; border-radius: 10px; padding: 6px; }
.si-opp .tileback { width: 12px; height: 16px; }   /* compact backs — the opponent's hands are just a count */
.si-opp-hand.locked { border-color: var(--brass); box-shadow: 0 0 0 1px #d9a44144 inset; }
.si-hand-head { font-size: 12px; opacity: .9; margin-bottom: 6px; display: flex; justify-content: space-between; gap: 8px; }
.si-backs { display: flex; flex-wrap: wrap; gap: 2px; }
.si-exposes { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
/* order:0 cancels the 4-player mobile rule `.felt { order: 2 }` that was leaking in
   via the shared .felt class and shoving the discard pile to the very bottom. */
.si-felt { order: 0; border-radius: 14px; padding: 10px 12px; min-height: 96px; }
.si-mine { background: #00000030; border: 1px solid #0005; border-radius: 12px; padding: 12px; }
.si-mine-label { font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.si-cardbtn { font-weight: 700; }
.si-myhand { background: #00000024; border: 1px solid #ffffff12; border-radius: 10px; padding: 8px 10px; margin-bottom: 8px; }
.si-myhand.locked { border-color: var(--brass); opacity: .92; }
.si-myhand .si-target { font-weight: 400; opacity: .75; font-size: 12px; }
.si-myhand .si-done { color: var(--brass); font-size: 12px; }
/* "Set you're aiming for" coach under each Siamese hand (easy mode) — kept compact.
   A tap-to-pin ranked strip, then the group tracker for the active set. */
.si-tracker { margin-top: 7px; padding: 7px 8px; background: #d9a4410f; border: 1px solid #d9a44133; border-radius: 10px; }
.si-ranked { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
.si-rchip { font-size: 11px; padding: 3px 8px; border-radius: 999px; border: 1px solid #ffffff26;
  background: #00000033; color: var(--ivory); cursor: pointer; white-space: nowrap; }
.si-rchip b { color: var(--brass); font-weight: 800; margin-left: 2px; }
.si-rchip.on { border-color: var(--brass); background: #d9a4412e; }
.si-rchip.on b { color: var(--ivory); }
.si-tracker .tracker { gap: 4px; }
.si-tracker .gchip { font-size: 11px; padding: 3px 7px; }
.si-rack { min-height: 68px; }
.si-myhand.si-active { border-color: var(--brass); box-shadow: 0 0 0 1px #d9a44144 inset; }
.si-myhand.si-active .si-hand-head b::after { content: ' · your turn'; color: var(--brass); font-weight: 600; font-size: 11px; }
.si-rack .tile.si-pick { cursor: pointer; }
.si-rack .tile.si-drawn { box-shadow: 0 0 0 3px var(--brass), 0 0 10px 1px #d9a44188; }
.si-rack .tile.si-sel { outline: 3px solid var(--brass); outline-offset: 2px; transform: translateY(-6px); }
.si-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 6px; }
.si-prompt { font-size: 13px; opacity: .92; }
.si-drawnchip { display: inline-flex; }
.si-btn { padding: 8px 14px; border-radius: 8px; border: 1px solid #ffffff2a; background: #ffffff10; color: var(--ivory); font-weight: 700; cursor: pointer; font-size: 13px; }
.si-btn.primary { background: var(--brass); color: var(--ink); border-color: transparent; }
.si-btn:active { transform: translateY(1px); }

/* Tap a small tile (discard, exposed meld) to pop it big in the centre of the
   screen; own-hand tiles do it on a double-tap (single tap still selects). Tap the
   big tile or the backdrop to dismiss. */
.pile .tile.small, .exgroup .tile.small, .si-exposes .tile.small { cursor: zoom-in; }
.tilezoom-ov {
  position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.62); cursor: zoom-out; padding: 20px; animation: winFade .15s ease;
}
.tile.bigtile {
  /* ~25% of the screen: height ≈ 40vh gives area ≈ h²·(5/7) ≈ a quarter of the viewport. */
  width: auto; height: 40vh; aspect-ratio: 5 / 7; max-width: 70vw; padding: 6px;
  border-radius: 14px; box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6); cursor: zoom-out;
  animation: winPop .18s cubic-bezier(.2, 1.4, .4, 1);
}

/* Winner celebration overlay + fireworks. */
.win-celebrate {
  position: fixed; inset: 0; z-index: 120;
  background: rgba(8, 16, 12, 0.3); animation: winFade 0.3s ease;
}
/* Anchored at the discard felt's centre (its left/top are set in JS). */
.win-anchor { position: absolute; transform: translate(-50%, -50%); z-index: 2; text-align: center;
  display: flex; flex-direction: column; align-items: center;
  max-width: 96vw; max-height: 94vh; overflow-y: auto; overflow-x: hidden; }
.win-text { text-align: center; animation: winPop 0.5s cubic-bezier(.2, 1.4, .4, 1); }
.win-name {
  font-size: clamp(38px, 9vw, 84px); font-weight: 900; line-height: 1;
  color: #ffd447; letter-spacing: 1px;
  text-shadow: 0 3px 0 #a6701a, 0 0 26px rgba(255, 212, 71, 0.6);
}
.win-sub {
  margin-top: 6px; font-size: clamp(22px, 6vw, 46px); font-weight: 800; color: #fff;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.55);
}
/* Wall game: calmer, silvery notice instead of the gold winner splash. */
.win-celebrate.wall .win-name { color: #cfe0d8; text-shadow: 0 3px 0 #2a3a33, 0 0 22px rgba(180, 210, 200, 0.35); }
.win-celebrate.wall .win-sub { color: #9fb4ab; text-transform: uppercase; letter-spacing: 2px; font-size: clamp(16px, 4.5vw, 30px); }
/* Continue / End game choice, centered under the announcement. */
/* Winner reveal: the actual winning hand + the set from the card. */
.win-reveal { margin-top: 14px; background: rgba(10, 18, 13, 0.82); border: 1px solid #ffffff26;
  border-radius: 14px; padding: 12px; max-width: min(560px, 94vw); -webkit-backdrop-filter: blur(6px) saturate(1.1); backdrop-filter: blur(6px) saturate(1.1); }
.win-tiles { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.win-meld { display: flex; gap: 2px; }
.win-meld.exposed { padding: 3px; background: #d9a4411f; border: 1px solid #d9a44140; border-radius: 7px; }
/* The winning set stays on ONE line so it lines up under the tiles for compare. */
.win-cardrow { margin-top: 10px; display: flex; align-items: center; gap: 10px; justify-content: flex-start;
  flex-wrap: nowrap; overflow-x: auto; background: #fdf6e6; color: #2b2b2b; border-radius: 8px; padding: 7px 12px; }
.win-cardrow .win-cat { font-weight: 800; color: #7a1f2b; font-size: 13px; white-space: nowrap; }
.win-cardrow .cv-pat { font-weight: 800; letter-spacing: 1px; white-space: nowrap; }
.win-cardrow .win-pts { margin-left: auto; padding-left: 10px; font-weight: 800; color: #2f6b3a; white-space: nowrap; }
.win-actions { margin-top: 18px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.win-btn {
  font: inherit; font-weight: 800; font-size: clamp(15px, 3.6vw, 19px); cursor: pointer;
  padding: 11px 22px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.28);
  background: rgba(20, 30, 24, 0.72); color: #eef4ef; -webkit-backdrop-filter: blur(6px) saturate(1.1); backdrop-filter: blur(6px) saturate(1.1);
}
.win-btn.primary { background: var(--brass, #ffd447); color: #201a06; border-color: transparent;
  box-shadow: 0 4px 0 #a6701a, 0 6px 18px rgba(255, 212, 71, 0.4); }
.win-btn:active { transform: translateY(1px); }
.fw-layer { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.fw {
  position: absolute; width: 9px; height: 9px; border-radius: 50%; opacity: 0;
  transform: translate(-50%, -50%); animation: fwBurst 1.1s ease-out forwards;
}
@keyframes fwBurst {
  0% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(0.4); }
}
@keyframes winPop { 0% { transform: scale(0.4); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
@keyframes winFade { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .win-text { animation: none; }
  .fw { display: none; }
}

/* ---------- Landscape phone: board on the left, the card on the right ----------
   Portrait stacks everything tall. Sideways we have width to spare, so split the
   screen: the play area (opponents, discards, your hand, coach) scrolls on the
   left; the card sits on the right. The oversized hand/discard strips are trimmed
   so you can see far more at a glance. */
@media (orientation: landscape) and (max-height: 560px) {
  /* Left = the board + your hand. Right = the "You're closest to…" coach with the
     focused card directly below it (the coach is moved into .side by render()). */
  #board-root.table { display: grid; grid-template-columns: minmax(0, 1fr) clamp(200px, 36vw, 320px);
    gap: 8px; align-items: start; }
  .board { min-width: 0; max-height: calc(100vh - 56px); overflow-y: auto; -webkit-overflow-scrolling: touch; }
  .board { min-width: 0; max-height: calc(100dvh - 56px); overflow-y: auto; -webkit-overflow-scrolling: touch; }
  .side { display: flex; flex-direction: column; gap: 8px; min-width: 0;
    max-height: calc(100vh - 56px);          /* iOS 15.1-15.3 has no dvh */
    max-height: calc(100dvh - 56px);
    overflow-y: auto; -webkit-overflow-scrolling: touch; }
  .side .panel:not(.cardpanel):not(.cluepanel) { display: none; }   /* drop the table log */
  .side .cluepanel { margin: 0; padding: 8px 9px; order: -1; }
  .side .cluepick select { font-size: 11px; }

  /* The coach (now on the right) — compact. */
  .side .coach { margin: 0; padding: 8px 9px; order: -1; }
  .side .coach-title { margin-bottom: 5px; font-size: 12px; }
  .side .targets { gap: 5px; }
  .side .targets .tchip { font-size: 10.5px; padding: 4px 7px; }
  .side .tracker { gap: 4px; }
  .side .tracker .gchip { font-size: 10px; padding: 2px 5px; }

  /* Below it, only the card section you're aiming at — smaller. */
  .side .cardpanel { margin: 0; padding: 8px; }
  .side .cardpanel h3 { display: none; }
  .side .cardview.compact { font-size: 10.5px; }
  /* Only ONE section fits beside the board, so the others are dropped - but only
     when there is a focused section to drop them FOR. Without .has-focus this
     hid every section on any table with no target, which is every classic game
     and every daily: the card panel collapsed to an empty 18px heading and the
     owner's tester reported the house card as simply missing.
     Written as a class rather than :has(), which iOS 15.1 does not have. */
  .side .cardview.compact.has-focus .cv-section:not(.cv-focus) { display: none; }
  .side .cv-head { font-size: 11px; padding: 3px 6px; }

  /* Left column: opponents, the discards, then your hand beneath them - the
     table reads top-down like the real one (owner, 9/10: "your hand should
     be beneath the discard pile"). */
  /* The three opponents keep their own orders (left 1, top 2, right 3) and
     share one row; the felt and the rack follow, each on its own row. */
  .felt { order: 4; }
  .myarea.you { order: 5; }
  .myarea { padding: 8px; }
  .myarea .rack { min-height: 0; gap: 4px; }
  .myarea .rack .tile { width: 32px; height: 45px; }
  .felt { min-height: 70px; }
  .pile { max-height: 74px; min-height: 74px; }
  .pile .tile.small { width: 20px; height: 28px; }
  .oseat .tileback { width: 9px; height: 13px; }
}

/* Brief "X called Y" flash so a turn jumping to a caller isn't confusing. */
.table-toast {
  position: fixed; left: 50%; top: 16%; transform: translate(-50%, -8px);
  z-index: 150; background: rgba(20, 32, 24, 0.94); color: var(--ivory);
  border: 1px solid var(--brass); border-radius: 999px; padding: 8px 18px;
  font-weight: 700; font-size: 14px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  opacity: 0; pointer-events: none; transition: opacity .18s ease, transform .18s ease; white-space: nowrap;
}
.table-toast.show { opacity: 1; transform: translate(-50%, 0); }

/* Tower picker: choose the 4-player climb or the Solitaire climb. */
.tower-pick { max-width: 520px; }
.tower-pick-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 12px 0; }
.tower-pick-card {
  display: flex; flex-direction: column; gap: 5px; text-align: left;
  padding: 13px 14px; border-radius: 11px; cursor: pointer;
  background: #ffffff0e; border: 1px solid #ffffff22; color: var(--ivory);
}
.tower-pick-card:hover { border-color: var(--brass); background: #d9a44120; }
.tower-pick-card .tp-title { font-size: 15px; font-weight: 700; color: var(--brass); }
.tower-pick-card .tp-sub { font-size: 11.5px; opacity: .78; line-height: 1.4; }
@media (max-width: 460px) { .tower-pick-row { grid-template-columns: 1fr; } }

/* TOWER branding on the felt while you're climbing. Wider tracking and the engraved
   title face, so it reads as a stamp on the table rather than as body text. */
.felt-kanji.felt-tower {
  font-family: 'Parlor', Copperplate, 'Times New Roman', serif;
  font-weight: 600; letter-spacing: .22em; text-indent: .22em;
  font-size: clamp(26px, 9vw, 66px);
}
/* Solitaire's table carries the same stamp during a climb, replacing the 龍. It sits
   BELOW the board rather than behind it — the board always occupies the middle, so a
   centred stamp is completely hidden (the 龍 had exactly this problem). In the open
   space underneath it can also be a little stronger and still not compete. */
.table.solo-mode.tower-climb::before {
  content: 'TOWER';
  font-family: 'Parlor', Copperplate, 'Times New Roman', serif;
  font-weight: 600; letter-spacing: .22em; text-indent: .22em;
  font-size: min(9vh, 13vw); line-height: 1;
  inset: auto 0 5% 0;            /* pinned near the bottom, full width */
  text-align: center;
  opacity: .13;
}

/* ── Achievements ───────────────────────────────────────────────────────────
   The wall in the Stats sheet, plus the unlock toast. Locked badges stay
   readable (dimmed, not hidden): a grid of grey blanks tells you nothing about
   what there is to chase. */
.ach-head { display: flex; justify-content: space-between; align-items: baseline;
  padding: 16px 4px 6px; border-top: 1px dashed #ffffff22; margin-top: 10px; }
.ach-group { font-size: 12px; letter-spacing: .12em; text-transform: uppercase;
  opacity: .62; padding: 12px 4px 6px; }
.ach-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 8px; }
.ach { display: flex; gap: 9px; align-items: flex-start; padding: 9px 10px;
  border: 1px solid #ffffff14; border-radius: 9px; background: #ffffff08; }
.ach.won { border-color: rgba(255, 212, 71, .45); background: rgba(255, 212, 71, .09); }
.ach-ico { font-size: 24px; line-height: 1.1; filter: grayscale(1) opacity(.4); }
.ach.won .ach-ico { filter: none; text-shadow: 0 0 10px rgba(255, 212, 71, .45); }
.ach-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ach-name { font-size: 13px; font-weight: 800; }
.ach.won .ach-name { color: var(--brass); }
.ach-desc { font-size: 11px; opacity: .72; line-height: 1.35; }
.ach-prog { font-size: 10px; font-weight: 800; opacity: .6; letter-spacing: .04em; }
.ach-bar { display: block; height: 3px; border-radius: 2px; background: #ffffff1a; margin: 4px 0 2px; }
.ach-bar > i { display: block; height: 100%; border-radius: 2px; background: var(--brass); }

#ach-toasts { position: fixed; left: 50%; transform: translateX(-50%); bottom: 5%;
  z-index: 9000; display: flex; flex-direction: column; gap: 8px; pointer-events: none; }
.ach-toast { display: flex; gap: 11px; align-items: center; padding: 11px 16px;
  border-radius: 12px; border: 1px solid rgba(255, 212, 71, .55);
  background: rgba(24, 20, 16, .96); box-shadow: 0 10px 30px #000a;
  animation: achIn .35s ease-out both; max-width: 88vw; }
.ach-toast.out { animation: achOut .55s ease-in both; }
.ach-toast-ico { font-size: 30px; line-height: 1; }
.ach-toast-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ach-toast-tag { font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--brass); font-weight: 800; }
.ach-toast-name { font-size: 15px; font-weight: 800; }
.ach-toast-desc { font-size: 11px; opacity: .74; }
@keyframes achIn  { from { opacity: 0; transform: translateY(14px) scale(.96); } }
@keyframes achOut { to   { opacity: 0; transform: translateY(-8px); } }
/* The toast is celebratory, not informational — hold it still if motion is unwelcome. */
@media (prefers-reduced-motion: reduce) {
  .ach-toast, .ach-toast.out { animation: none; }
}

/* ── Confirm dialog ─────────────────────────────────────────────────────────
   Replaces window.confirm() for destructive actions. Native dialogs are
   unstyled on the web and an iOS WebView may suppress them entirely, which
   would let a wipe happen with no warning at all. */
.confirm-overlay { position: fixed; inset: 0; z-index: 9500; display: grid; place-items: center;
  background: #000000a6; -webkit-backdrop-filter: blur(10px) saturate(1.2); backdrop-filter: blur(10px) saturate(1.2); padding: 20px; }
.confirm-box { max-width: 380px; width: 100%; padding: 20px 22px; border-radius: 14px;
  max-height: calc(100vh - 40px); max-height: calc(100dvh - 40px); overflow-y: auto;
  border: 1px solid #ffffff22; background: var(--tolex, #1b1512);
  box-shadow: 0 20px 60px #000c; }
.confirm-box h3 { margin: 0 0 8px; font-size: 18px; color: var(--brass); }
.confirm-box p { margin: 0 0 10px; font-size: 13px; line-height: 1.5; opacity: .84; }
.confirm-box .confirm-safe { color: #8fd6a4; opacity: .95; }
.confirm-row { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; flex-wrap: wrap; }   /* three buttons on a phone wrap, never overflow */
.confirm-row button { padding: 9px 18px; border-radius: 9px; font-size: 14px; font-weight: 700;
  cursor: pointer; border: 1px solid #ffffff2a; background: #ffffff12; color: inherit; }
.confirm-no:hover { background: #ffffff1e; }
/* THE PARLOR PASS SHEET (pass.js) - the same box as every confirm, with the
   product card Apple wants to see: name, price, period, what it unlocks. */
.pass-sheet .confirm-box { max-width: 420px; }
/* THE PAIRING SHEET SCROLLS. Owner, 9/8: "Play on multiple devices isn't
   scrolling down so I can go back." It grew tall (sign in, then a pairing code,
   then a recovery code) and a plain confirm-box is centered with no scroll, so
   its bottom fell off a short screen with no way to reach the close. Bound its
   height to the viewport and let the box itself scroll. */
.pair-panel .confirm-box { max-height: 88vh; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch; }
.pass-sheet .pass-lede { opacity: .9; }
.pass-card { border: 1px solid var(--brass); border-radius: 12px; padding: 12px 14px; margin: 10px 0; background: #d9a4410f; }
.pass-card .pass-name { font-size: 11px; letter-spacing: .12em; color: var(--brass); font-weight: 700; }
.pass-card .pass-prices { margin-top: 4px; font-size: 14px; }
.pass-card .pass-price { font-size: 22px; color: var(--brass); font-weight: 700; }
.pass-card .pass-or { opacity: .6; margin: 0 4px; }
.pass-card .pass-perks { margin: 8px 0 0 18px; padding: 0; font-size: 13px; line-height: 1.5; opacity: .9; }
.pass-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.pass-actions button { width: 100%; padding: 11px 14px; border-radius: 10px; font-size: 15px; }
.pass-status { font-size: 13px; color: #e9bd5a; margin: 8px 0 0; }
.pass-legal { font-size: 11.5px; opacity: .7; line-height: 1.45; margin: 10px 0 6px; }
.pass-links { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin: 4px 0 10px; }
.pass-links .linkish { background: none; border: none; color: var(--brass); text-decoration: underline; font-size: 12.5px; padding: 4px 2px; cursor: pointer; }
.pass-sheet .pass-later { width: 100%; margin-top: 4px; }
.pass-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin: 10px 0; padding: 10px 12px; border: 1px solid var(--brass); border-radius: 12px; background: #d9a4411a; font-size: 13.5px; }
.pass-bar .pb-text { flex: 1 1 200px; }
.pass-bar button.primary { padding: 7px 12px; border-radius: 9px; font-size: 13px; }
.pass-bar .linkish { background: none; border: none; color: var(--brass); text-decoration: underline; font-size: 12.5px; cursor: pointer; }
.pass-pill { display: inline-block; margin-left: 8px; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 700; color: #1d1d1d; background: var(--brass); vertical-align: middle; }
.pass-toast { position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%); z-index: 9600; padding: 10px 16px; border-radius: 12px; background: var(--t-1b3b2c); color: #f4efe4; border: 1px solid var(--brass); font-size: 14px; box-shadow: 0 8px 24px #0006; transition: opacity .4s; }
.pass-toast.gone { opacity: 0; }
.confirm-yes { border-color: #c0503a; background: #a33c28; color: #fff; }
.confirm-yes:hover { background: #b8452e; }

/* ── Solitaire in landscape ─────────────────────────────────────────────────
   The 4-player landscape rule is `#board-root.table { display: grid; ... }` with
   a second column for the coach. An ID selector outranks `.table.solo-mode`, so
   Solitaire inherited that grid too and got boxed into ~60% of the width with an
   empty coach column beside it. Solitaire has no side panel, so it takes the whole
   width and centres the board in whatever is left after the toolbar. */
@media (orientation: landscape) and (max-height: 620px) {
  #board-root.table.solo-mode {
    display: flex; flex-direction: column;
    grid-template-columns: none;
    padding: 8px 10px 10px;
    min-height: calc(100vh - 72px);
    min-height: calc(100dvh - 72px);
  }
  #board-root.table.solo-mode .sol-bar { margin-bottom: 4px; }
  #board-root.table.solo-mode .sol-info { font-size: 12.5px; }
  /* auto on all four sides centres the scaled board in the leftover space */
  #board-root.table.solo-mode .sol-scalewrap { margin: auto; }
  /* The ornament competes with a board that now fills the screen. */
  #board-root.table.solo-mode::before { font-size: min(46vh, 30vw); opacity: .04; }
}

/* Sideways, the notch is on the SIDE, not the top — so the 46px the portrait
   rule reserves above the header is dead space, and on a 390px-tall screen that
   is a tenth of everything. Fall back to the real inset (usually 0 here) with a
   small floor. Worth ~40px, which every mode gets back as board space. */
@media (orientation: landscape) and (max-height: 620px) {
  .topbar {
    padding-top: max(6px, env(safe-area-inset-top));
    padding-bottom: 5px;
  }
  .topbar h1 { font-size: 15px; }
}

/* Solitaire tiles respond on pointerdown (see solitaire.js). These two lines are
   the other half of that: `manipulation` drops the browser's wait-and-see delay
   for a possible double-tap zoom, and no text selection means a firm press does
   not start highlighting the tile instead of playing it. */
.stile { touch-action: manipulation; user-select: none; -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent; }
/* Every press is acknowledged instantly, including a blocked tile — silence on a
   tap is indistinguishable from a tap that was missed. */
/* Press feedback WITHOUT moving or shrinking the tile. Solitaire tiles are stacked
   and absolutely positioned, so any transform that makes one smaller or nudges it
   uncovers the tile underneath - and on iOS :active sticks after a tap, so it stayed
   uncovered. Brightness alone gives the press its feel and never exposes the stack. */
.stile.free:active { filter: brightness(1.1); }
.stile.blocked:active { filter: brightness(0.7) saturate(0.85); }
@media (prefers-reduced-motion: reduce) { .stile.free:active { filter: none; } }

/* ── Every end-of-game message, centre screen ───────────────────────────────
   The result of a hand or a board is the one thing you must not have to go
   looking for, so it is centred rather than left in the flow (Solitaire's used to
   sit under the board, off screen if you were looking at the tiles).
   pointer-events are off on the backdrop and back on for the card itself: the
   dimming reads as a modal, but the toolbar behind stays usable and you can still
   scroll the table to look at the final position instead of being locked out.
   :not(:empty) so an empty host never covers the screen or traps clicks.
   No entrance animation on purpose — draw() rebuilds this host on every frame
   while the hand is over, so a keyframe would restart continuously and flicker. */
#banner-host:not(:empty),
.end-overlay {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center;
  padding: 16px;
  background: #00000075;
  pointer-events: none;
}
#banner-host:not(:empty) .banner,
.end-overlay .banner {
  pointer-events: auto;
  margin: 0; width: min(440px, 100%);
  max-height: calc(100vh - 32px); overflow-y: auto;
  max-height: calc(100dvh - 32px); overflow-y: auto;
  box-shadow: 0 24px 70px #000a, 0 0 0 1px #00000040;
}
/* Solitaire's own banner lives inside the board, not here, and is unaffected. */

/* ── Siamese in landscape ───────────────────────────────────────────────────
   Stacked, Siamese ran to ~2200px on a phone held sideways: opponent, felt, both
   your racks, then a 1200px card view — all in the left 500px of an 844px screen,
   with the 4-player coach column sitting empty beside it. Play on the left with
   its own scroll, card on the right, same shape as the 4-player landscape view. */
@media (orientation: landscape) and (max-height: 620px) {
  #board-root.table.siamese-mode { display: block; grid-template-columns: none; padding: 8px 10px 10px; }
  .siamese-mode .si-wrap {
    display: grid; grid-template-columns: minmax(0, 1fr) clamp(190px, 33vw, 310px);
    gap: 8px; align-items: start;
  }
  .siamese-mode .si-play, .siamese-mode .si-cardview {
    min-width: 0;
    max-height: calc(100vh - 62px);          /* iOS 15.1-15.3 has no dvh */
    max-height: calc(100dvh - 62px);
    overflow-y: auto; -webkit-overflow-scrolling: touch;
  }
  .siamese-mode .si-cardview { margin: 0; }
  /* The opponent's two face-down racks are pure decoration — shrink, don't cut. */
  .siamese-mode .si-opp { padding: 6px 8px; }
  .siamese-mode .si-opp-name { font-size: 12px; }
}

/* ── Achievement unlocked ───────────────────────────────────────────────────
   Centre screen and it waits for a tap. z-index above the end-of-hand prompt,
   because finishing a hand is exactly when one of these usually lands. */
.ach-overlay {
  position: fixed; inset: 0; z-index: 9000;
  display: grid; place-items: center; padding: 20px;
  background: #000000ab; cursor: pointer; outline: none;
}
.ach-card {
  width: min(340px, 100%); padding: 22px 22px 18px; border-radius: 16px; text-align: center;
  background: linear-gradient(180deg, #241c16, #17110d);
  border: 1px solid var(--brass); box-shadow: 0 24px 70px #000c, inset 0 1px 0 #ffffff1a;
}
.ach-card-tag { font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--brass); opacity: .9; }
.ach-card-go { margin-top: 16px; font-size: 12px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--brass); opacity: .85; }
/* The popup shows the real Stats badge, so it only needs to give it room to
   breathe — the badge brings its own icon, name, description and progress. */
.ach-card .ach { width: 100%; cursor: default; }
.ach-card .ach-ico { font-size: 34px; }
.ach-card .ach-name { font-size: 16px; }

/* Resume sits above the two fresh-start cards and is the emphasised choice —
   the run you already have going is almost always what you came back for. */
.tower-resume { display: block; width: 100%; margin: 4px 0 2px; text-align: left;
  border-color: var(--brass); background: #ffffff14; }
.tower-resume .tp-title { color: var(--brass); }
.tower-pick-warn { margin: 6px 0 10px; font-size: 12px; opacity: .7; }


/* ── Daily puzzle card ──────────────────────────────────────────────────────
   Sits above the mode cards and leads with the streak: the number is the reason
   to come back, so it gets the size and the colour. */
.daily-card {
  display: block; width: 100%; text-align: left; cursor: pointer;
  margin: 0 0 14px; padding: 12px 14px 13px; border-radius: 14px;
  border: 1px solid var(--brass); background: linear-gradient(180deg, #ffffff14, #ffffff08);
  color: inherit;
}
.daily-card:hover { background: linear-gradient(180deg, #ffffff1e, #ffffff0d); }
.daily-card.done { border-color: #4f9d6a; }
.daily-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.daily-tag { font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--brass); }
.daily-streak { font-size: 17px; font-weight: 800; opacity: .5; }
.daily-streak.hot { opacity: 1; color: #ffb454; }
.daily-body h2 { margin: 4px 0 2px; font-size: 20px; }
.daily-body p { margin: 0; font-size: 12.5px; opacity: .8; line-height: 1.45; }
/* Fourteen days at a glance — filled = solved, hollow = tried, faint = missed. */
.daily-strip { display: flex; gap: 4px; margin: 9px 0 6px; }
.dday { width: 12px; height: 12px; border-radius: 3px; background: #ffffff14; border: 1px solid #ffffff1f; }
.dday.tried { background: #ffffff26; border-color: #ffffff3d; }
.dday.solved { background: var(--brass); border-color: var(--brass); }
.daily-goal { font-size: 12px; font-weight: 700; color: var(--brass); opacity: .95; }

/* ── Solitaire clock ────────────────────────────────────────────────────────
   Reads as a small brass-bezelled counter set into the rail: recessed well,
   lit top edge, tabular figures so the digits don't jitter as they tick. */
.sol-clock {
  display: inline-block; min-width: 4.2em; margin-left: 2px; padding: 3px 9px;
  border-radius: 7px; text-align: center;
  font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1;
  font-weight: 800; font-size: 13px; letter-spacing: .04em;
  color: #ffd9a0;
  background: linear-gradient(180deg, #00000059, #00000026);
  border: 1px solid #ffffff1f;
  box-shadow: inset 0 2px 5px #00000073, inset 0 -1px 0 #ffffff14, 0 1px 0 #ffffff0d;
  text-shadow: 0 1px 2px #000000a6, 0 0 10px #ffb45426;
}
/* Frozen on a solved board — turns brass so the finishing time reads as a result. */
.sol-info .sol-clock.final { color: var(--ink); background: linear-gradient(180deg, #f0c274, var(--brass));
  border-color: #ffe2ac; text-shadow: none; box-shadow: 0 2px 8px #00000059, inset 0 1px 0 #fff6; }
.times-box { margin-top: 14px; }
.times-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 6px; margin-top: 7px; }
.time-cell { display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  padding: 6px 9px; border-radius: 8px; background: #ffffff0f; border: 1px solid #ffffff17; }
.tc-shape { font-size: 12px; opacity: .85; }
.tc-time { font-variant-numeric: tabular-nums; font-weight: 800; color: var(--brass); font-size: 13px; }

/* Name is required before play, and it has to be clean. */
.name-required { margin: 6px 0 0; font-size: 12.5px; font-weight: 700; color: #ffb0a0; }
.title-name.missing input { border-color: #c0503a; box-shadow: 0 0 0 2px #c0503a44; }

/* On a phone the header already says which mode you are in, so the toolbar does
   not need to repeat it — dropping the prefix keeps the line (and the clock) on
   one row instead of wrapping to two. */
@media (max-width: 470px) { .sol-title { display: none; } }
/* Solitaire's banner is a plain text card — give it a little more presence now
   that it is centre-screen rather than tucked under the board. */
.end-overlay .banner { font-weight: 800; font-size: 16px; text-align: center; cursor: pointer; }
/* `.table.solo-mode > *` sets position:relative on every direct child (to sit above
   the felt ornament) and that outranks the overlay's own `fixed`, leaving the
   end-of-board message parked under the board. Re-assert it with an id. */
#board-root > .end-overlay { position: fixed; inset: 0; z-index: 60; }

/* End-of-hand nudges: how close you came, and the next badge in reach. */
.bnear { margin-top: 10px; padding-top: 8px; border-top: 1px solid #00000026;
  font-size: 13px; font-weight: 700; }
.bnext { display: flex; align-items: center; justify-content: center; gap: 8px;
  margin-top: 8px; font-size: 12.5px; opacity: .9; }
.bnext-ico { font-size: 18px; }

/* The name field locks once set — it is the identity your stats (and later other
   players) see, so it should not drift or be edited by accident. */
.name-lock { font-size: 12px; padding: 6px 10px; white-space: nowrap; }
/* The Save state of the same button: the one explicit step that stores the
   name, so it reads as the primary action while the field is open. */
.name-lock.name-save { background: var(--brass); color: #1d1408; border-color: var(--brass); font-weight: 800; }
.title-name.locked input { opacity: .75; cursor: default; border-style: dashed; }

/* Your name is the one piece of the table that is yours, so it is set in the
   masthead face rather than plain UI type — engraved caps on a brass-ruled plate,
   like the name card at a real parlour seat. Cinzel is a caps design, so it is
   uppercased; the placeholder stays in the normal face so it reads as instruction
   rather than as a name. */
.title-name label { display: flex; align-items: center; gap: 10px; }
.title-name input {
  font-family: 'Parlor', Copperplate, 'Times New Roman', serif;
  font-weight: 600; font-size: 19px; letter-spacing: .1em; text-transform: uppercase;
  text-align: center; color: var(--brass);
  padding: 9px 14px; min-width: 190px; border-radius: 10px;
  background: linear-gradient(180deg, #00000040, #00000018);
  border: 1px solid var(--brass);
  box-shadow: inset 0 2px 6px #00000059, inset 0 -1px 0 #ffffff14, 0 1px 0 #ffffff0d;
  text-shadow: 0 1px 2px #000000a6;
}
.title-name input::placeholder {
  font-family: var(--font); font-size: 13px; letter-spacing: .02em;
  text-transform: none; color: var(--ivory); opacity: .45; font-weight: 400;
}
.title-name input:focus { outline: none; border-color: #ffe2ac; box-shadow: inset 0 2px 6px #00000059, 0 0 0 3px #ffb45433; }
/* The name row is a flex line; without this the lock button gets squeezed to a
   blank sliver next to the wide name plate. */
.name-lock { flex: 0 0 auto; min-width: 92px; }
/* Live character count for the name — visible only while the field is editable. */
.name-count { flex: 0 0 auto; font-size: 11px; font-variant-numeric: tabular-nums;
  opacity: .6; min-width: 38px; text-align: right; }
.name-count.full { color: #ffb0a0; opacity: 1; font-weight: 700; }
/* The two end-of-hand nudges inside the celebration overlay (light-on-dark there,
   unlike the brass summary card). */
.win-anchor .bnear { color: var(--ivory); border-top-color: #ffffff26; opacity: .95; }
.win-anchor .bnext { color: var(--ivory); }

/* Big melds shrink a little so a quint does not overrun the seat it belongs to.
   Kongs get a light trim, quints a bit more; below ~26px the faces stop reading. */
.exgroup.big4 .tile.small { width: 27px; height: 38px; }
.exgroup.big5 .tile.small { width: 24px; height: 34px; padding: 1px; border-radius: 5px; }
.exgroup.big5 { gap: 1px; padding: 2px; }

/* An opponent seat is only about 90-125px wide, and in portrait all three sit
   across the top so every meld lays out HORIZONTALLY (see the .oseat override
   above). A quint at 5x24px plus gaps measures 128px, so it hung 8px past the seat
   border on a 430px phone and worse on anything narrower. The fixed sizes above are
   now a MAXIMUM: each meld is capped at its column and may flex down to fit, with
   aspect-ratio keeping tiles in proportion rather than letting them go skinny.
   Capping the group alone does nothing - .exposures is itself a flex item that
   overflows .obody, so 100% resolved against an already-too-wide parent. Every link
   in the chain needs min-width:0 before a flex item is allowed to shrink. */
.oseat .obody, .oseat .exposures { min-width: 0; max-width: 100%; }
.oseat .exgroup { min-width: 0; max-width: 100%; box-sizing: border-box; align-items: flex-start; }
.oseat .exgroup .tile.small { min-width: 0; height: auto; aspect-ratio: 5 / 7; }
/* ...and a seat holding a kong or quint gives that meld a full-width row of its own
   beneath the hand backs, rather than the ~80px left over beside them, so it shrinks
   as little as possible. */
.oseat .exposures.wide { flex-basis: 100%; }

/* A joker the CURRENT hand cannot legally spend — every joker, when the hand you
   are following is made only of pairs and singles, because a joker may never stand
   in for either. It must not wear the blue "counts toward this hand" ring (it does
   not) and it must not wear the grey "safe to throw" wash (it never is — a joker is
   still your best tile for switching hands or redeeming). So: its own muted amber
   dashed ring, meaning "held, but idle for this hand". */
.rack .tile.joker-idle {
  box-shadow: 0 0 0 2px var(--brass), 0 0 8px 1px #d9a44155;
  opacity: .82;
}

/* The rest of the hover styling, gated the same way. On a touchscreen every one of
   these sticks to whatever you last tapped and stays there through re-renders, which
   is how a chip, a card or a menu row ends up looking selected when it isn't. Under
   a coarse pointer they simply return to their resting appearance. */
@media (hover: none), (pointer: coarse) {
  .menu-drop button:hover { background: transparent; border-color: transparent; }
  button.swap:hover { background: #7a4fd0; }
  .emblem-pick:hover { border-color: #ffffff20; background: #ffffff0e; }
  .tchip:hover { border-color: #ffffff1a; }
  .sortbtn:hover { border-color: #ffffff2b; color: var(--ivory); }
  .cv-row.cv-pick:hover { background: transparent; }
  .mode-card:hover { transform: none; border-color: inherit; box-shadow: none; }
  .mode-card.tower-card:hover { border-color: inherit; }
  .title-links button:hover { border-color: inherit; background: transparent; }
  .title-links .title-tut:hover { background: var(--brass); }
  .dealskip:hover { border-color: inherit; color: inherit; }
  .stat-reset:hover { border-color: inherit; }
  .coach-bubble .cb-end:hover { border-color: inherit; }
  .stile.free:hover { background: inherit; transform: none; }
  .rack .tile.surplus:hover { opacity: .62; filter: saturate(.55); }
}

/* Forces the wrapping rack onto a new line, so a mini-set is never cut in half
   across two rows (see keepSetsWhole in ui.js). Zero height: it only breaks. */
.rack .rack-break { flex-basis: 100%; height: 0; margin: 0; padding: 0; border: 0; }

/* The winning hand's points sat flush against the last group of the pattern —
   "666630" — because the row can overflow horizontally, and once it does the
   margin-left:auto that normally pushes the points right has no free space left to
   distribute. A real gap and a hairline divider keep them apart either way. */
.win-cardrow .win-pts { padding-left: 14px; border-left: 1px solid #00000022; margin-left: auto; }
.win-cardrow .cv-pat { padding-right: 4px; }

/* --- End of a full round: final standings ---------------------------------- */
/* Fixed to the SCREEN, not the table — same lesson as the win card: anchoring an
   announcement to the board leaves it off-screen the moment the page is scrolled. */
.match-over {
  position: fixed; inset: 0; z-index: 200; display: grid; place-items: center; padding: 18px;
  background: rgba(6, 14, 10, 0.78); -webkit-backdrop-filter: blur(10px) saturate(1.2); backdrop-filter: blur(10px) saturate(1.2);
  animation: moFade .28s ease;
}
@keyframes moFade { from { opacity: 0; } to { opacity: 1; } }
.mo-card {
  width: min(460px, 100%); max-height: 92vh; max-height: 92dvh; overflow-y: auto;
  text-align: center; padding: 22px 20px 18px; border-radius: 18px;
  background: linear-gradient(180deg, var(--t-17301f), var(--t-102217));
  border: 1px solid #ffffff1f; box-shadow: 0 26px 70px #000b;
}
.mo-kicker { font-size: 12px; letter-spacing: 2px; text-transform: uppercase; color: var(--brass); opacity: .85; }
/* The cup gets a slow celebratory swing — the one bit of motion on the screen, so
   the eye lands on the winner first and reads downward from there. */
.mo-cup { font-size: 60px; line-height: 1; margin: 6px 0 2px; animation: moCup 1.6s ease-in-out infinite; transform-origin: 50% 80%; }
@keyframes moCup {
  0%, 100% { transform: rotate(-7deg) translateY(0); }
  50%      { transform: rotate(7deg) translateY(-5px); }
}
.mo-winner {
  font-size: clamp(30px, 8vw, 52px); font-weight: 900; line-height: 1.05; color: #ffd447;
  text-shadow: 0 3px 0 #a6701a, 0 0 26px rgba(255, 212, 71, .55);
  animation: winPop .5s cubic-bezier(.2, 1.4, .4, 1);
}
.mo-sub { font-size: clamp(14px, 4vw, 20px); color: var(--ivory); opacity: .9; margin-bottom: 14px; }
.mo-list { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 16px; }
/* Each place slides in just after the one above it, so the table reads top-down. */
.mo-row {
  display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: 10px;
  background: #ffffff0d; border: 1px solid #ffffff14; font-size: 15px;
  animation: moRow .42s cubic-bezier(.2, .9, .3, 1) both;
  animation-delay: calc(var(--i) * .09s + .15s);
}
@keyframes moRow { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.mo-row.first { background: #d9a4411f; border-color: #d9a44166; }
.mo-row.you .mo-name { color: var(--brass); }
.mo-place { font-size: 19px; width: 26px; text-align: center; flex: none; }
.mo-name { font-weight: 700; text-align: left; flex: 1 1 auto; color: var(--ivory); }
.mo-score { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--ivory); }
.mo-score.pos { color: #7fe0a2; }
.mo-score.neg { color: #ff9a9a; }
.mo-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
@media (prefers-reduced-motion: reduce) {
  .mo-cup, .mo-winner, .mo-row { animation: none; }
}

/* Whose deal it is. East moves one seat every hand now, so this cannot be left
   implicit in the wind letter — you would have to know that East deals. The seat
   plate says it outright and carries a brass edge so it reads at a glance. */
.onm .odealer {
  font-size: 9px; font-weight: 800; letter-spacing: 1px; line-height: 1;
  padding: 3px 5px; border-radius: 4px; white-space: nowrap;
  background: #d9a44126; color: var(--brass); border: 1px solid #d9a44166;
}
.oseat.dealer { border-color: #d9a44177; }

/* Undo the last call — only rendered inside the window where it is safe. Styled
   apart from the primary action so it never competes with Discard. */
.undo-call {
  background: #00000038; color: var(--ivory); border: 1px solid #ffffff30;
  font-weight: 600;
}
.undo-call:hover { border-color: var(--brass); color: var(--brass); }
@media (hover: none), (pointer: coarse) {
  .undo-call:hover { border-color: #ffffff30; color: var(--ivory); }
}

/* A tile with another resting ON it hides its face. The board draws flat tiles
   offset a few pixels per layer, so the strip left showing was a readable peek at
   the face underneath — on a real set that strip is the tile's EDGE and tells you
   nothing. The tile body still draws, so the stack keeps its depth; only the
   printed face is withheld until whatever sits on top is matched away. */
.stile.covered .face { visibility: hidden; }

/* =============================== POLISH LAYER ===============================
   The juice pass: press feedback, lighting, entrances, smoothness. Pure
   presentation - nothing here changes what anything does, only how it feels.
   Lives at the end of the sheet so it wins ties without !important. */

/* Press feedback: everything tappable gives a little under the finger, the
   detail that separates "web page" from "game". The fast transition on press
   and slower one on release is how native buttons feel. */
button { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
button:not(:disabled) { transition: transform .16s ease, box-shadow .16s ease, background .16s ease, border-color .16s ease, opacity .16s ease; }
button:not(:disabled):active { transform: scale(.96); transition-duration: .05s; }
.mode-card:not(:disabled):active { transform: translateY(-1px) scale(.985); }

/* Momentum scrolling on every scroll window - without it iOS drags feel like
   pulling paper against felt. */
.mo-card, .menu-drop, .learn-body { -webkit-overflow-scrolling: touch; }

/* Dragging: the lifted tile rides the GPU and never fights the scroller. */
.rack .tile.dragging, .tile.si-ghost { will-change: transform; }

/* Modal entrances: veils fade, cards settle into place with a small overshoot.
   Landing beats appearing. */
/* Transform-only on purpose: an animation frozen at its first frame (hidden
   tab, throttled WebView) must still show the dialog. An opacity entrance
   froze one at 0 - an invisible modal that swallowed every tap. */
@keyframes cardIn { from { transform: translateY(12px) scale(.97); } to { transform: none; } }
.name-modal, .confirm-box { animation: cardIn .24s cubic-bezier(.18, .9, .32, 1.12); }

/* Title lighting: a warm lamp pool from above and a soft vignette below - the
   "lamp over the card table" look. Sits under the panel, over the felt. */
.title-screen::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(60% 42% at 50% -6%, rgba(240, 196, 104, 0.14), transparent 70%),
    radial-gradient(130% 90% at 50% 112%, rgba(0, 0, 0, 0.38), transparent 55%);
}

/* The Play Online card is the hero: warm brass wash, an inner top highlight,
   and more shadow than its neighbours - it should read as the lit doorway. */
.mode-card.online-card {
  background: linear-gradient(180deg, rgba(217, 164, 65, 0.14), rgba(0, 0, 0, 0.22));
  border-color: rgba(217, 164, 65, 0.42);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10), 0 10px 26px rgba(0, 0, 0, 0.32);
}
.mode-card.online-card:hover { border-color: var(--brass); }
.online-now.lit { background: #0d2c1b; color: #b8f0c6; opacity: 1; }
.online-now.lit::before { animation: pulseDot 2.2s ease-in-out infinite; }
@keyframes pulseDot { 50% { opacity: .35; } }
#presence-chip.lit::before { animation: pulseDot 2.2s ease-in-out infinite; }

/* Mode cards catch the light: an inner top edge, and on hover the tile fan
   spreads - small, but it makes the cards feel like objects, not rectangles. */
.mode-card { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 4px 14px rgba(0, 0, 0, 0.22); }
@media (hover: hover) and (pointer: fine) {
  .mode-icons .tile { transition: transform .2s ease; }
  .mode-card:hover .mode-icons .tile { transform: rotate(calc((var(--k) - 1) * 11deg)) translateY(-3px); }
}

@media (prefers-reduced-motion: reduce) {
  .name-modal, .confirm-box { animation: none; }
  .online-now.lit::before, #presence-chip.lit::before { animation: none; }
  button:not(:disabled), .mode-icons .tile { transition: none; }
}

/* The live pill sits on its own line under the tag - inline it wrapped into a
   ragged three-line mess on narrow screens. */
.online-now { display: block; width: fit-content; margin: 5px auto 0; white-space: nowrap; }

/* Title entrance: the screen assembles itself - mascot settles, heading fades,
   cards rise in a stagger. One-shot on mount, skipped for reduced motion. */
@keyframes riseIn { from { transform: translateY(16px); } to { transform: none; } }   /* transform-only: see cardIn */
@keyframes mascotIn { from { transform: rotate(-14deg) translateY(-26px) scale(.9); } to { transform: rotate(-6deg); } }
.title-mascot .tile.big { animation: mascotIn .5s cubic-bezier(.2, .9, .3, 1.15) backwards; }
.title-heading { animation: riseIn .45s ease .08s backwards; }
.title-modes .mode-card { animation: riseIn .5s cubic-bezier(.2, .8, .3, 1) backwards; }
.title-modes .mode-card:nth-child(1) { animation-delay: .12s; }
.title-modes .mode-card:nth-child(2) { animation-delay: .19s; }
.title-modes .mode-card:nth-child(3) { animation-delay: .26s; }
.title-modes .mode-card:nth-child(4) { animation-delay: .33s; }
.title-modes .mode-card:nth-child(5) { animation-delay: .40s; }
.daily-card, .title-refer { animation: riseIn .5s ease .45s backwards; }

/* The seat whose turn it is breathes - the table tells you where the action
   is without a word. (The class was already there; it only changed color.) */
@keyframes seatBreathe { 50% { box-shadow: 0 0 0 2px rgba(217, 164, 65, 0.55), 0 6px 18px rgba(0, 0, 0, 0.3); } }
.oseat.active { animation: seatBreathe 2s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
  .title-mascot .tile.big, .title-heading, .title-modes .mode-card,
  .daily-card, .title-refer, .oseat.active { animation: none; }
}

/* ================================ iPad ====================================
   Tablets got the phone layout stretched wide: same 46px tiles adrift in
   space. Scale the objects, not just the container. Tile faces are SVG and
   every layout is flex/grid, so nothing here moves logic - it's all metrics.
   (Solitaire scales itself in JS, up to 2x, and is untouched.) */
@media (min-width: 768px) {
  .tile { width: 56px; height: 78px; border-radius: 9px; }
  .rack { gap: 8px; min-height: 90px; }
  .exgroup .tile, .pile .tile { width: 48px; height: 66px; }
  .oseat .tile { width: 46px; height: 64px; }   /* opponents keep the old scale; YOUR rack grows */
  button, select { font-size: 14.5px; padding: 8px 15px; border-radius: 9px; }
  .topbar { padding-left: 18px; padding-right: 18px; }

  /* Title: parlor-sign scale. */
  .title-heading h1 { font-size: 64px; }
  .title-sub { font-size: 16px; }
  .title-mascot .tile.big { width: 116px; height: 156px; }
  .title-modes { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 18px; }
  .mode-card { padding: 22px 18px 24px; border-radius: 18px; }
  .mode-card .mode-body p { font-size: 14.5px; }
  .mode-icons { height: 76px; }
  .mode-icons .tile { width: 48px; height: 66px; }
}

/* ============================== THE REVAMP =================================
   Material pass: the app reads as three surfaces - mahogany rail (chrome),
   green baize (play), ivory bone (tiles) - with brass as the only metal.
   Overrides the flatter base styles above; presentation only. */

/* The top bar is the mahogany rail of the table. Every screen opens on it. */
.topbar {
  background: linear-gradient(180deg, #4d301e 0%, #3a2415 100%);
  border-bottom: 1px solid #d9a44155;
  box-shadow: 0 1px 0 #ffffff14 inset, 0 8px 20px #00000048;
}

/* Ivory buttons become bone chips: a touch of 3D so they read as pressable. */
button, select {
  background: linear-gradient(180deg, #fdf8ec, var(--ivory) 55%, #e7dcc4);
  box-shadow: 0 1px 0 #ffffff90 inset, 0 1px 2px #00000042, 0 2px 5px #00000028;
  border-color: #c9bb9c;
}
button:disabled { box-shadow: none; }
/* Brass primaries gleam: metal, not paint. */
button.primary {
  background: linear-gradient(180deg, #eec367, var(--brass) 55%, #b9862c);
  box-shadow: 0 1px 0 #ffe9b8 inset, 0 2px 5px #00000045;
  border-color: #a67a24; color: #2a1c05;
}
button.primary:disabled { background: #ffffff12; box-shadow: none; }
button.callgood {
  background: linear-gradient(180deg, #3b9a63, #2f7d4f 60%, #256842);
  box-shadow: 0 1px 0 #ffffff30 inset, 0 2px 5px #00000045;
}
/* Dark chrome buttons (menu rows, name actions) keep their own colors. */
.menu-drop button, .name-actions button:not(.primary), .confirm-row button {
  background: #ffffff10; box-shadow: none; border-color: #ffffff2a; color: var(--ivory);
}

/* The discard pile is a sunken well in the baize - the one place on the felt
   that reads as INTO the table rather than on it. */
.pile-wrap {
  background: radial-gradient(ellipse at 50% 40%, var(--t-0b2e1d), var(--t-0a2818) 75%);
  border-radius: 12px;
  box-shadow: inset 0 2px 8px #000000b5, inset 0 1px 2px #00000090, 0 1px 0 #ffffff12;
}

/* Your hand sits on a wooden rack rail, like a real table. */
.rack {
  background: linear-gradient(180deg, transparent 0 58%, #00000030 59%, #3a2415cc 60%, #2c1b10cc);
  border-radius: 10px; padding: 6px 8px 9px;
}

/* Tiles: deeper ivory, crisper light. */
.tile {
  background: linear-gradient(168deg, #ffffff, #faf4e4 45%, var(--ivory) 65%, #d8cba9);
  box-shadow: 0 2px 3px #00000052, 0 4px 9px #00000030, inset 0 2px 2px #ffffffd8, inset 0 -3px 6px #00000014;
}

/* Right-hand panels and cards: dark glass with a brass hairline. */
.feltinfo { text-shadow: 0 1px 2px #00000080; }

/* The hand stays TWO ROWS, always: 14 tiles at 7 per row. On narrow phones the
   46px tile only fits 6 across, which wrapped the rack to a third row - so under
   430px the rack tile slims to exactly what seven-across needs. */
@media (max-width: 430px) {
  .rack { gap: 3px; padding-left: 4px; padding-right: 4px; }
  .rack .tile { width: 41px; height: 57px; }
}
@media (max-width: 350px) {
  .rack { gap: 3px; }
  .rack .tile { width: 33px; height: 46px; }
}

/* Mid-throw: the lifted tile announces it will be DISCARDED on release, and
   the hand below holds still (reordering is suspended while it glows). */
.rack .tile.dragging.throwing {
  box-shadow: 0 0 0 3px var(--brass), 0 0 22px 4px rgba(217, 164, 65, 0.55), 0 14px 26px rgba(0, 0, 0, 0.45) !important;
  filter: brightness(1.08);
}

/* The title card wears the same twin lanterns as the Parlor marquee - one
   sign, seen from the street and from inside. */
.pt-text::before, .pt-text::after {
  content: '🏮'; display: inline-block; font-size: 13px;
  animation: lanternSwayT 3.4s ease-in-out infinite; transform-origin: 50% -20%;
}
.pt-text::before { margin-right: 6px; }
.pt-text::after { margin-left: 6px; animation-delay: -1.7s; }
@keyframes lanternSwayT { 0%, 100% { transform: rotate(-8deg); } 50% { transform: rotate(8deg); } }
@media (prefers-reduced-motion: reduce) { .pt-text::before, .pt-text::after { animation: none; } }

/* Your standing online table, pinned on the title screen. */
#my-table-bar {
  display: block; width: min(560px, 100%); margin: 0 auto 14px;
  font: 700 14px var(--font); color: var(--ivory); text-align: left;
  background: linear-gradient(180deg, rgba(217, 164, 65, 0.18), rgba(0, 0, 0, 0.28));
  border: 1px solid rgba(217, 164, 65, 0.55); border-radius: 12px;
  padding: 11px 14px; cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
}
#my-table-bar:hover { border-color: var(--brass); }

/* ============================ READABILITY PASS ==============================
   Tester feedback: tiles are hard to see, especially the discard pile. The
   pile now runs BIGGER tiles in FEWER rows - you see less history but read
   every tile at a glance, and the pruning is measured, not counted, so the
   oldest rows still fall away on their own. */
.pile { gap: 5px; max-height: 288px; min-height: 288px; }   /* ~4 rows, RESERVED (min = max) so the rack never moves as the pile fills (owner, 9/12) */
.pile .tile { width: 47px; height: 65px; }
@media (min-width: 768px) {
  .pile { max-height: 240px; min-height: 240px; }
  .exgroup .tile, .pile .tile { width: 53px; height: 73px; }
}

/* The pile's whole history, one tap on its header away. */
/* The "buy your own card" note that stands where the house card would be on a
   THE CARD table. */
/* "I'm back" — reclaim your seat from the bot. A quiet corner card, not a modal:
   the table is still playing behind it and you may want to watch a beat first. */
.imback-ov { position: fixed; left: 0; right: 0;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px)); z-index: 60;
  display: flex; justify-content: center; pointer-events: none; padding: 0 12px; }
.imback-box { pointer-events: auto; max-width: 380px; width: 100%;
  background: var(--t-0e2b1fef); border: 1px solid var(--brass, #c9a558); border-radius: 12px;
  padding: 13px 15px; box-shadow: 0 8px 30px #000a; }
.imback-box h3 { margin: 0 0 4px; color: var(--brass, #d9a441); font-size: 15px; }
.imback-box p { margin: 0 0 10px; font-size: 13px; opacity: .85; }
.imback-btn { width: 100%; padding: 11px; font-size: 15px; font-weight: 700;
  border-radius: 9px; border: none; cursor: pointer;
  background: var(--brass, #d9a441); color: #23180a; }
.imback-btn:disabled { opacity: .5; cursor: default; }
.cardnote p { margin: 6px 0; font-size: 13px; opacity: .85; }
.cardnote a { color: var(--brass, #d9a441); font-weight: 700; }
/* "Your hand is dead" - a fact the player needs, said plainly and unmissably
   above the actions (owner, 9/3). */
.deadnote { margin: 6px 0; padding: 8px 11px; border-radius: 9px; font-size: 13px;
  font-weight: 600; line-height: 1.35; color: #ffd9d0;
  background: #4a1d16cc; border: 1px solid #d9614b88; }
.pile-toggle { cursor: pointer; user-select: none; -webkit-user-select: none; }
.pile .pile-old { display: none; }
/* Open shows the WHOLE history, but as a taller SCROLL area, never an unbounded
   one - an unbounded pile painted every discard every render and made late-game
   moves crawl (owner, 9/3). Bounded + overflow keeps only the visible rows
   painted; the pile auto-scrolls to the newest. */
.pile.pile-open { max-height: 46vh; min-height: 46vh; }   /* reserved too: opening the history is the one move, never the discards after */
.pile.pile-open .pile-old { display: block; }

/* The zoom's dead-tile count - the strategist's number under the big tile. */
.zoom-count {
  margin-top: 12px; padding: 7px 16px; border-radius: 99px;
  font: 700 14px var(--font); color: var(--ivory);
  background: var(--t-0b241ae8); border: 1px solid #ffffff2e;
}
.zoom-count.dead { color: #ff9d8f; border-color: #c0392b88; }

/* Player-chosen tile size, toggled from the menu mid-game. Large trades the
   two-row hand for readability - that is the player's call to make. */
:root[data-tiles="large"] .rack .tile { width: 52px; height: 72px; }
:root[data-tiles="large"] .rack { gap: 5px; }
:root[data-tiles="large"] .pile .tile { width: 54px; height: 75px; }
:root[data-tiles="large"] .pile { max-height: 330px; min-height: 330px; }
:root[data-tiles="large"] .exgroup .tile { width: 52px; height: 72px; }
/* LARGE MEANS LARGER ON A BIG SCREEN TOO (owner, 9/15, web: "the increased tile
   sizes are essentially the same size as normal - needs to be a bigger
   difference"). The rules above are phone sizes, and they outrank the 768px+
   standard sizes - so on a laptop Large set the rack to 52px when Standard was
   already 56px, and the pile grew one pixel. On wide screens Large is now about
   a quarter bigger than Standard everywhere you read tiles, and fitscreen.js no
   longer shrinks the table back down while it is on. */
@media (min-width: 768px) {
  :root[data-tiles="large"] .rack .tile { width: 70px; height: 97px; border-radius: 10px; }
  :root[data-tiles="large"] .rack { gap: 6px; }
  :root[data-tiles="large"] .pile .tile { width: 66px; height: 91px; }
  :root[data-tiles="large"] .exgroup .tile,
  :root[data-tiles="large"] .myarea .exgroup .tile.small { width: 66px; height: 91px; }
  :root[data-tiles="large"] .oseat .tile,
  :root[data-tiles="large"] .oseat .exgroup .tile.small { width: 52px; height: 72px; }
}

/* Hard mode's tracking panel: quiet, factual, judgment-free. */
.coach.hardtrack { opacity: .92; }
.coach.hardtrack .coach-title b { color: var(--brass); }
.coach.hardtrack .chat-dots { margin-top: 6px; font-size: 12px; opacity: .6; }

/* Today's Deal - the second daily, compact under the solitaire card. */
.deal-card {
  display: block; width: 100%; text-align: left; font: inherit; cursor: pointer;
  color: var(--ivory);
  background: linear-gradient(180deg, rgba(217, 164, 65, 0.10), rgba(0, 0, 0, 0.22));
  border: 1px solid rgba(217, 164, 65, 0.4); border-radius: 14px;
  padding: 10px 14px; margin: 10px 0 0;
}
.deal-card:hover { border-color: var(--brass); }
.deal-card.done { border-color: #4f9d6a; }
.deal-card .daily-body p { margin: 4px 0 0; font-size: 13px; opacity: .85; }
.deal-card .daily-body b { color: var(--brass); }
.deal-leader { font-size: 11.5px; color: var(--brass); font-weight: 700; }

/* Level chips: the small brass L-number that follows every name. */
.lvl {
  display: inline-block; margin-right: 5px; padding: 0 6px; border-radius: 8px;
  font-size: 10px; font-weight: 800; letter-spacing: .3px; vertical-align: 1px;
  color: #2a1c05; background: linear-gradient(180deg, #eec367, #c68f2e);
  border: 1px solid #a67a24;
}
/* The profile's XP bar: how far into this level, how far to the next. */
.fc-xpbar {
  height: 7px; border-radius: 99px; background: #00000045;
  border: 1px solid #ffffff1c; overflow: hidden; margin-top: 8px;
}
.fc-xpbar span { display: block; height: 100%; border-radius: 99px;
  background: linear-gradient(90deg, #c68f2e, #eec367); }

/* Rank materials: the level chip wears what tiles are made of. */
.lvl.lvl-bone   { color: #4a3d28; background: linear-gradient(180deg, #fdf8ec, #d9cdb2); border-color: #b9ad90; }
.lvl.lvl-bamboo { color: #0c2c18; background: linear-gradient(180deg, #6fca8f, #2f8a52); border-color: #256b41; }
.lvl.lvl-jade   { color: #06281c; background: linear-gradient(180deg, #7fe0c0, #2c9e78); border-color: #1f7a5b; }
.lvl.lvl-gold   { color: #2a1c05; background: linear-gradient(180deg, #eec367, #c68f2e); border-color: #a67a24; }
.lvl.lvl-dragon { color: #fff1ea; background: linear-gradient(180deg, #e06a50, #b03422); border-color: #8c2415; }

/* The profile's belt rack: ranks reached shine, ranks ahead wait in shadow. */
.fc-ranks { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 8px; }
.fc-ranks .lvl { margin-right: 0; font-size: 9.5px; padding: 1px 7px; }
.fc-ranks .lvl-locked { filter: grayscale(1) brightness(.55); opacity: .5; }

/* Prestige ranks above Dragon: the phoenix and the celestial court. */
.lvl.lvl-phoenix   { color: #fdf2ff; background: linear-gradient(180deg, #b06fd8, #7d3a9e); border-color: #632b80; }
.lvl.lvl-celestial { color: #eaf4ff; background: linear-gradient(180deg, #5a9be0, #2b5fa8); border-color: #1f4680; }

/* Onyx: the black belt. Level 50, the end of the mountain - black stone,
   gold lettering, the only chip that shines by NOT shining. */
.lvl.lvl-onyx { color: #e9bd5a; background: linear-gradient(180deg, #2a2a2a, #0c0c0c); border-color: #e9bd5a66; }

/* Names wear their rank color - you read a player's league at a glance.
   Doubled selectors out-rank the brass chat default without !important. */
.name-t-bone.name-t-bone { color: #e8dcc0; }   /* doubled class outguns .lob-chat-line b */
.name-t-bamboo.name-t-bamboo { color: #5fc287; }   /* doubled class outguns .lob-chat-line b */
.name-t-jade.name-t-jade { color: #58cfa4; }   /* doubled class outguns .lob-chat-line b */
.name-t-gold.name-t-gold { color: #e9bd5a; }   /* doubled class outguns .lob-chat-line b */
.name-t-dragon.name-t-dragon { color: #e8705a; }   /* doubled class outguns .lob-chat-line b */
.name-t-phoenix.name-t-phoenix { color: #c08ae0; }   /* doubled class outguns .lob-chat-line b */
.name-t-celestial.name-t-celestial { color: #7fb2ef; }   /* doubled class outguns .lob-chat-line b */
.name-t-onyx.name-t-onyx { color: #f5e9c8; text-shadow: 0 0 5px #000, 0 1px 1px #000; font-weight: 900; }

/* The in-game level number: small, colored by rank via name-t-*. */
.seat-lvl { font-size: 10px; font-weight: 800; margin-left: 4px; letter-spacing: .3px; opacity: .95; vertical-align: 1px; }

/* The shape record, under the solitaire board - somebody's name in lights. */
.sol-record {
  text-align: center; margin: 10px auto 2px; font-size: 13px; font-weight: 700;
  color: var(--brass); opacity: .92;
}
/* The parlor chat is ALWAYS fully visible - owner's rule: never folded,
   never hidden — and it scrolls INSIDE its own pane like a normal chat app
   (owner's ruling, replacing grow-forever): bounded height, newest pinned at
   the bottom by the painter, a reader scrolled up stays where they are. */
/* Never collapsed: a floor as well as a cap, so the log is always visibly a
   LOG (owner: "still closing up to where I can't see it"). */
#lounge-list { min-height: 140px; max-height: min(52vh, 420px); overflow-y: auto; overflow-anchor: none; }
/* A refused message says WHY right beside the box - the top banner was off-screen. */
#lounge-err { color: #e8705a; font-size: 13px; margin: 2px 0 4px; }

/* Conquered hands wear a brass check on the card (Alan's tracking). */
.cv-check { color: #d9a441; font-weight: 800; margin-right: 4px; }
.cv-row.cv-won .cv-pat { opacity: .92; }

/* The border key under the card - a temporary teaching reference (owner). */
.border-key { font-size: 12.5px; }
.border-key h3 { margin-bottom: 6px; }
.bk-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
.bk-swatch { flex: 0 0 auto; width: 18px; height: 24px; border-radius: 4px; background: #f2ecd8; }
.bk-blue { box-shadow: 0 0 0 2px #4d9bff; }
.bk-green { box-shadow: 0 0 0 2px #4bd07b; }
.bk-dotted { outline: 2px dotted #4d9bff; outline-offset: 1px; }
.bk-dim { opacity: .45; filter: saturate(.5); }
.bk-gold { box-shadow: 0 0 0 2px #d9a441; }
.bk-note { margin-top: 8px; font-size: 12px; opacity: .85; line-height: 1.45; }

/* Charleston pass hint: a positive "good to pass" marker. Dimming here read
   as DISABLED to a tester - so nothing dims while passing. Coach-blue and
   DOTTED, so it reads as kin to the set borders but clearly says "let go",
   with a soft glow so it cannot be missed (owner: make it more noticeable). */
.rack .tile.pass-hint {
  outline: 3px dotted #4d9bff; outline-offset: 2px;
  box-shadow: 0 0 10px 1px #4d9bff59;
}

/* First-time tip bubble: one at a time, above its anchor, tap to dismiss. */
.tip-bubble {
  position: fixed; z-index: 9450; transform: translateY(-100%);
  background: var(--t-1c2f23); border: 1px solid var(--brass, #c9a558); border-radius: 12px;
  color: var(--ivory, #f5efdc); font-size: 13.5px; line-height: 1.45;
  padding: 10px 13px; box-shadow: 0 6px 22px #000a; cursor: pointer;
  animation: tipin .28s ease-out;
}
@keyframes tipin { from { transform: translateY(-100%) scale(.92); } to { transform: translateY(-100%) scale(1); } }
@media (prefers-reduced-motion: reduce) { .tip-bubble { animation: none; } }

/* The zen note under an offline board: quieter than the record it sits below. */
.sol-record.xp-note { font-weight: 400; font-size: 11.5px; opacity: .55; margin-top: 2px; }
/* The XP price on an achievement card, between the badge and the tap hint. */
.ach-card-xp { font-size: 17px; font-weight: 800; color: #57c98a; margin-top: 6px; }

/* Your player card (offline app's Menu). */
.mycard { max-width: 340px; }
.mycard-head { display: flex; align-items: center; gap: 10px; margin: 6px 0 10px; }
.mycard-head svg { flex: 0 0 auto; }
.mycard-name { font-size: 20px; font-weight: 800; }
.mycard-sub { font-size: 12.5px; opacity: .85; margin-top: 8px; }
.mycard .fc-xpbar { height: 9px; border-radius: 99px; background: #00000045; border: 1px solid #ffffff1c; overflow: hidden; }
.mycard .fc-xpbar span { display: block; height: 100%; background: linear-gradient(90deg, #c68f2e, #eec367); }
.mycard .fc-ranks { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 10px; }
.mycard .fc-ranks .lvl { margin-right: 0; font-size: 9.5px; padding: 1px 7px; }
.mycard .lvl-locked { filter: grayscale(1) brightness(.55); opacity: .5; }

/* Your identity atop your own statistics. */
.stats-id { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.stats-id svg { flex: 0 0 auto; }
.stats-id-name { font-size: 17px; font-weight: 800; }
.stats-id-xp { margin-left: auto; font-size: 11.5px; opacity: .75; }

/* Paste a whole card. Collapsed by default so it never crowds the hand list;
   the textarea is monospace because a card typed in columns reads as columns. */
.ce-bulk { margin: 8px 0 12px; }
.ce-bulk > button { background: #ffffff12; border: 1px solid #ffffff2a; color: var(--ivory);
  border-radius: 7px; padding: 6px 11px; cursor: pointer; font-size: 12px; }
.ce-bulkpanel { margin-top: 8px; padding: 10px; border: 1px solid #ffffff22; border-radius: 9px; background: #0002; }
.ce-bulkhelp { font-size: 12px; opacity: .72; line-height: 1.45; margin-bottom: 7px; }
.ce-bulktext { width: 100%; box-sizing: border-box; background: #0003; border: 1px solid var(--brass);
  color: var(--ivory); border-radius: 7px; padding: 9px 10px;
  font: 13px/1.5 Consolas, Menlo, monospace; resize: vertical; }
.ce-bulktally { font-size: 12.5px; opacity: .85; margin: 7px 0 3px; }
.ce-bulkerr { font-size: 12px; color: #ffb4b4; display: flex; flex-direction: column; gap: 2px; margin-bottom: 7px; }
.ce-bulkpanel > button { background: #ffffff12; border: 1px solid #ffffff2a; color: var(--ivory);
  border-radius: 7px; padding: 6px 12px; cursor: pointer; font-size: 12.5px; }
.ce-bulkpanel > button:disabled { opacity: .4; cursor: default; }

/* Health notes on a typed card: warnings, not refusals - it is the player's
   card, and a real one may do something the checker does not expect. */
.ce-bulkwarn { color: #ffd9a0; }
.ce-health { margin: 8px 0 4px; display: flex; flex-direction: column; gap: 3px; font-size: 12.5px; color: #ffd9a0; }
.ce-health .ce-h-head { color: var(--ivory); opacity: .8; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; }


/* The publish result. Its own class: it shared one with the paste tally, and
   the two live in the same panel, so the message landed where nobody looked. */
.ce-pubnote { font-size: 13px; margin: 6px 0 10px; color: var(--brass); min-height: 1.2em; }

/* One picker for "which card is on the table" — house years and your own
   cards in the same list, because it is one decision. */
.title-cardsel { background: #00000038; border: 1px solid #ffffff2a; color: var(--ivory);
  border-radius: 8px; padding: 6px 10px; font-size: 13px; font-family: var(--font); max-width: 100%; }

/* The pairing code: big, spaced, and unmistakably meant to be read aloud to
   somebody holding another device. */
.pair-code { font: 700 30px/1.3 Consolas, Menlo, monospace; letter-spacing: .14em;
  color: var(--brass); margin: 10px 0 4px; user-select: all; }

/* The web address, on the title screen. Quiet - it is a signpost, not an ask. */
.title-web { font-size: 12.5px; opacity: .62; margin: 14px 0 2px; line-height: 1.5; }
.title-web b { color: var(--brass); opacity: .95; }

/* The host's table-setup screen. Same row shape as the offline setup panel —
   one choice per line, label left, control right — so hosting a table and
   starting a solo game feel like the same act. */
.lob-card .setup-row, .confirm-box .setup-row { display: flex; align-items: center; gap: 10px; margin: 10px 0; flex-wrap: wrap; }
/* The offline "Set up your game" panel is a SETTINGS screen, not a two-button
   question, so it gets settings-screen spacing: rows that breathe, a rule above
   the actions, and Launch pushed away from Back. They sat shoulder to shoulder
   and the owner called it — on a phone that is one thumb away from launching a
   game you meant to back out of. */
.confirm-box .setup-row { margin: 14px 0; }
.confirm-box .setup-row > label:first-child { min-width: 6em; }
.confirm-box .setup-row button { padding: 8px 14px; font-size: 13px; }
.confirm-box .confirm-actions {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin-top: 18px; padding-top: 16px; border-top: 1px solid #ffffff1f;
}
/* Back sits at the far end. Distance IS the safety here — nothing else stops a
   thumb aimed at Launch from landing on the way out. */
.confirm-box .confirm-actions > button:last-child { margin-left: auto; }
.confirm-box .confirm-actions button { padding: 9px 16px; border-radius: 9px; font-size: 13.5px; }
.confirm-box .confirm-actions button.primary { padding: 10px 20px; font-weight: 600; }
.lob-card .setup-row > label:first-child, .confirm-box .setup-row > label:first-child { font-size: 13px; opacity: .75; min-width: 5.5em; }
.lob-card .setup-row select, .confirm-box .setup-row select { flex: 1 1 auto; min-width: 0; background: #00000038;
  border: 1px solid #ffffff2a; color: var(--ivory); border-radius: 8px; padding: 6px 10px; font-size: 13px; }
.lob-card .setup-row button, .confirm-box .setup-row button { background: #ffffff12; border: 1px solid #ffffff2a; color: var(--ivory);
  border-radius: 7px; padding: 6px 11px; cursor: pointer; font-size: 12.5px; }
.lob-card .setup-row button.primary, .confirm-box .setup-row button.primary { background: var(--brass); border-color: var(--brass); color: #10261b; font-weight: 600; }

/* THE OTHER HANDS on the end-of-hand card. Everybody's tiles come down when the
   hand is decided, the way they do at a table - most of what you learn from a
   hand you lost is in what the others were holding. Deliberately smaller than
   the winning hand above it, and each row scrolls itself so a full rack never
   widens the card on a phone. */
.win-others { margin-top: 12px; display: flex; flex-direction: column; gap: 6px; }
.win-others-h {
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  opacity: .6; text-align: center;
}
.win-other { display: flex; align-items: center; gap: 8px; }
.win-other-n {
  flex: 0 0 72px; font-size: 12px; opacity: .75; text-align: right;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.win-other-t {
  flex: 1 1 auto; min-width: 0; display: flex; gap: 2px; overflow-x: auto;
  background: #0a120dad; border: 1px solid #ffffff1a; border-radius: 8px; padding: 4px;
}
.win-other-t .tile { width: 20px; height: 28px; border-radius: 4px; padding: 1px; flex: 0 0 auto; }
.win-other-t .tile.was-exposed { box-shadow: 0 0 0 1px var(--brass) inset; }
@media (max-width: 420px) {
  .win-other-n, .nw-other-n { flex-basis: 56px; font-size: 11px; }
  .win-other-t .tile { width: 17px; height: 24px; }
}

/* THE CLUE. On a table playing a card the app does not hold there is no card
   panel and no coach - just a pointer at the cardboard in the player's own
   hand, and only when they ask for it. Sized like a control, not a headline:
   it is a hint, and it should not out-shout the board. */
.clue-eye { width: 100%; font-size: 14px; padding: 8px 10px; }
.clue-line { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
/* A clue row is a BUTTON: tapping one tracks that line and brackets the tiles
   that serve it. The tracked one is marked, because a border with no visible
   cause is just the app doing something unexplained. */
.clue-row { text-align: left; width: 100%; padding: 7px 9px; font-weight: 600;
  font-size: 14px; color: #e8c979; font-variant-numeric: tabular-nums;
  background: #ffffff0d; border: 1px solid #c9a55c44; border-radius: 8px; cursor: pointer; }
.clue-row:hover { border-color: #c9a55c88; }
.clue-row.on { background: #d9a4411f; border-color: var(--brass, #d9a441); }
.clue-legacy { margin-top: 8px; font-weight: 600; font-size: 15px; color: #e8c979;
  letter-spacing: .01em; font-variant-numeric: tabular-nums; }
.clue-sub { margin: 6px 0 0; font-size: 11.5px; opacity: .72; line-height: 1.35; }

/* THE CARD-ENTRY INSTRUCTIONS. Long enough to be a reference rather than a
   hint, so it needs structure: headings people can scan for the bit they are
   stuck on, and examples in a fixed pitch beside what they mean. */
.ce-bulkhelp { font-size: 12.5px; line-height: 1.5; opacity: .9; }
.ce-bulkhelp .ce-help-h { margin: 10px 0 3px; font-weight: 700; opacity: 1;
  color: var(--brass, #d9a441); letter-spacing: .01em; }
.ce-bulkhelp .ce-help-h:first-child { margin-top: 2px; }
.ce-bulkhelp .ce-help-eg { display: flex; gap: 8px; align-items: baseline; margin: 2px 0; flex-wrap: wrap; }
.ce-bulkhelp .ce-help-eg code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; padding: 1px 6px; border-radius: 4px;
  background: #ffffff14; white-space: nowrap; }

/* The post-hand "here is why it could not win" line. Deliberately quiet: it is
   a lesson, not an accusation, and it only ever appears on your own screen. */
.bdead, .nw-died { color: #d9a86c; }

/* --- the card builder -------------------------------------------------------
   Tapping a card in has to feel like tapping, not like filling a form: big
   targets, everything on screen at once, and the line you are building visible
   under your thumb so there is never a translation step between what you tapped
   and what gets saved. */
.ce-builder { display: flex; flex-direction: column; gap: 7px; margin: 10px 0 12px;
  padding: 10px; border: 1px solid #6b552c55; border-radius: 10px; background: #00000018; }
.ce-brow { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; }
.ce-blab { min-width: 54px; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; opacity: .7; }
.ce-bquick { display: flex; flex-wrap: wrap; gap: 5px; padding-left: 54px; }
.ce-bchip { padding: 5px 9px; border-radius: 7px; border: 1px solid #6b552c66;
  background: #ffffff10; color: inherit; font-size: 12.5px; cursor: pointer; }
.ce-bchip:hover { background: #d9a4411a; }
.ce-bchip.on { background: #d9a441; color: #2a1e08; border-color: #d9a441; font-weight: 600; }
.ce-bchip.primary { background: #d9a441; color: #2a1e08; font-weight: 600; }
.ce-bchip.primary:disabled { background: #ffffff10; color: inherit; opacity: .45; cursor: default; }
.ce-bnumchip { min-width: 30px; font-variant-numeric: tabular-nums; }
.ce-bsec { flex: 1 1 160px; min-width: 0; padding: 5px 8px; border-radius: 7px;
  border: 1px solid #6b552c66; background: #ffffff10; color: inherit; }
.ce-bnum { width: 58px; padding: 5px 6px; border-radius: 7px; border: 1px solid #6b552c66;
  background: #ffffff10; color: inherit; font-variant-numeric: tabular-nums; }
/* The line so far, as tiles you can still poke at. */
.ce-bmade { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-height: 30px;
  padding: 5px; border-radius: 7px; background: #00000022; }
.ce-bmadechip { font-family: ui-monospace, Menlo, Consolas, monospace; }
.ce-btiles { margin-left: auto; font-size: 11.5px; opacity: .65; font-variant-numeric: tabular-nums; }
.ce-btiles.ok { opacity: 1; color: #8fd694; font-weight: 600; }
/* Exactly what will be written. No surprises at save time. */
.ce-bpreview { display: block; padding: 6px 8px; border-radius: 6px; background: #00000030;
  font-size: 12.5px; overflow-x: auto; white-space: pre; }
.ce-bactions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.ce-bprog { margin-left: auto; font-size: 11.5px; opacity: .65; }

/* --- FAQ ---------------------------------------------------------------------
   Shares the learn overlay's shell, so it does not read as a different app.
   Native <details>, because a disclosure the browser understands is keyboard
   navigable and findable by in-page search — a div pretending to be a button
   is neither. */
.faq-q { border-bottom: 1px solid #6b552c33; padding: 2px 0; }
.faq-q:last-child { border-bottom: none; }
.faq-q > summary {
  cursor: pointer; padding: 9px 2px; font-weight: 600; list-style: none;
  display: flex; align-items: baseline; gap: 8px;
}
.faq-q > summary::-webkit-details-marker { display: none; }
/* The marker is drawn rather than inherited, so it turns without a script. */
.faq-q > summary::before {
  content: '▸'; flex: 0 0 auto; opacity: .65;
  transition: transform .12s ease;
}
.faq-q[open] > summary::before { transform: rotate(90deg); }
.faq-q > summary:hover { color: var(--brass, #d9a441); }
.faq-q > p { margin: 0 0 9px 20px; opacity: .88; line-height: 1.5; }
.faq-q > p.faq-bullet { margin-left: 26px; text-indent: -10px; }
@media (prefers-reduced-motion: reduce) {
  .faq-q > summary::before { transition: none; }
}

/* --- the account panel -------------------------------------------------------
   Sits above the pairing code, because the pairing code assumes you can reach
   the device you are already signed in on — and the person reading this most
   urgently is the one who cannot. */
.acct-panel { display: flex; flex-direction: column; gap: 7px; }
.acct-links { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 2px; }
.linkish {
  background: none; border: none; padding: 0; color: var(--brass, #d9a441);
  font: inherit; font-size: 13px; text-decoration: underline; cursor: pointer;
}
.linkish:hover { opacity: .8; }
.acct-extra { display: flex; flex-direction: column; gap: 7px; margin-top: 8px;
  padding-top: 10px; border-top: 1px solid #6b552c44; }
/* The panel toggles its rows with the hidden attribute, and display:flex on
   .lob-row / .acct-extra used to win over it - the sign-in row stayed on screen
   under the set-up form. */
.acct-panel [hidden] { display: none !important; }
.lob-note.bad { color: #e0a0a0; }
/* The pairing and recovery codes, folded under the account (owner, 9/8: "make
   this less complex") - signing in is the way between devices; the codes are
   for people who never made a password. */
details.ce-adv { margin-top: 14px; border-top: 1px solid #6b552c44; padding-top: 8px; }
details.ce-adv > summary { cursor: pointer; color: var(--brass, #d9a441); font-size: 13px; list-style: none; }
details.ce-adv > summary::-webkit-details-marker { display: none; }
details.ce-adv > summary::before { content: '▸ '; opacity: .7; }
details.ce-adv[open] > summary::before { content: '▾ '; }

/* Somebody ended the Charleston a round early. Said on the board, because the
   courtesy pass arriving early is otherwise the only sign, and without a name
   that reads as the app skipping a round rather than a player's choice. */
.ch-declined { color: #e8c97a; font-weight: 600; }

/* Today's Deal in the parlor. A card that is entirely a button, so the whole
   thing is the target rather than a link buried in a paragraph. */
.daily-card-btn {
  display: block; width: 100%; text-align: left; cursor: pointer;
  border: 1px solid #6b552c66; font: inherit; color: inherit;
}
.daily-card-btn:hover { border-color: var(--brass, #d9a441); }
.daily-card-btn h3 { margin: 0 0 4px; }

/* --- the offline room --------------------------------------------------------
   Its own screen, so the title can ask one question - online or not - instead of
   showing five cards that compete for the same glance. */
/* THE BACK BUTTON MUST NOT SIT UNDER THE CLOCK. This screen is a .title-screen,
   which now pads itself clear of the Dynamic Island - and then this line put the
   padding straight back to 10px and undid it, because it is the same specificity
   and comes later. Back is at the top LEFT, which is exactly where the status bar
   clock is, so on a notched phone the one way out of this room was untappable.
   Owner: "The back button doesn't work after I select offline mode - it's
   blocked by my clock and won't work." */
.offline-screen { padding-top: 10px; padding-top: max(10px, calc(env(safe-area-inset-top) + 10px)); }
.off-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; margin: 0 0 12px; }
.off-back {
  background: none; border: 1px solid #6b552c66; border-radius: 8px;
  padding: 5px 11px; color: inherit; font: inherit; cursor: pointer;
  /* The one way out of this screen, so it is a full thumb target rather than the
     33px the text alone came to. Apple asks for 44; this game is played mostly on
     phones and mostly not by twenty-year-olds. */
  min-height: 44px; min-width: 44px;
}
.off-back:hover { border-color: var(--brass, #d9a441); }
.off-title { margin: 0; font-size: 1.5rem; }
.off-sub { flex: 1 1 100%; margin: 0; opacity: .7; font-size: 13px; }

/* The saved climb is the thing to tap; the fresh starts below it are the
   accident (Allen, 9/8: "The Resume button is too small. I accidentally undid
   my progress"). Resume reads as the main button; the two new-climb cards sit
   smaller beneath it whenever a climb is saved. */
.tower-pick .tower-resume { padding: 16px 18px; border: 2px solid var(--brass); background: #d9a44126; margin: 10px 0 4px; }
.tower-pick .tower-resume .tp-title { font-size: 19px; }
.tower-pick .tower-resume .tp-sub { font-size: 13px; opacity: .9; }
.tower-pick .tower-resume ~ .tower-pick-row .tower-pick-card { padding: 8px 10px; opacity: .85; }
.tower-pick .tower-resume ~ .tower-pick-row .tp-title { font-size: 13px; }
.tower-pick .tower-resume ~ .tower-pick-row .tp-sub { font-size: 11px; }
/* The race board: no double-tap zoom anywhere on it - two quick taps are a
   pair, not a zoom (owner, 9/8: "online solitaire still resizes after every
   click of the tile"). */
.solr-wrap, .sol-scalewrap, .sol-board { touch-action: manipulation; }
/* The Parlor's table board is one button and a window (owner, 9/8). */
.ot-open { width: 100%; text-align: left; font-size: 15px; padding: 12px 14px; }
.tables-box { max-width: 560px; width: min(94vw, 560px); max-height: 82vh; overflow: auto; }
.tables-box .ot-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #ffffff14; }
.tables-box .ot-row > span { flex: 1; min-width: 0; }

/* ONE SIZE PER MELD ROW on iPad and desktop (Allen's laptop screenshot, 9/8:
   a 3-tile meld at 53px beside a kong at 27px). The readability pass above
   grows `.exgroup .tile` to 53px, but the kong/quint trims (.big4/.big5) are
   more specific and kept their phone sizes, so the two sat together at half
   scale. Opponents' melds: one width for every meld, height by aspect ratio.
   Your own melds: the big ones trim gently instead of collapsing. */
@media (min-width: 768px) {
  .oseat .exgroup .tile.small, .oseat .exgroup.big4 .tile.small, .oseat .exgroup.big5 .tile.small { width: 40px; height: auto; }
  .myarea .exgroup .tile.small { width: 53px; height: 73px; }
  .myarea .exgroup.big4 .tile.small { width: 48px; height: 66px; }
  .myarea .exgroup.big5 .tile.small { width: 44px; height: 60px; padding: 2px; }
}

/* The header's Solo / 4 players toggle is gone (Allen, 9/10): the title screen chooses the game, and switching modes mid-game confused testers. setToggle still marks them; nobody sees it. */
.topbar button[data-players] { display: none !important; }
.pw-eye { background: none; border: 0; font: inherit; font-size: 1.1em; padding: 0 6px; cursor: pointer; opacity: .8; flex: 0 0 auto; }
.pw-eye:hover { opacity: 1; }

/* A tile the TRACKED line could still use under any reading (owner, 9/11): a soft blue ring, lighter than a set border. */
.rack .tile.line-fit { box-shadow: inset 0 0 0 2px rgba(77,155,255,.55); }
.rack .tile.line-fit.pass-hint { box-shadow: inset 0 0 0 2px rgba(77,155,255,.55); }

/* FIT TO SCREEN (js/fitscreen.js): the table column sets the scale; the side
   column scrolls inside what the scale leaves it. */
#board-root.fit .side { max-height: var(--fit-side, none); overflow-y: auto; }
/* THE PILE GROWS WITH THE SCREEN (Axe, 9/11, iPad: "all of that space could be
   showing the discards"). The four-row cap is a phone's; on a tablet the felt
   below it sat empty while 88 discards scrolled inside a small box. Tall and
   wide screens give the pile close to half the height, reserved (min = max)
   so the rack still never moves. */
@media (min-width: 700px) and (min-height: 900px) {
  .pile, :root[data-tiles="large"] .pile { max-height: 46vh; min-height: 46vh; }
  .pile.pile-open, :root[data-tiles="large"] .pile.pile-open { max-height: 62vh; }
}

/* THE OPPONENTS' PANELS ARE RESERVED TOO (owner, 9/12: "the table keeps
   jerking"). On a portrait phone the three seats sit in one row above the
   pile, and every meld a bot exposed added a ~46px row to its panel - the
   pile and the rack rode down eight times in one measured hand. Each seat
   now holds room for its name, its backs and THREE melds from the deal, with
   the exposure tiles a touch smaller so the rack still fits an 812px screen;
   only a fourth meld (rare) grows the row. Landscape keeps its own layout. */
@media (max-width: 560px) and (orientation: portrait) {
  .oseat { min-height: 210px; }
  .oseat .exgroup .tile.small { width: 20px; }
}

/* THE RACK'S HEADER ROW IS TWO FIXED LINES on a portrait phone: the name on
   the first, the Sort and Coaching buttons on the second, each button on ONE
   line (ellipsis). The Sort label changes mid-hand ("Sorted by suit — tap for
   1-9" becomes "Auto-sort OFF — tap for suit order" once the rack reads as
   hand-arranged) and at 375px that wrapped it a third line, dropping the
   rack 12px (layouttest, 9/12). */
@media (max-width: 560px) and (orientation: portrait) {
  .myarea .label { flex-wrap: wrap; min-height: 61px; align-content: flex-start; row-gap: 6px; }
  .myarea .label .nm { flex: 1 1 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .myarea .label .sortbtn { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; max-width: 100%; }
  /* The Sort button yields (it shrinks with an ellipsis); the Coaching chip
     keeps its size - so the two always share the second line. */
  .myarea .label .sortbtn:not(.coach-chip) { flex: 1 1 0; }
  .myarea .label .coach-chip { flex: 0 0 auto; }
}

/* A pass already given on this discard: dimmed, not clickable (owner, 9/15). */
.actions button.passed-call, .actions button.passed-call:disabled { opacity: .45; cursor: default; filter: saturate(.4); }

/* A bot covering for a person who left (owner, 9/15). */
.obot { color: #9fc3ff; font-weight: 700; font-size: 10px; letter-spacing: .06em; white-space: nowrap; }

/* Sort: three buttons, the lit one is the setting (owner, 9/15). */
.sortseg { margin-left: auto; display: inline-flex; flex: 0 0 auto; border-radius: 99px; overflow: hidden; border: 1px solid #ffffff2e; }
.sortseg .sortopt { margin-left: 0; border: 0; border-radius: 0; padding: 4px 10px; background: #00000038; color: inherit; opacity: .75; }
.sortseg .sortopt + .sortopt { border-left: 1px solid #ffffff22; }
.sortseg .sortopt.on { background: var(--brass, #c9a558); color: #1c1508; opacity: 1; font-weight: 700; }
.myarea .label .sortseg .sortopt { max-width: none; min-width: auto; overflow: visible; text-overflow: clip; flex: 0 0 auto; white-space: nowrap; }
.myarea .label .sortseg { flex: 0 0 auto; }

/* SIDE SEATS LIE THEIR MELDS DOWN FLAT ON A WIDE SCREEN (owner, 9/15, laptop
   screenshot: "way too much dead space - caused by called tiles"). The left and
   right seats stacked every called tile in a column one tile wide - four
   winds, three winds, two jokers ran eleven tiles down the side - so the felt
   grew tall, the pile sat over empty green, and fit-to-screen had to keep
   shrinking the table (77% -> 58% in one hand). They lie in rows now, the way
   phones already showed them: melds wrap, a kong or quint wraps inside its own
   box, the hand backs fan in rows. */
@media (min-width: 861px) and (min-height: 561px) {
  .oseat.pos-left .backs.vert, .oseat.pos-right .backs.vert { flex-direction: row; flex-wrap: wrap; }
  .oseat.pos-left .backs.vert .tileback, .oseat.pos-right .backs.vert .tileback { width: 10px; height: 14px; }
  .oseat.pos-left .exposures.vert, .oseat.pos-right .exposures.vert { flex-direction: row; flex-wrap: wrap; gap: 4px; align-items: flex-start; }
  .oseat.pos-left .exposures.vert .exgroup, .oseat.pos-right .exposures.vert .exgroup { flex-direction: row; flex-wrap: wrap; gap: 1px; }
  .oseat.pos-left .exgroup .tile.small, .oseat.pos-left .exgroup.big4 .tile.small, .oseat.pos-left .exgroup.big5 .tile.small,
  .oseat.pos-right .exgroup .tile.small, .oseat.pos-right .exgroup.big4 .tile.small, .oseat.pos-right .exgroup.big5 .tile.small { width: 26px; height: auto; }
  /* "why not make the calls a tad smaller so that you can save space" (owner,
     9/15): other people's called tiles are for reading, not for playing, so the
     top seat's come down too - the tiles in YOUR hand and the pile are
     untouched. */
  .oseat.pos-top .exgroup .tile.small, .oseat.pos-top .exgroup.big4 .tile.small, .oseat.pos-top .exgroup.big5 .tile.small { width: 34px; height: auto; }
  .oseat.pos-left .obody, .oseat.pos-right .obody { flex-direction: column; }
}

/* THE CALL METER (owner, 9/15: "make a meter for time to call counting down 10
   seconds", "small and next to you hand"). It rides in the actions row under
   the rack, beside the Call and Pass buttons - a thin bar that empties and the
   seconds beside it. Amber while there is time, red under three seconds. */
.callmeter { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; }
.callmeter-bar {
  display: block; width: 66px; height: 6px; border-radius: 3px;
  background: rgba(0, 0, 0, .28); box-shadow: inset 0 1px 2px rgba(0, 0, 0, .35);
  overflow: hidden;
}
.callmeter-fill { display: block; height: 100%; width: 100%; border-radius: 3px; background: #e8b64c; }
.callmeter-num { font-size: 12px; font-variant-numeric: tabular-nums; color: #e8dcc0; min-width: 22px; }
.callmeter.low .callmeter-fill { background: #d4553f; }
.callmeter.low .callmeter-num { color: #f0b9ad; }
@media (max-width: 860px) {
  .callmeter-bar { width: 48px; }
}

/* THE HOST'S TABLE CODE AND THE DOOR TO THE PARLOR (owner, 9/15: "when you are
   the host, you should be able to see your table code somewhere on your screen
   even when playing", and a button to make a private game public mid-game).
   It rides in the rack header beside the sort buttons - always on screen, never
   over the felt. */
.tablechip { display: inline-flex; align-items: center; gap: 6px; margin-left: 8px; vertical-align: middle; }
.tablechip-code {
  font-size: 12px; letter-spacing: .08em; font-weight: 700; color: #e8dcc0;
  background: rgba(0, 0, 0, .25); border: 1px solid rgba(232, 182, 76, .35);
  border-radius: 6px; padding: 2px 7px; white-space: nowrap;
}
.tablechip-share {
  font-size: 11px; color: #e8dcc0; background: rgba(0, 0, 0, .2);
  border: 1px solid rgba(232, 220, 192, .3); border-radius: 6px;
  padding: 2px 8px; cursor: pointer; white-space: nowrap;
}
.tablechip-share:hover { background: rgba(232, 182, 76, .18); }
.tablechip-share.on { color: #bfe3c2; border-color: rgba(120, 200, 130, .45); }
@media (max-width: 860px) {
  .tablechip { margin-left: 6px; gap: 4px; }
  .tablechip-code, .tablechip-share { font-size: 10px; padding: 1px 5px; }
}

/* "You can play on your computer" (owner, 9/16). A quiet line in the Parlor's
   Start a table card - the moment somebody is choosing a game is the moment
   the bigger screen is worth knowing about. */
.lob-anywhere { margin-top: 8px; opacity: .85; }
.lob-anywhere b { color: #e8b64c; font-weight: 600; white-space: nowrap; }

/* The softer of the two coach warnings: the line is still reachable, but not
   with anything you are holding (Allen, 9/16). */
.trackwarn.soft { opacity: .92; }

/* Today's players - the owner's list (todaysheet.js). */
.today-stats { display: flex; gap: 8px; margin: 10px 0 6px; flex-wrap: wrap; }
.today-stats > div { flex: 1 1 70px; background: #00000030; border: 1px solid #ffffff18; border-radius: 10px; padding: 8px 10px; text-align: center; }
.today-stats b { display: block; font-size: 24px; color: var(--brass); line-height: 1.1; }
.today-stats span { font-size: 11.5px; opacity: .85; }
.today-note { font-size: 12.5px; opacity: .8; margin: 8px 0; }
.today-row { display: flex; align-items: baseline; gap: 10px; padding: 7px 2px; border-bottom: 1px solid #ffffff12; }
.today-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.today-at { font-size: 12px; opacity: .75; white-space: nowrap; }
.today-new { font-size: 10.5px; background: #1f8a4c; color: #fff; border-radius: 6px; padding: 1px 6px; margin-left: 4px; }
.today-noacct { font-size: 10.5px; border: 1px solid #ffffff30; border-radius: 6px; padding: 0 5px; margin-left: 4px; opacity: .7; }

/* Table colour picker (tablecolor.js). */
.tcolor-modal { width: min(440px, 94vw); }
.tcolor-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 8px; margin: 10px 0; }
.tcolor { display: flex; align-items: center; gap: 8px; padding: 9px 10px; border-radius: 10px; cursor: pointer;
  background: #00000030; border: 1px solid #ffffff1f; color: var(--ivory); font-size: 14px; text-align: left; }
.tcolor.on { border-color: var(--brass); box-shadow: 0 0 0 2px var(--brass) inset; }
.tcolor-chip { width: 22px; height: 22px; border-radius: 50%; border: 1px solid #ffffff55; flex: 0 0 auto; }

/* The three dailies as ONE card on the title (owner, 9/21), and the chooser it
   opens (title.js buildDailiesSummary / showDailiesChoice). Rows: the challenge,
   today's leader, and where you stand; they stack on a phone. */
.dailies-card { display: block; width: 100%; text-align: left; cursor: pointer; color: inherit;
  margin: 0 0 14px; padding: 12px 14px 13px; border-radius: 14px;
  border: 1px solid var(--brass); background: linear-gradient(180deg, #ffffff14, #ffffff08);
  animation: riseIn .5s ease .45s backwards; }
.dailies-card:hover { background: linear-gradient(180deg, #ffffff1e, #ffffff0d); }
.dsum-count { font-size: 12px; font-weight: 700; color: var(--brass); white-space: nowrap; }
.dsum-row { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.2fr) minmax(0, .9fr); gap: 4px 10px;
  align-items: baseline; padding: 8px 0; border-top: 1px solid #ffffff14; }
.dsum-row:first-of-type { border-top: none; }
.daily-head + .dsum-row { margin-top: 6px; }
.dsum-name b { display: block; font-size: 14.5px; }
.dsum-name span { display: block; font-size: 11.5px; opacity: .75; }
.dsum-lead { font-size: 12.5px; color: var(--brass); font-weight: 700; overflow-wrap: anywhere; }
.dsum-none { color: var(--ivory); opacity: .55; font-weight: 400; }
.dsum-mine { font-size: 12px; opacity: .85; text-align: right; }
.dsum-row.done .dsum-mine { color: #8fd6a6; opacity: 1; }
.dailies-card .mode-play { display: inline-block; margin-top: 8px; }
@media (max-width: 460px) {
  .dsum-row { grid-template-columns: minmax(0, 1fr) auto; }
  .dsum-lead { grid-column: 1 / -1; grid-row: 2; }
  .dsum-mine { grid-row: 1; grid-column: 2; }
}
.dailies-pick { width: min(520px, 94vw); max-height: 88vh; max-height: 88dvh; overflow-y: auto; }
.dailies-list { margin: 12px 0 4px; }
.dailies-list .daily-card, .dailies-list .deal-card { animation: none; }
@media (prefers-reduced-motion: reduce) { .dailies-card { animation: none; } }
