:root { --ink: #0a0a12; --panel: #1b1b2e; --panel-2: #24243c; --panel-3: #32324f; --fg: #e9e6ff; --fg-dim: #9a97c2; --yellow: #ffd23f; --pink: #ff5da2; --cyan: #4cd4ff; --green: #5fd75f; --red: #ff5555; --orange: #ffa657; --radius: 0px; --mono: 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Consolas, monospace; --pixel: 'Press Start 2P', var(--mono); --sans: 'Segoe UI', system-ui, -apple-system, Roboto, sans-serif; --shadow-off: 4px; } * { box-sizing: border-box; image-rendering: pixelated; } html, body { height: 100%; margin: 0; } body { min-height: 100vh; color: var(--fg); font-family: var(--sans); display: flex; justify-content: center; padding: 14px 16px; overflow-y: auto; background: repeating-linear-gradient(rgba(0,0,0,.18) 0 1px, transparent 1px 3px), radial-gradient(ellipse at top, #232347, #0f0f1e 72%); } #app { width: 100%; max-width: 1400px; height: calc(100vh - 28px); max-height: calc(100vh - 28px); display: flex; flex-direction: column; gap: 10px; } /* ---------- pixel button primitive ---------- */ .btn, .icon-btn, .theme-badge, .cat-card, .theme-card { border-radius: var(--radius); } .btn { border: 3px solid var(--ink); padding: 12px 16px; font-family: var(--pixel); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; cursor: pointer; color: var(--ink); background: var(--fg); box-shadow: var(--shadow-off) var(--shadow-off) 0 var(--ink); transition: transform .08s ease, box-shadow .08s ease, filter .1s ease; } .btn:hover:not(:disabled) { filter: brightness(1.08); transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--ink); } .btn:active:not(:disabled) { transform: translate(var(--shadow-off), var(--shadow-off)); box-shadow: 0 0 0 var(--ink); } .btn:disabled { opacity: .4; cursor: not-allowed; box-shadow: 2px 2px 0 var(--ink); } .btn.small { padding: 7px 10px; font-size: 9px; box-shadow: 2px 2px 0 var(--ink); } .btn.ghost { background: var(--panel-2); color: var(--fg); } .btn.primary { background: var(--cyan); } .btn.reveal { background: linear-gradient(90deg, var(--yellow), var(--orange)); } .btn.reveal:not(:disabled) { animation: revealPulse 1.6s steps(2) infinite; } .btn.big { padding: 16px 22px; font-size: 13px; } .btn.danger { background: var(--red); color: var(--ink); } .btn.danger.armed { background: var(--yellow); } @keyframes revealPulse { 0%, 100% { box-shadow: var(--shadow-off) var(--shadow-off) 0 var(--ink), 0 0 0 0 rgba(255,210,63,.6); } 50% { box-shadow: var(--shadow-off) var(--shadow-off) 0 var(--ink), 0 0 0 6px rgba(255,210,63,0); } } /* ---------- topbar ---------- */ .topbar { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; } .brand { display: flex; align-items: center; gap: 10px; } .brand-swatches { display: flex; gap: 3px; } .brand-swatches i { display: block; width: 8px; height: 20px; border: 2px solid var(--ink); } .brand h1 { font-family: var(--pixel); font-size: 15px; margin: 0; color: var(--fg); text-shadow: 2px 2px 0 var(--ink); } .brand h1 span { color: var(--yellow); } .theme-badge { margin-left: auto; font-family: var(--pixel); font-size: 9px; color: var(--fg); background: var(--panel-2); border: 3px solid var(--ink); padding: 8px 12px; cursor: pointer; box-shadow: 3px 3px 0 var(--ink); transition: transform .08s ease; } .theme-badge:hover { transform: translate(-1px, -1px); } .theme-badge strong { color: var(--orange); } .topbar-actions { display: flex; gap: 8px; flex-shrink: 0; } .icon-btn { width: 38px; height: 38px; border: 3px solid var(--ink); background: var(--panel-2); color: var(--fg); cursor: pointer; font-size: 15px; box-shadow: 3px 3px 0 var(--ink); transition: transform .08s ease; } .icon-btn:hover { transform: translate(-1px, -1px); } .solo-timer-hud { display: flex; align-items: center; gap: 8px; font-family: var(--pixel); font-size: 9px; color: var(--fg); background: var(--panel-2); border: 3px solid var(--ink); padding: 8px 12px; box-shadow: 3px 3px 0 var(--ink); } .solo-timer-hud.hidden { display: none; } .solo-timer-badge { color: var(--cyan); } /* ---------- progress ---------- */ .progress-wrap { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; } .progress-track { flex: 1; height: 16px; background: var(--panel-2); border: 3px solid var(--ink); overflow: hidden; } .progress-fill { height: 100%; width: 0%; background: repeating-linear-gradient(90deg, var(--green) 0 8px, #4bc44b 8px 16px); transition: width .5s steps(12); } #progress-label { font-family: var(--pixel); font-size: 9px; color: var(--fg-dim); white-space: nowrap; } /* ---------- match timer HUD (#11: countdown + time warning) ---------- */ /* A second, visually distinct progress bar from .progress-wrap above — * that one tracks "N/12 token kinds themed"; this one tracks time * remaining in the round, alongside the quit button. Built by * game/timerHud.ts / game/multiplayerMatch.ts into the empty * #match-timer-hud / #match-quit-btn / #match-timer-warning mounts in * index.html. Hidden by default: not mounted into any live view until a * later ticket. */ .match-timer-hud { display: flex; align-items: center; gap: 10px; font-family: var(--pixel); font-size: 9px; color: var(--fg); background: var(--panel-2); border: 3px solid var(--ink); padding: 8px 12px; box-shadow: 3px 3px 0 var(--ink); } #match-timer-hud.hidden { display: none; } .match-hud-row { display: flex; align-items: center; gap: 10px; } #match-quit-btn.hidden { display: none; } .match-timer-badge { color: var(--yellow); flex-shrink: 0; white-space: nowrap; } .match-timer-track { flex: 1; min-width: 80px; height: 12px; background: var(--panel-3); border: 2px solid var(--ink); overflow: hidden; } .match-timer-fill { height: 100%; width: 100%; background: var(--cyan); transition: width .3s linear, background-color .3s linear; } .match-timer-clock { flex-shrink: 0; white-space: nowrap; color: var(--fg-dim); } .match-timer-hud--elapsed .match-timer-clock { color: var(--fg); } .match-timer-warning-toast { position: fixed; top: 68px; left: 50%; transform: translateX(-50%); z-index: 80; pointer-events: none; font-family: var(--pixel); font-size: 10px; color: var(--ink); background: var(--yellow); border: 3px solid var(--ink); box-shadow: 3px 3px 0 var(--ink); padding: 8px 14px; animation: modalIn .2s steps(4); } #match-timer-warning.hidden { display: none; } /* ---------- board layout ---------- */ .board { flex: 1 1 auto; min-height: 0; display: flex; gap: 14px; } .canvas-wrap { position: relative; flex: 1 1 auto; min-height: 0; min-width: 0; background: var(--panel); border: 3px solid var(--ink); box-shadow: 5px 5px 0 var(--ink); padding: 6px; overflow: auto; } #code-canvas { display: block; cursor: default; } .panel-col { width: 330px; flex: 0 0 330px; min-height: 0; display: flex; flex-direction: column; gap: 8px; } .panel { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-content: start; padding: 2px 6px 2px 2px; } .panel-actions { flex: 0 0 auto; display: flex; flex-direction: column; gap: 8px; } .panel-actions .btn { width: 100%; } /* ---------- split view (in-match opponent progress pane) ---------- */ .split-view { flex: 1 1 auto; min-height: 0; display: flex; gap: 14px; } .split-view.hidden { display: none; } .split-pane { flex: 1 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 6px; } .split-pane-label { font-family: var(--pixel); font-size: 10px; letter-spacing: .5px; color: var(--fg-dim); flex: 0 0 auto; } .split-canvas-wrap { flex: 1 1 auto; } .split-canvas { display: block; } .cat-card { background: var(--panel-2); border: 3px solid var(--ink); padding: 8px; display: flex; align-items: center; gap: 8px; cursor: pointer; box-shadow: 3px 3px 0 var(--ink); transition: transform .08s ease, box-shadow .08s ease; position: relative; min-width: 0; } .cat-card:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 var(--ink); } .cat-card.active { border-color: var(--yellow); box-shadow: 3px 3px 0 var(--yellow); } .cat-card.assigned::after { content: '✓'; position: absolute; top: 2px; right: 4px; font-size: 9px; color: var(--green); font-family: var(--pixel); } .cat-swatch { width: 24px; height: 24px; border: 2px solid var(--ink); flex-shrink: 0; background: repeating-conic-gradient(#4b4540 0% 25%, #302f2a 0% 50%) 0 0/8px 8px; transition: transform .15s ease; } .cat-swatch.pop { animation: pop .4s steps(4); } @keyframes pop { 0% { transform: scale(1); } 40% { transform: scale(1.3); } 100% { transform: scale(1); } } .cat-info { min-width: 0; } .cat-label { font-size: 8px; line-height: 1.5; font-family: var(--pixel); color: var(--fg); white-space: normal; display: block; margin-bottom: 4px; } .cat-count { font-size: 10px; color: var(--fg-dim); } /* ---------- popover ---------- */ .popover { position: fixed; z-index: 50; background: var(--panel-2); border: 3px solid var(--ink); padding: 12px; box-shadow: 5px 5px 0 var(--ink); width: 220px; animation: popIn .15s steps(3); } .popover.hidden { display: none; } @keyframes popIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } .popover-label { font-family: var(--pixel); font-size: 10px; color: var(--yellow); margin-bottom: 10px; text-transform: uppercase; } .popover-row { display: flex; gap: 8px; margin-bottom: 10px; } .popover-row input[type="color"] { width: 40px; height: 34px; border: 2px solid var(--ink); background: none; cursor: pointer; padding: 0; } .popover-row input[type="text"] { flex: 1; min-width: 0; background: var(--panel); border: 2px solid var(--ink); color: var(--fg); font-family: var(--mono); padding: 0 8px; font-size: 13px; } .popover-actions { display: flex; justify-content: flex-end; gap: 6px; } /* ---------- modals ---------- */ .modal-overlay { position: fixed; inset: 0; background: rgba(6,6,14,.65); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 20px; } .modal-overlay.hidden { display: none; } .modal { background: var(--panel); border: 4px solid var(--ink); box-shadow: 8px 8px 0 var(--ink); padding: 26px 28px; max-width: 480px; width: 100%; max-height: 90vh; overflow: auto; animation: modalIn .2s steps(4); } @keyframes modalIn { from { opacity: 0; transform: scale(.9) translateY(14px); } to { opacity: 1; transform: scale(1) translateY(0); } } .modal h2 { margin-top: 0; font-family: var(--pixel); font-size: 16px; text-shadow: 2px 2px 0 var(--ink); color: var(--yellow); } .modal ol { padding-left: 20px; line-height: 1.7; color: var(--fg-dim); font-size: 13px; } .modal p { color: var(--fg-dim); font-size: 13px; } .pick-theme-label { font-family: var(--pixel); font-size: 9px; color: var(--cyan); text-align: center; margin: 18px 0 10px; } .theme-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 16px; } .theme-card { background: var(--card-bg, var(--panel-2)); border: 3px solid var(--ink); padding: 10px; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 6px; box-shadow: 3px 3px 0 var(--ink); transition: transform .08s ease; } .theme-card:hover { transform: translate(-1px, -1px); } .theme-card.selected { border-color: var(--yellow); box-shadow: 3px 3px 0 var(--yellow); } .theme-swatches { display: flex; gap: 3px; } .theme-swatches i { width: 12px; height: 12px; border: 1px solid rgba(0,0,0,.5); display: block; } .theme-name { font-family: var(--pixel); font-size: 8px; color: var(--fg); text-shadow: 1px 1px 0 var(--ink); text-align: center; } /* ---------- main menu (Solo / Private / Matchmaking) ---------- */ .menu-modal { max-width: 760px; } .menu-sections { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin: 18px 0 4px; } .menu-section { background: var(--panel-2); border: 3px solid var(--ink); box-shadow: 4px 4px 0 var(--ink); padding: 14px; display: flex; flex-direction: column; gap: 10px; } .menu-section h3 { margin: 0; font-family: var(--pixel); font-size: 11px; text-shadow: 1px 1px 0 var(--ink); } #menu-solo h3 { color: var(--cyan); } #menu-private h3 { color: var(--pink); } #menu-matchmaking h3 { color: var(--green); } .menu-section-desc { margin: 0; font-size: 11px; line-height: 1.5; color: var(--fg-dim); } .menu-section-actions { display: flex; flex-direction: column; gap: 8px; margin-top: auto; } .menu-section-actions .btn { width: 100%; } /* Standalone matchmaking-ban countdown banner (frontend/src/ui/banBanner.ts). * Not mounted anywhere yet — ready for ticket #14 to mount into * #menu-matchmaking once queue:banned is wired up. */ .ban-banner { display: flex; align-items: center; gap: 8px; font-family: var(--pixel); font-size: 10px; color: var(--ink); background: var(--orange); border: 3px solid var(--ink); box-shadow: 3px 3px 0 var(--ink); padding: 8px 10px; } .ban-banner-icon { flex-shrink: 0; } #menu-solo .theme-grid { margin-bottom: 0; } .solo-config-group { display: flex; flex-direction: column; gap: 6px; } .solo-config-label { margin: 0; font-family: var(--pixel); font-size: 9px; color: var(--fg-dim); letter-spacing: .5px; } .solo-radio-row { display: flex; gap: 6px; flex-wrap: wrap; } .solo-radio { display: flex; align-items: center; justify-content: center; gap: 5px; flex: 1 1 auto; background: var(--panel); border: 2px solid var(--ink); padding: 6px 8px; font-size: 10px; color: var(--fg-dim); cursor: pointer; } .solo-radio:has(input:checked) { background: var(--panel-3); color: var(--fg); border-color: var(--yellow); } .solo-radio input { accent-color: var(--yellow); margin: 0; } #solo-theme-picker.hidden, #solo-bot-difficulty.hidden { display: none; } @media (max-width: 640px) { .menu-sections { grid-template-columns: 1fr; } } /* ---------- theme preview countdown ---------- */ .preview-box { background: var(--panel); border: 4px solid var(--ink); box-shadow: 8px 8px 0 var(--ink); padding: 26px 30px; max-width: 92vw; text-align: center; animation: modalIn .2s steps(4); } .preview-countdown { font-family: var(--pixel); font-size: 56px; line-height: 1; color: var(--yellow); text-shadow: 4px 4px 0 var(--ink); margin-bottom: 10px; } .preview-countdown.tick { animation: countdownPulse .45s steps(6); } @keyframes countdownPulse { 0% { transform: scale(1.7); color: var(--pink); } 100% { transform: scale(1); color: var(--yellow); } } .preview-caption { font-family: var(--pixel); font-size: 10px; color: var(--cyan); margin-bottom: 18px; letter-spacing: .05em; } .preview-canvas-wrap { display: inline-block; border: 3px solid var(--ink); box-shadow: 4px 4px 0 var(--ink); padding: 4px; max-width: 100%; max-height: 60vh; overflow: auto; } #preview-canvas { display: block; } /* ---------- score meter ---------- */ .score-meter-wrap { text-align: center; margin: 10px 0 4px; } .score-meter-number { font-family: var(--pixel); font-size: 34px; color: var(--yellow); text-shadow: 3px 3px 0 var(--ink); margin-bottom: 10px; } .score-meter-track { display: flex; gap: 3px; justify-content: center; flex-wrap: wrap; } .meter-block { width: 14px; height: 20px; background: var(--panel-2); border: 2px solid var(--ink); } .meter-block.lit { background: var(--lit-color, var(--green)); } .verdict { text-align: center; font-family: var(--pixel); font-size: 12px; margin-bottom: 16px; min-height: 16px; color: var(--fg); } .breakdown { display: flex; flex-direction: column; gap: 7px; max-height: 240px; overflow-y: auto; padding-right: 4px; } .breakdown-row { display: flex; align-items: center; gap: 10px; opacity: 0; animation: rowIn .3s steps(4) forwards; } @keyframes rowIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } } .swatch-pair { display: flex; flex-shrink: 0; } .swatch-pair span { width: 14px; height: 14px; display: block; border: 2px solid var(--ink); } .swatch-pair span + span { margin-left: 2px; } .breakdown-info { flex: 1; min-width: 0; } .breakdown-label { font-size: 10px; color: var(--fg-dim); margin-bottom: 3px; } .breakdown-bar { height: 6px; background: var(--panel-2); border: 1px solid var(--ink); overflow: hidden; } .breakdown-bar-fill { height: 100%; width: 0%; transition: width .6s steps(10); } .breakdown-pct { font-size: 11px; font-weight: 700; width: 34px; text-align: right; color: var(--fg); } .compare-toggle { display: flex; margin: 16px 0 6px; background: var(--panel-2); border: 2px solid var(--ink); padding: 3px; gap: 3px; } .toggle-btn { flex: 1; border: none; background: transparent; color: var(--fg-dim); padding: 8px 6px; font-family: var(--pixel); font-size: 8px; cursor: pointer; } .toggle-btn.active { background: var(--panel-3); color: var(--fg); } /* ---------- multiplayer scoreboard (#12: two-column round reveal) ---------- */ .mp-scoreboard.hidden { display: none; } .mp-winner-banner { text-align: center; font-family: var(--pixel); font-size: 13px; padding: 10px; margin-bottom: 14px; border: 3px solid var(--ink); background: var(--panel-2); color: var(--fg); } .mp-winner-banner.mp-winner-you { background: var(--green); color: var(--ink); } .mp-winner-banner.mp-winner-opponent { background: var(--red); color: var(--ink); } .mp-winner-banner.mp-draw { background: var(--yellow); color: var(--ink); } .mp-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } .mp-column { border: 3px solid var(--ink); background: var(--panel-2); padding: 12px 10px; min-width: 0; } .mp-column-label { text-align: center; font-family: var(--pixel); font-size: 10px; color: var(--fg-dim); margin-bottom: 8px; letter-spacing: .5px; } .mp-column .score-meter-number { font-size: 20px; } .mp-column .score-meter-track { gap: 2px; } .mp-column .meter-block { width: 8px; height: 14px; } .mp-column .breakdown { max-height: 200px; } @media (max-width: 640px) { .mp-columns { grid-template-columns: 1fr; } } .modal-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; } .modal-actions .btn { flex: 1 1 auto; min-width: 110px; } @media (max-width: 900px) { body { height: auto; overflow-y: auto; } #app { height: auto; max-height: none; } .board { flex-direction: column; } .canvas-wrap { max-height: 60vh; } .panel-col { width: 100%; flex: 0 0 auto; } .panel { grid-template-columns: repeat(3, 1fr); max-height: 40vh; } .theme-grid { grid-template-columns: repeat(2, 1fr); } }