@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; }