faience / ui /board.css
RemiFabre's picture
Deploy browser player — run4/ckpt-037888 elo 2361.0
955e8d0
Raw
History Blame Contribute Delete
31.7 kB
@import url("theme.css");
/* Azul board kit — the portable half of the page.
*
* Direction: glazed azulejo ceramic on a linen table. A tile that is on the
* board is saturated, rimmed and raised; a square that is empty is a neutral
* recessed well. That single contrast is what makes the position readable from
* across a room, and it is the thing this file exists to protect.
*
* **Every colour comes from theme.css.** There is not one hex literal below
* this comment — if you find yourself typing one, add a token instead, or the
* next skin will quietly break. See THEMING.md.
*
* Scope: tiles, the two player boards, the middle of the table, the status
* band, the settings panel, the move navigator, the move log, the inline
* scoring panels and the flight layer. Nothing in here knows about a server, a
* form or a page shell — this file plus ui/*.js is what another page needs to
* draw the same table.
*
* Motion: animations are governed by the in-game speed setting alone (see
* animate.js), never by an OS accessibility flag. `body[data-anim="off"]` is
* the only thing that turns them off, and the setting writes it.
*/
.panel {
padding: 14px 16px 16px;
/* the solid colour matters as well as the gradient: it is what a contrast
* check (and a browser with gradients disabled) actually reads */
background-color: var(--panel-bottom);
background-image: linear-gradient(var(--panel-top), var(--panel-bottom));
border: 1px solid var(--panel-edge);
border-radius: 4px;
box-shadow: var(--shadow-panel);
}
.panel-title {
margin: 0 0 10px;
font-family: var(--font-label);
font-size: 11px;
font-weight: 400;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--ink-faint);
}
/* ============================================================== status band ==
* The signature: one wide plaster cartouche that says what is happening, keeps
* the score, and turns into the AI's clock when it is thinking. Because it
* always holds the answer, nothing ever has to cover the board to ask you to
* wait — there is not a single overlay in this page. */
.status {
position: relative;
display: flex;
align-items: center;
gap: clamp(14px, 3vw, 40px);
padding: 14px clamp(16px, 2.4vw, 26px) 16px;
overflow: hidden;
background: linear-gradient(var(--plaster-hi), var(--plaster-sunk));
border: 1px solid var(--panel-edge);
border-left: 5px solid var(--tone-idle);
border-radius: 4px;
box-shadow: var(--shadow-panel);
}
.status[data-tone="you"] { border-left-color: var(--tone-you); }
.status[data-tone="ai"] { border-left-color: var(--tone-ai); }
.status[data-tone="scoring"] { border-left-color: var(--tone-scoring); }
.status[data-tone="end"] { border-left-color: var(--tone-end); }
/* browsing the history: the band is the only thing that changes colour, so you
* always know at a glance that what you are looking at is the past */
.status[data-tone="history"] { border-left-color: var(--ink-soft); }
.status-text { flex: 1; min-width: 0; }
.status-headline {
margin: 0;
font-family: var(--font-display);
font-size: clamp(21px, 2.5vw, 31px);
line-height: 1.15;
letter-spacing: .005em;
}
.status-detail {
margin: 4px 0 0;
font-family: var(--font-label);
font-size: 12.5px;
letter-spacing: .08em;
color: var(--ink-soft);
min-height: 15px;
}
.status-score { display: flex; align-items: center; gap: 10px; }
.status-dash { color: var(--ink-faint); font-family: var(--font-display); font-size: 22px; }
.status-side { display: flex; flex-direction: column; align-items: center; min-width: 62px; }
.status-side-score {
font-family: var(--font-display);
font-size: 34px;
line-height: 1;
color: var(--ink-faint);
}
.status-side.leading .status-side-score { color: var(--ink); }
.status-side-name {
font-family: var(--font-label);
font-size: 10px;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--ink-faint);
}
/* the clock: a glaze bar drawn along the foot of the band */
.status-bar {
position: absolute;
inset: auto 0 0 0;
height: 3px;
background: var(--c3-wash);
opacity: 0;
transition: opacity .2s ease;
}
.status.ticking .status-bar { opacity: 1; }
.status-fill {
display: block;
width: 0;
height: 100%;
background: linear-gradient(90deg, var(--c0-hi), var(--c0));
transition: width .12s linear;
}
.status.indeterminate .status-fill {
width: 34% !important;
animation: glaze-sweep-bar 1.1s ease-in-out infinite alternate;
}
@keyframes glaze-sweep-bar {
from { transform: translateX(-100%); }
to { transform: translateX(300%); }
}
/* ==================================================================== tile ==
* A tile is *there*: saturated, rimmed with its own fired edge, and standing a
* little proud of the board. `background-color` carries the glaze on its own so
* that the colour survives even where gradients do not. */
.tile {
position: relative;
width: var(--tile);
height: var(--tile);
padding: 0;
border: 0;
border-radius: var(--radius);
background-color: var(--base);
background-image:
radial-gradient(circle at 31% 24%, rgba(255, 255, 255, .55), rgba(255, 255, 255, 0) 56%),
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, .2) 0 17%, rgba(255, 255, 255, 0) 18%),
conic-gradient(from 45deg at 50% 50%,
rgba(255, 255, 255, .14) 0 25%, rgba(0, 0, 0, .08) 0 50%,
rgba(255, 255, 255, .14) 0 75%, rgba(0, 0, 0, .08) 0),
linear-gradient(158deg, var(--hi), var(--base) 52%, var(--lo));
box-shadow:
0 0 0 1px var(--rim),
0 1px 0 rgba(255, 255, 255, .45) inset,
0 -2px 4px rgba(0, 0, 0, .28) inset,
var(--shadow-tile);
}
.tile[data-color="0"] { --base: var(--c0); --hi: var(--c0-hi); --lo: var(--c0-lo); --rim: var(--c0-rim); }
.tile[data-color="1"] { --base: var(--c1); --hi: var(--c1-hi); --lo: var(--c1-lo); --rim: var(--c1-rim); }
.tile[data-color="2"] { --base: var(--c2); --hi: var(--c2-hi); --lo: var(--c2-lo); --rim: var(--c2-rim); }
.tile[data-color="3"] { --base: var(--c3); --hi: var(--c3-hi); --lo: var(--c3-lo); --rim: var(--c3-rim); }
.tile[data-color="4"] { --base: var(--c4); --hi: var(--c4-hi); --lo: var(--c4-lo); --rim: var(--c4-rim); }
.tile.mini { width: 22px; height: 22px; }
/* a tile that has reached the wall is the most permanent thing on the board */
.tile.placed { box-shadow: 0 0 0 1.5px var(--rim), 0 1px 0 rgba(255, 255, 255, .5) inset, 0 -2px 5px rgba(0, 0, 0, .3) inset, var(--shadow-tile-raised); }
button.tile { cursor: pointer; transition: transform .13s ease, box-shadow .13s ease; }
button.tile:hover {
transform: translateY(-2px);
box-shadow: 0 0 0 1px var(--rim), 0 1px 0 rgba(255, 255, 255, .45) inset,
0 -2px 4px rgba(0, 0, 0, .28) inset, var(--shadow-tile-raised);
}
button.tile.taken { transform: translateY(-4px) scale(1.04); }
button.tile.taken::after {
content: "";
position: absolute;
inset: -4px;
border: 2px solid var(--ink);
border-radius: 6px;
opacity: .85;
}
button.tile.dimmed { opacity: .34; }
button.tile:disabled { cursor: default; }
.marker {
width: var(--tile);
height: var(--tile);
display: grid;
place-items: center;
border-radius: var(--radius);
background: linear-gradient(var(--marker-face), var(--marker-lo));
box-shadow: 0 0 0 1px var(--grout-deep) inset, var(--shadow-tile);
font-family: var(--font-display);
font-size: 16px;
color: var(--ink-soft);
}
.marker.tiny { width: calc(var(--tile) - 4px); height: calc(var(--tile) - 4px); font-size: 12px; }
/* ============================================================= empty slot ==
* A slot is a *hole*: neutral (no glaze, ever), sunk into the board, ringed by
* its own shadow. One rule, one look, everywhere a square is empty. */
.slot, .cell.empty {
width: var(--tile);
height: var(--tile);
border-radius: var(--radius);
background-color: var(--slot);
background-image: linear-gradient(var(--slot-lo), var(--slot) 46%);
box-shadow:
0 0 0 1px var(--slot-rim) inset,
0 3px 5px -1px var(--slot-shade) inset,
0 -1px 0 rgba(255, 255, 255, .3) inset;
}
/* ================================================= middle of the table == */
.factories {
display: flex;
flex-wrap: wrap;
gap: 14px 18px;
justify-content: center;
padding: 4px 0 2px;
}
.factory {
--dish-size: calc(var(--tile) * 2 + 32px);
position: relative;
width: var(--dish-size);
height: var(--dish-size);
display: grid;
grid-template-columns: repeat(2, var(--tile));
grid-auto-rows: var(--tile);
gap: 5px;
place-content: center;
border-radius: 50%;
background: radial-gradient(circle at 38% 30%, var(--dish-hi), var(--dish) 62%, var(--dish-lo));
box-shadow:
0 0 0 1px var(--dish-rim) inset,
0 3px 0 -1px rgba(255, 255, 255, .5) inset,
0 -6px 10px -6px var(--slot-shade) inset,
0 6px 14px -8px rgba(48, 34, 14, .5);
}
.factory:nth-child(odd) { transform: rotate(-1.1deg); }
.factory:nth-child(even) { transform: rotate(1.3deg); }
.factory .fac-id {
position: absolute;
bottom: 3px;
left: 50%;
transform: translateX(-50%);
font-family: var(--font-label);
font-size: 11px;
letter-spacing: .12em;
color: var(--ink-faint);
}
.factory.empty { opacity: .55; }
.factory .empty-note { grid-column: 1 / -1; text-align: center; }
.center-dish {
margin: 14px auto 0;
min-height: calc(var(--tile) + 24px);
max-width: 680px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 6px 10px;
padding: 11px 20px;
border-radius: 999px;
background: radial-gradient(120% 160% at 50% 0%, var(--dish-hi), var(--dish) 60%, var(--dish-lo));
box-shadow:
0 0 0 1px var(--dish-rim) inset,
0 3px 0 -1px rgba(255, 255, 255, .5) inset,
0 -8px 14px -8px var(--slot-shade) inset,
0 8px 18px -12px rgba(48, 34, 14, .5);
}
.center-dish .empty-note, .factory .empty-note {
font-family: var(--font-label);
font-size: 11px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--ink-faint);
}
.group { display: flex; gap: 4px; }
/* the dish a move is drawing from, while it draws */
.factory.picked, .center-dish.picked {
box-shadow:
0 0 0 2px var(--c0) inset,
0 3px 0 -1px rgba(255, 255, 255, .5) inset,
0 0 0 4px var(--c0-wash),
0 6px 14px -8px rgba(48, 34, 14, .55);
}
/* ================================================================= board == */
/* Both boards are this component with `interactive` flipped, so the human's and
* the AI's are the same size, the same rows, the same everything. */
.board {
/* both boards share this: identical size is the point of the twin layout */
--tile: clamp(20px, 2.5vw, 42px);
display: flex;
flex-direction: column;
gap: 10px;
align-items: stretch;
padding: 12px 14px 14px;
background-color: var(--panel-bottom);
background-image: linear-gradient(var(--panel-top), var(--panel-bottom));
border: 1px solid var(--panel-edge);
border-radius: 4px;
box-shadow: var(--shadow-panel);
}
.board.to-move { border-color: var(--grout); box-shadow: var(--shadow-panel), 0 0 0 2px var(--c4-wash); }
.board-head {
display: grid;
grid-template-columns: 1fr auto;
align-items: baseline;
column-gap: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--panel-edge);
}
.board-who {
font-family: var(--font-label);
font-size: 12px;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--ink-soft);
}
.board-score { font-family: var(--font-display); font-size: 30px; line-height: 1; text-align: right; }
.board-who .bot-chip {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 3px;
margin-right: 7px;
vertical-align: -1px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .35), 0 1px 0 rgba(255, 255, 255, .45) inset;
}
.board-score .score-eqn { color: var(--ink-faint); }
.board-notes {
grid-column: 1 / -1;
font-family: var(--font-label);
font-size: 11px;
letter-spacing: .06em;
color: var(--ink-faint);
min-height: 14px;
}
/* The board's spine: pattern line r and wall row r are ONE grid row, exactly as
* on the cardboard, so a full line visibly slides into the row beside it. */
.board-grid {
--row-h: calc(var(--tile) + 6px);
display: grid;
grid-template-columns: minmax(0, 1fr) max-content;
grid-auto-rows: var(--row-h);
column-gap: clamp(8px, 1vw, 18px);
row-gap: 0;
}
.line {
grid-column: 1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
height: 100%;
padding: 0 5px;
border: 1px solid transparent;
border-radius: 3px;
background: none;
text-align: right;
}
button.line { cursor: default; font: inherit; color: inherit; }
button.line.open {
cursor: pointer;
border-color: var(--c4-lo);
background: var(--c4-wash);
box-shadow: 0 0 0 3px var(--c4-wash);
}
button.line.open:hover { background: var(--c4-wash); filter: brightness(1.04); }
button.line.blocked { cursor: not-allowed; opacity: .5; }
.line .row-note {
flex: 1;
min-width: 0;
margin-right: auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: left;
font-family: var(--font-label);
font-size: 11px;
letter-spacing: .04em;
color: var(--ink-faint);
}
.line.open .row-note { color: var(--c4-lo); }
.line.incoming { border-color: var(--c0); background: var(--c0-wash); }
/* One grouted strip per wall row; stacked with no row gap they read as one
* mortared panel, and each strip stays level with its pattern line. */
.wall-row {
grid-column: 2;
display: grid;
grid-template-columns: repeat(5, var(--tile));
align-items: center;
justify-content: center;
gap: 4px;
height: 100%;
padding: 0 5px;
/* the mortar has to be darker than the squares set into it, or the wall reads
* as one slab instead of a grid */
background: var(--grout-deep);
box-shadow: 0 2px 6px -3px var(--slot-shade) inset;
}
.wall-row.top { border-radius: 4px 4px 0 0; }
.wall-row.bottom { border-radius: 0 0 4px 4px; }
/* An empty wall square is a hole with the memory of its glaze in it. The well
* stays neutral (hue means occupied), but sunk into it is a small diamond of
* the colour that belongs there, drawn the way a setter would mark plaster: a
* matte, translucent wash of the glaze (`--cN-ghost`), edged with a fine line
* of the true colour (`--cN-motif`), over a breath of the same pigment settled
* into the recess. Flat, unlit and see-through — everything a glossy raised
* tile is not — so it reads as "this colour goes here", never as "a tile is
* here", and it reads from across the table. */
.cell { width: var(--tile); height: var(--tile); border-radius: var(--radius); }
.cell.empty { position: relative; }
.cell.empty::before {
content: "";
position: absolute;
inset: 0;
border-radius: var(--radius);
background: radial-gradient(circle at 50% 55%, var(--ghost-wash), transparent 72%);
}
.cell.empty::after {
content: "";
position: absolute;
inset: 27%;
transform: rotate(45deg);
border: 2px solid var(--ghost-glaze);
border-radius: 15%;
background: linear-gradient(155deg, var(--ghost-fill), var(--ghost-fill) 45%, var(--ghost-wash));
opacity: var(--slot-motif);
}
.cell.empty[data-color="0"] { --ghost-glaze: var(--c0-motif); --ghost-fill: var(--c0-ghost); --ghost-wash: var(--c0-wash); }
.cell.empty[data-color="1"] { --ghost-glaze: var(--c1-motif); --ghost-fill: var(--c1-ghost); --ghost-wash: var(--c1-wash); }
.cell.empty[data-color="2"] { --ghost-glaze: var(--c2-motif); --ghost-fill: var(--c2-ghost); --ghost-wash: var(--c2-wash); }
.cell.empty[data-color="3"] { --ghost-glaze: var(--c3-motif); --ghost-fill: var(--c3-ghost); --ghost-wash: var(--c3-wash); }
.cell.empty[data-color="4"] { --ghost-glaze: var(--c4-motif); --ghost-fill: var(--c4-ghost); --ghost-wash: var(--c4-wash); }
.floor {
display: flex;
align-items: flex-start;
gap: 4px;
padding: 4px 6px;
border: 1px solid transparent;
border-radius: 3px;
background: none;
}
.floor .cellwrap { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.floor .pen { font-family: var(--font-label); font-size: 10px; letter-spacing: .06em; color: var(--ink-faint); }
button.floor.open {
cursor: pointer;
border-color: var(--c2);
background: var(--c2-wash);
box-shadow: 0 0 0 3px var(--c2-wash);
}
button.floor.open:hover { background: var(--c2-wash); filter: brightness(1.04); }
.floor-wrap { display: flex; flex-direction: column; gap: 4px; }
.floor-note { font-family: var(--font-label); font-size: 11px; color: var(--ink-faint); }
.floor-note.warn { color: var(--c2-ink); }
/* ========================================================= settings panel ==
* A gear and a row of presets, opened *inline*: it pushes the table down rather
* than covering it, because this page has no pop-ups and this is not the place
* to start. */
.settings { display: flex; flex-direction: column; }
.settings-head { display: flex; align-items: center; gap: 10px; }
.gear {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 34px;
padding: 4px 12px 4px 9px;
border: 1px solid var(--grout);
border-radius: 3px;
background: linear-gradient(var(--btn-top), var(--btn-bottom));
cursor: pointer;
font-family: var(--font-label);
font-size: 11.5px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--ink-soft);
}
.gear:hover { background: linear-gradient(var(--btn-top-hover), var(--btn-bottom-hover)); }
.gear .gear-icon { width: 15px; height: 15px; display: block; fill: var(--ink-soft); transition: transform .35s ease; }
.settings.open .gear .gear-icon { transform: rotate(60deg); }
.settings-note { font-family: var(--font-label); font-size: 11.5px; letter-spacing: .05em; color: var(--ink-faint); }
.settings-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 18px;
margin-top: 10px;
padding: 12px 14px;
background: var(--plaster-sunk);
border: 1px solid var(--panel-edge);
border-radius: 4px;
box-shadow: 0 2px 5px -3px var(--slot-shade) inset;
}
.settings-panel[hidden] { display: none; }
.settings-label {
font-family: var(--font-label);
font-size: 11px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--ink-faint);
}
.speeds { display: flex; gap: 0; }
/* `.flag` is the same segmented button in any other row of the panel (score
* pop-ups, future switches) — one look for "pick one of these". */
.speed, .flag {
min-width: 52px;
min-height: 34px;
padding: 5px 12px;
border: 1px solid var(--grout);
border-left-width: 0;
background: var(--field-bg);
cursor: pointer;
font-family: var(--font-label);
font-size: 13px;
letter-spacing: .06em;
color: var(--ink-soft);
}
.speed:first-child, .flag:first-child { border-left-width: 1px; border-radius: 3px 0 0 3px; }
.speed:last-child, .flag:last-child { border-radius: 0 3px 3px 0; }
.speed:hover, .flag:hover { background: var(--btn-bottom-hover); }
.speed[aria-pressed="true"], .flag[aria-pressed="true"] {
color: var(--btn-primary-ink);
border-color: var(--c0-lo);
background: linear-gradient(var(--btn-primary-top), var(--btn-primary-bottom));
}
.settings-gap { flex-basis: 100%; height: 0; margin: 0; }
.settings-hint {
flex-basis: 100%;
margin: 0;
font-family: var(--font-label);
font-size: 11.5px;
letter-spacing: .04em;
color: var(--ink-faint);
}
/* ========================================================= move navigator ==
* Previous / next, and the only place on the page that talks about the past. */
.nav { display: inline-flex; align-items: center; gap: 6px; }
.nav-btn {
min-width: 38px;
min-height: 34px;
padding: 4px 11px;
border: 1px solid var(--grout);
border-radius: 3px;
background: linear-gradient(var(--btn-top), var(--btn-bottom));
cursor: pointer;
font-family: var(--font-label);
font-size: 14px;
color: var(--ink-soft);
}
.nav-btn:hover:not(:disabled) { background: linear-gradient(var(--btn-top-hover), var(--btn-bottom-hover)); }
.nav-btn:disabled { opacity: .4; cursor: not-allowed; }
.nav-count {
min-width: 84px;
text-align: center;
font-family: var(--font-label);
font-size: 11.5px;
letter-spacing: .08em;
color: var(--ink-faint);
}
/* while you are browsing, the board is a record, not a game */
body.viewing .board-grid, body.viewing .factories, body.viewing .center-dish { cursor: default; }
body.viewing .middle-table { opacity: .93; }
/* ============================================================== move log ==
* Newest at the top, so the move you just made is the one you are looking at.
* Every entry, always, is drawn the same: a log is a record, not a headline —
* nothing here is highlighted, including the newest. */
.log {
margin: 0;
padding: 0;
list-style: none;
max-height: 300px;
overflow-y: auto;
}
.log-entry {
display: grid;
grid-template-columns: 44px minmax(0, 1fr);
gap: 10px;
padding: 5px 0;
border-top: 1px solid var(--panel-edge);
font-size: 13.5px;
line-height: 1.4;
color: var(--ink-soft);
}
.log-entry:first-child { border-top: 0; }
.log-tag {
font-family: var(--font-label);
font-size: 10px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--ink-faint);
padding-top: 3px;
}
.log-text { min-width: 0; }
/* the pictographic half: the tiles that moved, drawn as tiles */
.glyphs { display: inline-flex; align-items: center; gap: 2px; vertical-align: -3px; margin-right: 6px; }
.glyph {
width: 15px;
height: 15px;
border-radius: 2px;
background-color: var(--base);
background-image: linear-gradient(150deg, var(--hi), var(--base) 55%, var(--lo));
box-shadow: 0 0 0 1px var(--rim), 0 1px 0 rgba(255, 255, 255, .35) inset;
}
.glyph[data-color="0"] { --base: var(--c0); --hi: var(--c0-hi); --lo: var(--c0-lo); --rim: var(--c0-rim); }
.glyph[data-color="1"] { --base: var(--c1); --hi: var(--c1-hi); --lo: var(--c1-lo); --rim: var(--c1-rim); }
.glyph[data-color="2"] { --base: var(--c2); --hi: var(--c2-hi); --lo: var(--c2-lo); --rim: var(--c2-rim); }
.glyph[data-color="3"] { --base: var(--c3); --hi: var(--c3-hi); --lo: var(--c3-lo); --rim: var(--c3-rim); }
.glyph[data-color="4"] { --base: var(--c4); --hi: var(--c4-hi); --lo: var(--c4-lo); --rim: var(--c4-rim); }
.glyph.marker-glyph {
display: grid;
place-items: center;
background: linear-gradient(var(--marker-face), var(--marker-lo));
box-shadow: 0 0 0 1px var(--grout-deep);
font-family: var(--font-display);
font-size: 10px;
color: var(--ink-soft);
}
.glyph.spill { opacity: .5; }
.log-where { font-family: var(--font-label); font-size: 12.5px; letter-spacing: .04em; }
.log-arrow { color: var(--ink-faint); padding: 0 3px; }
.log-aside { color: var(--ink-faint); font-family: var(--font-label); font-size: 11.5px; }
.coach-chip { display: inline-flex; align-items: baseline; gap: 7px; margin-left: 8px; vertical-align: baseline; }
.coach-delta {
padding: 1px 7px 2px;
border-radius: 2px;
background: var(--plaster-sunk);
box-shadow: 0 0 0 1px var(--grout) inset;
font-family: var(--font-label);
font-size: 11.5px;
letter-spacing: .06em;
color: var(--ink-soft);
white-space: nowrap;
}
.coach-chip[data-grade="best"] .coach-delta { color: var(--grade-best); box-shadow: 0 0 0 1px var(--c0-ink) inset; background: var(--c0-wash); }
.coach-chip[data-grade="slip"] .coach-delta { color: var(--grade-slip); box-shadow: 0 0 0 1px var(--c1-ink) inset; background: var(--c1-wash); }
.coach-chip[data-grade="blunder"] .coach-delta { color: var(--grade-blunder); box-shadow: 0 0 0 1px var(--c2-ink) inset; background: var(--c2-wash); }
.coach-chip[data-grade="pending"] .coach-delta { font-style: italic; }
.coach-why { font-family: var(--font-label); font-size: 11.5px; letter-spacing: .03em; color: var(--ink-faint); }
.coach-legend {
margin: 0 0 8px;
font-family: var(--font-label);
font-size: 11.5px;
letter-spacing: .04em;
color: var(--ink-faint);
}
/* ========================================================= inline scoring == */
.scoring {
padding: 14px 16px 16px;
background: linear-gradient(var(--plaster-hi), var(--plaster-sunk));
border: 1px solid var(--panel-edge);
border-left: 5px solid var(--tone-scoring);
border-radius: 4px;
box-shadow: var(--shadow-panel);
}
.scoring.final { border-left-color: var(--tone-end); }
.scoring[hidden] { display: none; }
.scoring-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px; margin-bottom: 12px; }
.scoring-title { margin: 0; font-family: var(--font-display); font-size: 22px; font-weight: 400; }
.scoring-note { font-family: var(--font-label); font-size: 11.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
.scoring-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(12px, 1.6vw, 20px); }
.score-card { padding: 12px 14px; border-radius: 4px; background: var(--plaster-sunk); box-shadow: 0 0 0 1px var(--grout) inset; }
.score-card-who {
margin: 0 0 6px;
font-family: var(--font-label);
font-size: 11px;
font-weight: 400;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--ink-faint);
}
.score-delta { margin: 0 0 8px; font-family: var(--font-display); font-size: 24px; }
.score-delta.big { font-size: 38px; line-height: 1; }
.score-delta.up { color: var(--gain); }
.score-delta.down { color: var(--loss); }
.score-delta.won { color: var(--c0); }
.score-tiles { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 8px; }
.score-tile { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.score-tile .pts { font-family: var(--font-display); font-size: 14px; }
.score-tile .where { font-family: var(--font-label); font-size: 9.5px; letter-spacing: .06em; color: var(--ink-faint); }
.score-line { margin: 2px 0 0; font-size: 13px; color: var(--ink-soft); }
.bonus-list { display: grid; grid-template-columns: 1fr auto; gap: 3px 12px; margin: 0; font-size: 13px; }
.bonus-list dt { color: var(--ink-soft); }
.bonus-list dd { margin: 0; text-align: right; font-family: var(--font-label); letter-spacing: .04em; }
.bonus-list dt.sum, .bonus-list dd.sum { padding-top: 5px; border-top: 1px solid var(--grout); color: var(--ink); }
/* ============================================================== hand tray ==
* Where a held selection goes while you choose a row: a small dish pinned to
* the corner of the middle panel. Purely a picture of the selection — the real
* tiles are hidden underneath it and come back the moment you change your mind. */
.middle-panel { position: relative; }
.hand {
position: absolute;
top: 8px;
right: 12px;
display: flex;
align-items: center;
gap: 9px;
padding: 6px 10px;
border-radius: 999px;
background: radial-gradient(120% 160% at 50% 0%, var(--dish-hi), var(--dish) 60%, var(--dish-lo));
box-shadow:
0 0 0 1px var(--dish-rim) inset,
0 2px 0 -1px rgba(255, 255, 255, .5) inset,
0 6px 14px -10px rgba(48, 34, 14, .5);
}
.hand[hidden] { display: none; }
.hand-label {
font-family: var(--font-label);
font-size: 10px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--ink-faint);
}
.hand-tiles { display: flex; gap: 4px; }
/* ============================================================= confirm bar ==
* Confirm mode's one voice. It lives *inside* the middle panel's action row:
* while a move is placed, the row's usual controls step aside and the banner
* takes their exact place — centred, same height, so nothing on the page moves
* when it appears or goes. Never a pop-up, never over the board. */
.middle-actions.placing > :not(.confirm-bar) { display: none; }
.confirm-bar {
display: flex;
flex: 1;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 8px 14px;
min-height: 34px; /* the height of the buttons it replaces */
}
.confirm-bar[hidden] { display: none; }
.confirm-title {
display: inline-flex;
align-items: center;
gap: 9px;
font-family: var(--font-display);
font-size: 18px;
font-weight: 400;
color: var(--ink);
white-space: nowrap;
}
/* a small ochre diamond — the attention hue, in the table's own shape */
.confirm-title::before {
content: "";
width: 9px;
height: 9px;
transform: rotate(45deg);
border-radius: 1.5px;
background: var(--tone-scoring);
}
.confirm-detail {
font-family: var(--font-label);
font-size: 12.5px;
letter-spacing: .05em;
color: var(--ink-soft);
}
/* the placed move's own tiles: a breathing ring of the attention hue, the one
* moving thing on a page that is otherwise holding still for your word */
.proposed { position: relative; }
.proposed::after {
content: "";
position: absolute;
inset: -4px;
border: 2px solid var(--tone-scoring);
border-radius: 6px;
box-shadow: 0 0 0 3px var(--c1-wash), 0 0 12px var(--c1-wash);
pointer-events: none;
animation: proposed-pulse 1.1s ease-in-out infinite alternate;
}
@keyframes proposed-pulse {
from { opacity: .5; }
to { opacity: 1; }
}
body[data-anim="off"] .proposed::after { animation: none; opacity: 1; }
/* ============================================================== score pops ==
* Every point, announced where it is earned: "+3" floats off a wall tile as it
* lands, "−2" off a floor line as it bills you. Their layer sits above the
* flights and is separate from them — a number is not a tile. */
.pop-layer { position: fixed; inset: 0; z-index: 30; pointer-events: none; }
.score-pop {
position: fixed;
transform: translate(-50%, -70%);
font-family: var(--font-display);
font-size: 21px;
color: var(--gain);
text-shadow:
0 1px 0 var(--plaster-hi),
0 0 6px var(--plaster-hi);
opacity: 0;
animation: score-rise 1.1s cubic-bezier(.2, .5, .3, 1) both;
}
.score-pop[data-kind="loss"] { color: var(--loss); }
@keyframes score-rise {
0% { opacity: 0; transform: translate(-50%, -40%) scale(.7); }
18% { opacity: 1; transform: translate(-50%, -80%) scale(1.12); }
70% { opacity: 1; }
100% { opacity: 0; transform: translate(-50%, -190%) scale(1); }
}
/* =============================================================== flights == */
.fly-layer { position: fixed; inset: 0; z-index: 25; pointer-events: none; }
/* Straight line, constant speed: a tile is being moved by a hand, not thrown.
* The duration is written onto each clone by animate.js from the speed setting,
* so nothing here — and nothing in the OS — can shorten it behind your back. */
.fly-tile {
position: fixed;
transition-property: transform;
transition-timing-function: cubic-bezier(.36, .62, .38, 1);
will-change: transform;
box-shadow: 0 0 0 1px var(--rim), 0 1px 0 rgba(255, 255, 255, .45) inset, var(--shadow-tile-raised);
}
/* a clone that has arrived: the in-flight lift drops and it sits like a tile */
.fly-tile.landed {
box-shadow:
0 0 0 1px var(--rim),
0 1px 0 rgba(255, 255, 255, .45) inset,
0 -2px 4px rgba(0, 0, 0, .28) inset,
var(--shadow-tile);
}
.fly-tile.marker { display: grid; place-items: center; }
/* a completed line's tile arriving on the wall */
@keyframes glaze-land {
0% { transform: scale(1.3); opacity: 0; }
60% { transform: scale(1); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
.tile.placed.landing { animation: glaze-land var(--land-ms, .42s) ease-out both; }
/* The only motion switch on this page is the one in the settings panel. An OS
* "reduce motion" flag is deliberately NOT honoured here: it used to silence
* every tile flight, which made the game look like it was teleporting tiles and
* left no way to turn the animation back on. */
body[data-anim="off"] .fly-tile,
body[data-anim="off"] .tile.placed.landing {
animation-duration: .001ms !important;
transition-duration: .001ms !important;
}