Serve practice mode from the main server, add mode switch, fix touch

Practice mode is now at /practice on server.py instead of a separate
server, with a button in the bottom-right corner of each page to the
other mode. Touch: no double-tap zoom, no sticky hover after a tap, no
long-press selection on the board, and a pressed look on tapped squares.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
tukaunuandClaude Opus 5.5 committed 2026-10-22 01:47:27 +02:00
1 parent c52f30dc59
commit ce6b71eb35
7 files changed
+51 -42

No files matched your search

+25 -4
View File
@@ -42,7 +42,7 @@ body {
main {
width: 100%;
max-width: 420px;
padding: 48px 16px;
padding: 48px 16px 96px; /* room at the bottom to scroll clear of the mode switch */
}
h1 { font-size: 2.2rem; margin: 0 0 8px; letter-spacing: -0.02em; }
@@ -57,10 +57,15 @@ button {
background: var(--surface);
color: var(--text);
cursor: pointer;
touch-action: manipulation; /* no double-tap zoom or tap delay on touch screens */
}
button:hover { border-color: var(--muted); }
button.primary { background: var(--text); color: var(--bg); border-color: var(--text); width: 100%; }
button.primary:hover { opacity: 0.9; }
/* Hover styles only for real pointers: on touch screens they stick after a tap */
@media (hover: hover) {
button:hover { border-color: var(--muted); }
button.primary:hover { opacity: 0.9; }
.cell.playable:hover { border-color: var(--muted); }
}
button.small { padding: 6px 12px; font-size: 0.875rem; }
button.ghost { background: transparent; }
@@ -141,6 +146,8 @@ button.player[aria-pressed="true"] .who { font-weight: 600; }
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-rows: repeat(3, minmax(0, 1fr));
aspect-ratio: 1; width: min(100%, 60vh); margin: 0 auto;
touch-action: manipulation;
user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; /* no long-press selection or menu */
}
.cell {
position: relative;
@@ -148,9 +155,10 @@ button.player[aria-pressed="true"] .who { font-weight: 600; }
border: 1px solid var(--border); background: var(--surface);
display: grid; place-items: center;
cursor: default;
-webkit-tap-highlight-color: transparent;
}
.cell.playable { cursor: pointer; }
.cell.playable:hover { border-color: var(--muted); }
.cell.playable:active { border-color: var(--muted); background: color-mix(in srgb, var(--border) 40%, var(--surface)); }
.cell.win { background: color-mix(in srgb, var(--win) 30%, var(--surface)); border-color: var(--win); }
.cell svg { position: absolute; inset: 21%; width: 58%; height: 58%; overflow: visible; }
.cell svg * { fill: none; stroke-width: 12; stroke-linecap: round; }
@@ -178,4 +186,17 @@ button.player[aria-pressed="true"] .who { font-weight: 600; }
.actions > button { flex: 1; }
.actions > button.primary { flex: 2; }
.actions > button:disabled { opacity: 0.45; cursor: default; border-color: var(--border); }
/* Fixed bottom-right button to the other mode (multiplayer <-> practice) */
.mode-switch {
position: fixed;
right: calc(16px + env(safe-area-inset-right, 0px));
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
padding: 8px 16px;
border: 1px solid var(--border); border-radius: 999px; background: var(--surface);
box-shadow: 0 2px 10px rgb(0 0 0 / 0.12);
color: var(--text); font-size: 0.875rem; font-weight: 600; text-decoration: none;
touch-action: manipulation;
}
@media (hover: hover) { .mode-switch:hover { border-color: var(--muted); } }
.meta { color: var(--muted); font-size: 0.875rem; text-align: center; margin: 12px 0 0; min-height: 1.5em; }