faience / css /style.css
RemiFabre's picture
Deploy browser player — run4/ckpt-037888 elo 2361.0
955e8d0
Raw
History Blame Contribute Delete
17.3 kB
/* Page shell for the in-browser player.
*
* The table itself — tiles, the two boards, the status band, the log, the inline
* scoring panels and the tile flights — lives in ui/board.css, which is a copy of
* web/play/ui/board.css (see web/play/ui/PORTING.md). The hosted player and the
* local GUI therefore draw the *same* table from two different back ends: a Flask
* server there, a Web Worker running ONNX in the tab here.
*
* This file is only what belongs to *this* page: the linen ground, the header and
* its setup bar, the engine (model download) strip, the column layout, the supply
* and "how this works" boxes, and a harder look at small screens — this one is
* meant to be playable on a phone.
*
* Layout, top to bottom: header · engine strip · status band · the playfield
* (the factories on the left, the two identical boards — you above, AI below —
* beside them; <body data-boards="stack">, the + in settings, restores the big
* boards under the factories) · inline scoring · the move log and the about
* box · the bottom bar (the AI's clock and the tile totals). Nothing is ever
* covered by anything: there is no overlay and no toast on this page.
*/
@import url("../ui/board.css");
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
font: 15px/1.5 var(--font-body);
background-color: var(--linen);
/* woven linen: two hairline threads plus a warm vignette */
background-image:
linear-gradient(var(--linen-thread) 1px, rgba(0, 0, 0, 0) 1px),
linear-gradient(90deg, var(--linen-thread) 1px, rgba(0, 0, 0, 0) 1px),
radial-gradient(120% 90% at 50% 0%, var(--linen-warm) 0%, var(--linen) 55%, var(--linen-dark) 100%);
background-size: 7px 7px, 7px 7px, 100% 100%;
}
h1, h2, h3 { margin: 0; font-weight: 400; }
button, input, select { font: inherit; color: inherit; }
/* ------------------------------------------------------------------ top bar */
.topbar {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 14px 24px;
padding: 12px clamp(14px, 3vw, 34px);
background: linear-gradient(var(--topbar-top), var(--topbar-bottom));
border-bottom: 1px solid var(--grout);
box-shadow: 0 6px 18px -14px rgba(60, 44, 20, .6);
}
.brand { display: flex; align-items: baseline; gap: 12px; margin-right: auto; }
.brand-name {
font-family: var(--font-display);
font-size: 28px;
letter-spacing: .16em;
text-transform: uppercase;
/* the title is glazed like a tile: cobalt with a highlight along the top */
background: linear-gradient(var(--c0-hi), var(--c0) 55%, var(--c0-lo));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.brand-sub { font-family: var(--font-label); font-size: 12.5px; letter-spacing: .08em; color: var(--ink-soft); }
.setup { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 14px; }
.field { display: flex; flex-direction: column; gap: 4px; }
.field-label {
font-family: var(--font-label);
font-size: 11px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--ink-faint);
}
.field select, .field input {
padding: 6px 9px;
min-width: 132px;
background: var(--field-bg);
border: 1px solid var(--grout);
border-radius: 2px;
box-shadow: 0 1px 0 rgba(255, 255, 255, .8) inset;
}
.field-narrow input { min-width: 78px; width: 88px; }
.field-narrow select { min-width: 132px; }
/* the opponent's glaze, as a small tile beside the menu and on its board */
.bot-pick { display: flex; align-items: center; gap: 7px; }
.bot-pick select { min-width: 150px; }
.bot-chip {
width: 14px;
height: 14px;
border-radius: 3px;
flex: none;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .35), 0 1px 0 rgba(255, 255, 255, .45) inset;
}
.check { display: flex; align-items: center; gap: 7px; padding-bottom: 6px; font-size: 14px; }
.check.coach { padding: 5px 10px 5px 8px; border: 1px solid var(--grout); border-radius: 2px; background: var(--field-bg); }
.check.coach.off { opacity: .55; }
.btn {
padding: 7px 15px;
border: 1px solid var(--grout);
border-radius: 2px;
background: linear-gradient(var(--btn-top), var(--btn-bottom));
cursor: pointer;
font-family: var(--font-label);
font-size: 13px;
letter-spacing: .1em;
text-transform: uppercase;
transition: transform .12s ease, box-shadow .12s ease, background .12s ease;
}
.btn:hover { background: linear-gradient(var(--btn-top-hover), var(--btn-bottom-hover)); }
.btn:active { transform: translateY(1px); }
.btn-primary {
color: var(--btn-primary-ink);
border-color: var(--c0-lo);
background: linear-gradient(var(--btn-primary-top), var(--c0) 60%, var(--btn-primary-bottom));
box-shadow: 0 1px 0 rgba(255, 255, 255, .35) inset, 0 2px 6px -3px rgba(20, 40, 80, .8);
}
.btn-primary:hover { background: linear-gradient(var(--btn-primary-top-hover), var(--btn-primary-bottom-hover)); }
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; }
:where(a, button, input, select, [tabindex]):focus-visible {
outline: 2px solid var(--c4-lo);
outline-offset: 2px;
}
/* ------------------------------------------------------- engine status bar */
/* The net is a 13 MB download, so its state is not a detail to hide in a corner:
* it gets its own strip under the header, and the Deal button stays disabled
* until the worker says it is ready. */
.engine-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
padding: 8px clamp(14px, 3vw, 34px);
background: linear-gradient(var(--engine-top), var(--engine-bottom));
border-bottom: 1px solid var(--grout);
font-family: var(--font-label);
font-size: 12px;
letter-spacing: .06em;
color: var(--ink-soft);
}
.engine-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--c1);
box-shadow: 0 0 0 3px var(--c1-wash);
animation: kiln 1.2s ease-in-out infinite;
}
@keyframes kiln {
0%, 100% { opacity: .35; }
50% { opacity: 1; }
}
.engine-bar.ready .engine-dot { background: var(--c4); box-shadow: 0 0 0 3px var(--c4-wash); animation: none; }
.engine-bar.failed .engine-dot { background: var(--c2); box-shadow: 0 0 0 3px var(--c2-wash); animation: none; }
.engine-text { flex: 1 1 auto; min-width: 200px; }
/* what's new: one dismissible line, edged in cobalt so it reads as news */
.news-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 8px clamp(14px, 3vw, 34px);
background: linear-gradient(var(--plaster-hi), var(--plaster));
border-bottom: 1px solid var(--grout);
border-left: 4px solid var(--c0);
font-family: var(--font-label);
font-size: 13px;
letter-spacing: .03em;
color: var(--ink);
}
.news-badge {
padding: 2px 8px;
border-radius: 999px;
background: var(--c0);
color: var(--plaster-hi);
font-size: 10.5px;
letter-spacing: .14em;
text-transform: uppercase;
white-space: nowrap;
}
.news-bar[hidden] { display: none; }
.news-text { flex: 1 1 auto; }
.news-line { display: block; line-height: 1.55; }
.news-close {
border: 1px solid var(--badge-edge);
background: none;
border-radius: 999px;
width: 22px;
height: 22px;
line-height: 1;
cursor: pointer;
color: var(--ink-soft);
}
.news-close:hover { color: var(--ink); }
.engine-badge {
padding: 3px 9px;
border-radius: 999px;
background: var(--badge-bg);
border: 1px solid var(--badge-edge);
text-transform: uppercase;
letter-spacing: .12em;
font-size: 11px;
color: var(--c0-ink);
white-space: nowrap;
}
/* -------------------------------------------------------------------- table */
.table {
display: flex;
flex-direction: column;
gap: clamp(10px, 1.4vw, 18px);
padding: clamp(12px, 1.8vw, 22px) clamp(12px, 3vw, 34px) 44px;
max-width: 1560px;
margin: 0 auto;
}
.middle-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 12px;
margin-top: 10px;
}
/* The playfield. Two ways to draw it, picked in the settings panel and
* defaulted per device (see ui/layout.js):
* stack (the +, a desktop's default) — the classic table: big boards, side
* by side below the factories;
* side (the −, a phone's default) — the whole game on one screen: the
* factories on the left, both boards, smaller, stacked beside them.
* The base rules below are the stack look, so a desktop paints correctly
* before any script runs; the phone media query flips its own default. */
.playfield {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: clamp(10px, 1.4vw, 18px);
align-items: start;
}
.boards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: clamp(10px, 1.4vw, 18px);
align-items: start;
}
body[data-boards="side"] .playfield {
grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr);
}
body[data-boards="side"] .boards {
grid-template-columns: minmax(0, 1fr);
gap: clamp(8px, 1vw, 14px);
}
/* side layout: both boards must fit beside the factories, so they shrink */
body[data-boards="side"] .board { --tile: clamp(13px, 2.1vw, 36px); }
.below {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(240px, .8fr);
gap: clamp(10px, 1.4vw, 18px);
align-items: start;
}
.below-side { display: flex; flex-direction: column; gap: clamp(10px, 1.4vw, 18px); }
/* ------------------------------------------------------------- game record */
/* Shown only once a game is over: the finished game, offered as a file. Quiet
* by construction, because it is an invitation and not a demand. */
.record-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 18px;
}
.record-bar[hidden] { display: none; }
.record-note {
flex: 1 1 320px;
margin: 0;
font-size: 13.5px;
color: var(--ink-soft);
}
.record-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
.record-link {
font-family: var(--font-label);
font-size: 12px;
letter-spacing: .06em;
color: var(--c0);
}
.record-bar.done .record-note { color: var(--ink); }
/* --------------------------------------------------------------- bottom bar */
/* The quiet end of the page: the AI's clock lives here (moved off the top bar,
* where it crowded a phone), next to one line of totals. #lid-row inside the
* totals is where discarded tiles fly — a target, so it keeps a real box. */
.bottom-bar {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 12px 22px;
}
.counts {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 16px;
padding-bottom: 8px;
font-family: var(--font-label);
font-size: 12px;
letter-spacing: .06em;
color: var(--ink-soft);
}
#lid-row { border-radius: 3px; padding: 2px 4px; transition: background .3s ease; }
#lid-row.receiving { background: var(--lid-receiving); }
/* --------------------------------------------------------------- about box */
.about p { margin: 0 0 8px; font-size: 13.5px; color: var(--ink-soft); }
.about p:last-child { margin-bottom: 0; }
.about a { color: var(--c0); }
.about-meta {
font-family: var(--font-label);
font-size: 11.5px;
letter-spacing: .04em;
color: var(--ink-faint);
}
body.locked .board-grid, body.locked .factories, body.locked .center-dish { cursor: progress; }
.hidden { display: none !important; }
/* -------------------------------------------------------------- responsive */
@media (max-width: 1080px) {
.below { grid-template-columns: minmax(0, 1fr); }
.status { flex-wrap: wrap; }
}
@media (max-width: 720px) {
/* Phone layout: the boards are the page, one above the other so neither of
* them has to shrink below a playable tile. Every control keeps a 44px target. */
.topbar { gap: 6px 10px; padding: 8px 10px; }
.brand { width: 100%; margin-right: 0; gap: 8px; }
.brand-name { font-size: 19px; }
.brand-sub { font-size: 11px; }
/* seed, "you go first" and Deal share one row: the band, smaller */
.setup { width: 100%; gap: 6px 8px; align-items: center; flex-wrap: nowrap; }
.setup .btn { flex: 0 0 auto; padding: 10px 11px; white-space: nowrap; }
.setup .field { flex: 0 1 100px; }
.setup .field .field-label { display: none; }
.setup .check { flex: 0 0 auto; }
.field select, .field input { min-width: 0; padding: 9px 8px; }
.field-narrow select, .field-narrow input { width: auto; min-width: 0; }
.btn { padding: 10px 14px; min-height: 44px; }
.engine-bar { padding: 5px 10px; font-size: 11px; gap: 4px 10px; }
.engine-text { min-width: 0; }
/* the settings and the navigator are controls, so they get a tap target too */
.gear, .speed, .nav-btn { min-height: 44px; }
.settings-panel { gap: 8px 12px; }
.check { padding-bottom: 0; min-height: 44px; }
.check input { width: 20px; height: 20px; }
.table { padding: 8px 8px 32px; gap: 8px; }
.panel { padding: 11px 11px 13px; }
.scoring-grid { grid-template-columns: minmax(0, 1fr); }
.log { max-height: 190px; }
/* the status band earns its keep in one small line, not a headline */
.status { padding: 8px 12px 10px; gap: 10px; }
.status-headline { font-size: 15px; }
.status-detail { font-size: 10.5px; margin-top: 2px; min-height: 0; }
.status-side { min-width: 40px; }
.status-side-score { font-size: 19px; }
.status-dash { font-size: 15px; }
/* stack (+): the old phone table — one big board under the other */
body[data-boards="stack"] .boards { grid-template-columns: minmax(0, 1fr); }
body[data-boards="stack"] .board { --tile: clamp(26px, 9vw, 38px); }
body[data-boards="stack"] .factories { gap: 10px 12px; }
body[data-boards="stack"] .factory { --dish-size: calc(var(--tile) * 2 + 22px); }
body[data-boards="stack"] .middle-table { --tile: clamp(24px, 8vw, 32px); }
body[data-boards="stack"] .board-grid { column-gap: 8px; }
body[data-boards="stack"] .line { padding: 0 2px; }
body[data-boards="stack"] .line .row-note { font-size: 10.5px; line-height: 1.15; }
/* side (−, the default): factories left, both boards beside them. Small
* tiles are the accepted price of seeing the whole game at once. */
body:not([data-boards="stack"]) .playfield {
grid-template-columns: minmax(0, 1.04fr) minmax(0, .96fr);
gap: 6px;
}
body:not([data-boards="stack"]) .boards { grid-template-columns: minmax(0, 1fr); gap: 6px; }
body:not([data-boards="stack"]) .board {
--tile: clamp(11px, calc((48vw - 50px) / 10), 38px);
padding: 7px 6px 8px;
gap: 6px;
}
body:not([data-boards="stack"]) .board-grid { column-gap: 3px; }
body:not([data-boards="stack"]) .line { padding: 0 1px; gap: 2px; }
body:not([data-boards="stack"]) .line .row-note { display: none; }
body:not([data-boards="stack"]) .wall-row { gap: 2px; padding: 0 3px; }
body:not([data-boards="stack"]) .board-head { column-gap: 6px; padding-bottom: 5px; }
body:not([data-boards="stack"]) .board-who { font-size: 10px; letter-spacing: .1em; }
body:not([data-boards="stack"]) .board-score { font-size: 18px; }
body:not([data-boards="stack"]) .board-notes { font-size: 9.5px; min-height: 12px; }
body:not([data-boards="stack"]) .floor { padding: 3px 2px; gap: 2px; }
body:not([data-boards="stack"]) .floor .pen { font-size: 8.5px; }
body:not([data-boards="stack"]) .middle-panel { padding: 8px 6px 10px; }
body:not([data-boards="stack"]) .middle-table { --tile: clamp(17px, 5.6vw, 32px); }
body:not([data-boards="stack"]) .factories { gap: 8px 8px; }
body:not([data-boards="stack"]) .factory { --dish-size: calc(var(--tile) * 2 + 16px); }
body:not([data-boards="stack"]) .center-dish { padding: 8px 10px; margin-top: 8px; }
}
@media (max-width: 420px) {
body[data-boards="stack"] .board { --tile: clamp(23px, 10vw, 34px); }
body[data-boards="stack"] .middle-table { --tile: clamp(21px, 8.5vw, 28px); }
.engine-badge { font-size: 10px; letter-spacing: .08em; }
.status { gap: 8px; }
}
/* A coarse pointer never hovers, so the hover lift just makes taps feel laggy. */
@media (hover: none) {
button.tile:hover { transform: none; }
button.line.open:hover { background: var(--c4-wash); filter: none; }
}
/* ========================================================== corner buttons ==
* Two small round buttons pinned to the bottom-right, the way every board-game
* site pins its "?" — one scrolls to the What-this-is panel, one opens the
* public tally. Fixed, but tiny: they float over the linen, never the table. */
.corner {
position: fixed;
right: 14px;
bottom: 14px;
z-index: 20;
display: flex;
flex-direction: column;
gap: 8px;
}
.corner-btn {
width: 42px;
height: 42px;
display: grid;
place-items: center;
padding: 0;
border: 1px solid var(--grout);
border-radius: 50%;
background: linear-gradient(var(--btn-top), var(--btn-bottom));
box-shadow: var(--shadow-tile);
cursor: pointer;
color: var(--ink-soft);
font-family: var(--font-display);
font-size: 21px;
line-height: 1;
text-decoration: none;
}
.corner-btn:hover {
background: linear-gradient(var(--btn-top-hover), var(--btn-bottom-hover));
color: var(--ink);
}
.corner-btn svg { width: 18px; height: 18px; fill: currentColor; display: block; }
/* the "?" button's landing: the panel it scrolled to glows for a breath */
.about.lit { box-shadow: var(--shadow-panel), 0 0 0 3px var(--c4-wash); }
.about { transition: box-shadow .5s ease; }