/* 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; , 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; }