From 125bb6c367c6eb991e859008199c84e0f28afcdf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 18:39:00 +0000
Subject: [PATCH 1/4] Initial plan
From 796f377e77edc0f469503e035c33979dbee8e771 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 18:46:55 +0000
Subject: [PATCH 2/4] feat: add Digital Jass Scoreboard (Schiefertafel Z/Z) app
Co-authored-by: LFDave <36726874+LFDave@users.noreply.github.com>
---
README.md | 14 +
jass-scoreboard/README.md | 74 +++++
jass-scoreboard/css/styles.css | 534 +++++++++++++++++++++++++++++++++
jass-scoreboard/index.html | 124 ++++++++
jass-scoreboard/js/app.js | 35 +++
jass-scoreboard/js/renderer.js | 114 +++++++
jass-scoreboard/js/scoring.js | 55 ++++
jass-scoreboard/js/state.js | 63 ++++
jass-scoreboard/js/storage.js | 41 +++
jass-scoreboard/js/ui.js | 169 +++++++++++
10 files changed, 1223 insertions(+)
create mode 100644 jass-scoreboard/README.md
create mode 100644 jass-scoreboard/css/styles.css
create mode 100644 jass-scoreboard/index.html
create mode 100644 jass-scoreboard/js/app.js
create mode 100644 jass-scoreboard/js/renderer.js
create mode 100644 jass-scoreboard/js/scoring.js
create mode 100644 jass-scoreboard/js/state.js
create mode 100644 jass-scoreboard/js/storage.js
create mode 100644 jass-scoreboard/js/ui.js
diff --git a/README.md b/README.md
index 2dd86b3..d0701ae 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,20 @@ small apps for various use cases
## Apps Available
+### π Jass Scoreboard (Schiefertafel Z/Z)
+A browser-based digital Jass scoreboard that replicates a traditional Swiss chalk slate board with the classic Z/Z layout.
+
+**Features:**
+- Classic Z/Z slate board look with chalk tally marks (e.g. `||||\ ||||\ |`)
+- Two teams, editable names and target score (default 2500)
+- Win detection with animated overlay (gold glow, chalk particles)
+- Undo last entry, reset game, flip board orientation
+- Game state persisted to localStorage β resume on page reload
+- Responsive: works on mobile (320px+), tablet, and desktop
+- No external frameworks β pure HTML, CSS, Vanilla JS
+
+**Play:** [jass-scoreboard/index.html](jass-scoreboard/index.html)
+
### π GeoTriad - Geography Quiz Game
A fun, educational geography quiz game for kids around 10 years old!
diff --git a/jass-scoreboard/README.md b/jass-scoreboard/README.md
new file mode 100644
index 0000000..87e9b6b
--- /dev/null
+++ b/jass-scoreboard/README.md
@@ -0,0 +1,74 @@
+# Jass Scoreboard β Schiefertafel Z/Z
+
+A browser-based digital Jass scoreboard that replicates the look and feel of a traditional Swiss chalk slate board (Jasstafeln) with the classic Z/Z layout.
+
+## Features
+
+- π― **Z/Z Board Layout** β Visual division of two mirrored team areas with red chalk guide lines
+- π **Tally Mark Display** β Points shown as chalk tally marks (`||||\ ||||\ |`) instead of numbers
+- π’ **Numeric Totals** β Running totals displayed at the bottom of each panel
+- π **Win Detection** β Animated win overlay when a team exceeds the target score
+- β© **Undo** β Remove the last recorded entry
+- π **Reset** β Clear all scores while keeping team names and target score
+- β **Flip Board** β Swap panel positions for players on opposite sides of the table
+- πΎ **Persistence** β Game state saved to `localStorage` and restored on page reload
+- π± **Responsive** β Works on mobile (320px+), tablet, and desktop
+
+## Usage
+
+1. Open `index.html` in any modern browser (requires ES module support)
+2. Edit team names by clicking on the name fields at the top
+3. Set your target score (default: 2500)
+4. Select a team, enter points (1β500), and click **Eintragen**
+5. Use **β© RΓΌckgΓ€ngig** to undo the last entry
+6. Use **π Neu** to reset the game
+7. Use **β** to flip the board orientation
+
+## Project Structure
+
+```
+jass-scoreboard/
+βββ index.html # Root HTML structure
+βββ css/
+β βββ styles.css # Chalk board styling, Z/Z grid, animations
+βββ js/
+β βββ app.js # Application bootstrap
+β βββ state.js # Global state definition and mutations
+β βββ storage.js # localStorage persistence
+β βββ scoring.js # Score entry, totals, win check
+β βββ renderer.js # Board, tallies, totals, win state rendering
+β βββ ui.js # Event binding and input validation
+βββ assets/
+β βββ textures/ # (placeholder for optional slate texture)
+β βββ fonts/ # (placeholder for optional chalk fonts)
+βββ README.md
+```
+
+## Technologies
+
+- HTML5
+- CSS3
+- Vanilla JavaScript (ES6 modules)
+
+No external frameworks or libraries are used.
+
+## Game Rules
+
+- Two teams record scores round by round
+- A team wins when its total **exceeds** the target score
+- Default target: **2500 points**
+- Valid point entries: integers from 1 to 500
+- After a win, score entry is disabled but undo remains available
+
+## Tally Mark Rendering
+
+Points are displayed as tally marks:
+
+| Points | Display |
+|--------|-----------------------|
+| 3 | `\| \| \|` |
+| 5 | `\|\|\|\|\ ` |
+| 7 | `\|\|\|\|\ \| \|` |
+| 10 | `\|\|\|\|\ \|\|\|\|\ ` |
+
+Groups of 5 are rendered as `||||\ ` and remainders as `| `.
diff --git a/jass-scoreboard/css/styles.css b/jass-scoreboard/css/styles.css
new file mode 100644
index 0000000..5b9d632
--- /dev/null
+++ b/jass-scoreboard/css/styles.css
@@ -0,0 +1,534 @@
+/* styles.css β Jass Scoreboard chalk slate styling */
+
+/* βββ Reset & Base ββββββββββββββββββββββββββββββββββββββββββββββ */
+*, *::before, *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+:root {
+ --slate-bg: #1a2a1a;
+ --slate-surface: #1e321e;
+ --chalk-white: #f0ede0;
+ --chalk-dim: #c8c4b0;
+ --chalk-red: #e05050;
+ --panel-border: rgba(255, 255, 255, 0.12);
+ --input-bg: #243224;
+ --input-border: rgba(240, 237, 224, 0.3);
+ --btn-primary: #4a7c59;
+ --btn-primary-h: #5a9c6e;
+ --btn-secondary: #3a5a4a;
+ --btn-danger: #7a3535;
+ --btn-danger-h: #9a4545;
+ --winner-glow: #ffd700;
+ --font-main: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ --radius: 6px;
+}
+
+html, body {
+ height: 100%;
+}
+
+body {
+ background-color: var(--slate-bg);
+ background-image:
+ radial-gradient(ellipse at 20% 30%, rgba(255,255,255,0.02) 0%, transparent 60%),
+ radial-gradient(ellipse at 80% 70%, rgba(255,255,255,0.015) 0%, transparent 60%);
+ color: var(--chalk-white);
+ font-family: var(--font-main);
+ min-height: 100vh;
+ min-width: 320px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+/* βββ Screen-reader only utility βββββββββββββββββββββββββββββββ */
+.sr-only {
+ position: absolute;
+ width: 1px; height: 1px;
+ padding: 0; margin: -1px;
+ overflow: hidden;
+ clip: rect(0,0,0,0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* βββ App wrapper βββββββββββββββββββββββββββββββββββββββββββββββ */
+.app-wrapper {
+ width: 100%;
+ max-width: 760px;
+ padding: 12px 12px 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+/* βββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+.app-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 8px 4px;
+ border-bottom: 2px solid var(--chalk-red);
+}
+
+.header-title {
+ font-size: 1.6rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ color: var(--chalk-white);
+ text-shadow: 0 0 12px rgba(240,237,224,0.35);
+}
+
+.header-settings {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.setting-label {
+ font-size: 0.85rem;
+ color: var(--chalk-dim);
+}
+
+.setting-input {
+ width: 80px;
+ padding: 4px 8px;
+ background: var(--input-bg);
+ border: 1px solid var(--input-border);
+ border-radius: var(--radius);
+ color: var(--chalk-white);
+ font-size: 0.9rem;
+ text-align: center;
+}
+
+.setting-input:focus {
+ outline: 2px solid var(--chalk-dim);
+ outline-offset: 1px;
+}
+
+/* βββ Name editors ββββββββββββββββββββββββββββββββββββββββββββββ */
+.name-editors {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.name-editor-group {
+ flex: 1;
+}
+
+.team-name-input {
+ width: 100%;
+ padding: 6px 10px;
+ background: transparent;
+ border: none;
+ border-bottom: 1px dashed rgba(240,237,224,0.3);
+ color: var(--chalk-white);
+ font-size: 1rem;
+ font-weight: 600;
+ text-align: center;
+ letter-spacing: 0.04em;
+ transition: border-color 0.2s;
+}
+
+.team-name-input:focus {
+ outline: none;
+ border-bottom-color: var(--chalk-white);
+}
+
+.name-editor-divider {
+ color: var(--chalk-dim);
+ font-size: 0.8rem;
+ flex-shrink: 0;
+}
+
+/* βββ Board (Z/Z grid) ββββββββββββββββββββββββββββββββββββββββββ */
+.board {
+ display: grid;
+ grid-template-columns: 1fr 60px 1fr;
+ min-height: 340px;
+ background: var(--slate-surface);
+ border: 1px solid var(--panel-border);
+ border-radius: var(--radius);
+ overflow: hidden;
+ /* Subtle noise texture overlay via box-shadow */
+ box-shadow:
+ inset 0 0 60px rgba(0,0,0,0.35),
+ 0 4px 20px rgba(0,0,0,0.5);
+}
+
+/* βββ Board panels ββββββββββββββββββββββββββββββββββββββββββββββ */
+.board-panel {
+ padding: 16px 14px 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 0;
+ transition: box-shadow 0.4s ease;
+}
+
+.panel-left {
+ border-right: none;
+}
+
+.panel-right {
+ border-left: none;
+}
+
+/* Winner highlight */
+.winner-panel {
+ box-shadow:
+ inset 0 0 40px rgba(255, 215, 0, 0.18),
+ inset 0 0 10px rgba(255, 215, 0, 0.08);
+ animation: panelPulse 1.6s ease-in-out infinite alternate;
+}
+
+@keyframes panelPulse {
+ from { box-shadow: inset 0 0 40px rgba(255,215,0,0.12), inset 0 0 10px rgba(255,215,0,0.06); }
+ to { box-shadow: inset 0 0 60px rgba(255,215,0,0.28), inset 0 0 20px rgba(255,215,0,0.14); }
+}
+
+/* βββ Chalk text ββββββββββββββββββββββββββββββββββββββββββββββββ */
+.chalk-text {
+ color: var(--chalk-white);
+ text-rendering: geometricPrecision;
+}
+
+/* βββ Team name inside panel βββββββββββββββββββββββββββββββββββ */
+.panel-team-name {
+ font-size: 1.1rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-align: center;
+ padding-bottom: 6px;
+ border-bottom: 1px solid rgba(224, 80, 80, 0.45);
+ text-shadow: 0 0 8px rgba(240,237,224,0.3);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* βββ Entries area (tally marks) βββββββββββββββββββββββββββββββ */
+.entries-area {
+ flex: 1;
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 0.95rem;
+ line-height: 1.7;
+ letter-spacing: 0.05em;
+ overflow-y: auto;
+ word-break: break-word;
+ min-height: 120px;
+ padding: 4px 2px;
+ color: var(--chalk-dim);
+}
+
+.tally-row {
+ padding: 1px 0;
+ white-space: nowrap;
+}
+
+.no-entries {
+ color: rgba(240,237,224,0.2);
+ font-size: 1.2rem;
+}
+
+/* βββ Total row βββββββββββββββββββββββββββββββββββββββββββββββββ */
+.panel-total-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 8px;
+ border-top: 1px solid rgba(224, 80, 80, 0.45);
+}
+
+.total-label {
+ font-size: 0.8rem;
+ color: var(--chalk-dim);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.total-value {
+ font-size: 1.4rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-shadow: 0 0 10px rgba(240,237,224,0.4);
+}
+
+/* βββ Z/Z Divider βββββββββββββββββββββββββββββββββββββββββββββββ */
+.board-divider {
+ display: flex;
+ align-items: stretch;
+ position: relative;
+}
+
+.z-divider-svg {
+ width: 100%;
+ height: 100%;
+}
+
+/* βββ Score input area ββββββββββββββββββββββββββββββββββββββββββ */
+.score-input-area {
+ background: var(--input-bg);
+ border: 1px solid var(--panel-border);
+ border-radius: var(--radius);
+ padding: 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.score-input-area.disabled {
+ opacity: 0.55;
+ pointer-events: none;
+}
+
+.input-row {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.select-team {
+ flex: 1;
+ min-width: 100px;
+ padding: 10px 10px;
+ background: var(--slate-bg);
+ border: 1px solid var(--input-border);
+ border-radius: var(--radius);
+ color: var(--chalk-white);
+ font-size: 0.95rem;
+ cursor: pointer;
+}
+
+.select-team:focus {
+ outline: 2px solid var(--chalk-dim);
+ outline-offset: 1px;
+}
+
+.points-input {
+ flex: 2;
+ min-width: 80px;
+ padding: 10px 12px;
+ background: var(--slate-bg);
+ border: 1px solid var(--input-border);
+ border-radius: var(--radius);
+ color: var(--chalk-white);
+ font-size: 1rem;
+ text-align: center;
+ transition: border-color 0.2s;
+}
+
+.points-input:focus {
+ outline: none;
+ border-color: var(--chalk-white);
+}
+
+/* βββ Buttons βββββββββββββββββββββββββββββββββββββββββββββββββββ */
+.btn {
+ padding: 10px 18px;
+ border: none;
+ border-radius: var(--radius);
+ font-size: 0.95rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.18s, transform 0.1s;
+ white-space: nowrap;
+}
+
+.btn:active {
+ transform: scale(0.97);
+}
+
+.btn:focus-visible {
+ outline: 2px solid var(--chalk-white);
+ outline-offset: 2px;
+}
+
+.btn-primary {
+ background: var(--btn-primary);
+ color: #fff;
+ flex-shrink: 0;
+}
+
+.btn-primary:hover {
+ background: var(--btn-primary-h);
+}
+
+.btn-secondary {
+ background: var(--btn-secondary);
+ color: var(--chalk-white);
+}
+
+.btn-secondary:hover {
+ background: #4a7a6a;
+}
+
+.btn-danger {
+ background: var(--btn-danger);
+ color: var(--chalk-white);
+}
+
+.btn-danger:hover {
+ background: var(--btn-danger-h);
+}
+
+.btn-icon {
+ background: transparent;
+ border: 1px solid var(--input-border);
+ color: var(--chalk-white);
+ padding: 6px 12px;
+ font-size: 1.1rem;
+}
+
+.btn-icon:hover {
+ background: rgba(255,255,255,0.08);
+}
+
+/* βββ Action row ββββββββββββββββββββββββββββββββββββββββββββββββ */
+.action-row {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+}
+
+/* βββ Error message βββββββββββββββββββββββββββββββββββββββββββββ */
+.error-msg {
+ min-height: 1.2em;
+ font-size: 0.85rem;
+ color: #ff8080;
+ opacity: 0;
+ transition: opacity 0.25s;
+ text-align: center;
+}
+
+.error-msg.visible {
+ opacity: 1;
+}
+
+/* βββ Win overlay βββββββββββββββββββββββββββββββββββββββββββββββ */
+#win-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(10, 25, 10, 0.88);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 100;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.4s ease;
+ backdrop-filter: blur(3px);
+}
+
+#win-overlay.active {
+ opacity: 1;
+ pointer-events: all;
+}
+
+.win-content {
+ text-align: center;
+ animation: winPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
+}
+
+@keyframes winPop {
+ from { transform: scale(0.5); opacity: 0; }
+ to { transform: scale(1); opacity: 1; }
+}
+
+.win-icon {
+ font-size: 4rem;
+ margin-bottom: 12px;
+ animation: winSpin 0.8s ease-out;
+}
+
+@keyframes winSpin {
+ from { transform: rotate(-20deg) scale(0.6); }
+ to { transform: rotate(0deg) scale(1); }
+}
+
+#win-overlay h2 {
+ font-size: 2.8rem;
+ color: var(--winner-glow);
+ text-shadow: 0 0 24px rgba(255,215,0,0.7);
+ margin-bottom: 10px;
+ letter-spacing: 0.1em;
+}
+
+.win-team {
+ font-size: 1.8rem;
+ color: var(--chalk-white);
+ text-shadow: 0 0 16px rgba(240,237,224,0.6);
+ margin-bottom: 16px;
+}
+
+.win-hint {
+ font-size: 0.8rem;
+ color: var(--chalk-dim);
+ opacity: 0.7;
+ margin-top: 18px;
+}
+
+/* βββ Chalk dust particles ββββββββββββββββββββββββββββββββββββββ */
+.chalk-particles {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ overflow: hidden;
+}
+
+.chalk-particles span {
+ position: absolute;
+ display: block;
+ width: 6px;
+ height: 6px;
+ background: rgba(240, 237, 224, 0.6);
+ border-radius: 50%;
+ animation: float 3s ease-in-out infinite;
+}
+
+.chalk-particles span:nth-child(1) { left: 10%; top: 20%; animation-delay: 0s; animation-duration: 2.8s; width: 4px; height: 4px; }
+.chalk-particles span:nth-child(2) { left: 25%; top: 60%; animation-delay: 0.4s; animation-duration: 3.2s; }
+.chalk-particles span:nth-child(3) { left: 45%; top: 15%; animation-delay: 0.8s; animation-duration: 2.5s; width: 8px; height: 8px; }
+.chalk-particles span:nth-child(4) { left: 65%; top: 70%; animation-delay: 1.2s; animation-duration: 3.6s; width: 5px; height: 5px; }
+.chalk-particles span:nth-child(5) { left: 80%; top: 30%; animation-delay: 0.2s; animation-duration: 2.9s; }
+.chalk-particles span:nth-child(6) { left: 90%; top: 80%; animation-delay: 1.6s; animation-duration: 3.4s; width: 3px; height: 3px; }
+.chalk-particles span:nth-child(7) { left: 55%; top: 45%; animation-delay: 0.6s; animation-duration: 2.6s; width: 7px; height: 7px; }
+.chalk-particles span:nth-child(8) { left: 35%; top: 85%; animation-delay: 1.0s; animation-duration: 3.0s; }
+.chalk-particles span:nth-child(9) { left: 72%; top: 10%; animation-delay: 1.4s; animation-duration: 2.7s; width: 5px; height: 5px; }
+
+@keyframes float {
+ 0% { transform: translateY(0) rotate(0deg); opacity: 0.8; }
+ 50% { transform: translateY(-30px) rotate(180deg); opacity: 0.4; }
+ 100% { transform: translateY(-60px) rotate(360deg); opacity: 0; }
+}
+
+/* βββ Responsive ββββββββββββββββββββββββββββββββββββββββββββββββ */
+@media (max-width: 480px) {
+ .app-wrapper { padding: 8px 8px 20px; }
+ .header-title { font-size: 1.3rem; }
+ .board { grid-template-columns: 1fr 44px 1fr; }
+ .panel-team-name { font-size: 0.95rem; }
+ .entries-area { font-size: 0.82rem; }
+ .total-value { font-size: 1.15rem; }
+ .input-row { flex-direction: column; }
+ .select-team, .points-input, .btn-primary { width: 100%; }
+ #win-overlay h2 { font-size: 2rem; }
+ .win-team { font-size: 1.3rem; }
+}
+
+@media (min-width: 600px) {
+ .board { min-height: 400px; }
+ .entries-area { font-size: 1rem; }
+}
+
+/* βββ Scrollbar styling βββββββββββββββββββββββββββββββββββββββββ */
+.entries-area::-webkit-scrollbar { width: 4px; }
+.entries-area::-webkit-scrollbar-track { background: transparent; }
+.entries-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 4px; }
diff --git a/jass-scoreboard/index.html b/jass-scoreboard/index.html
new file mode 100644
index 0000000..250f723
--- /dev/null
+++ b/jass-scoreboard/index.html
@@ -0,0 +1,124 @@
+
+
+
+
+
+ Jass Scoreboard β Schiefertafel Z/Z
+
+
+
+
+
+
+
π
+
Gewonnen!
+
+
Tippe zum Schliessen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
vs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jass-scoreboard/js/app.js b/jass-scoreboard/js/app.js
new file mode 100644
index 0000000..e83dc97
--- /dev/null
+++ b/jass-scoreboard/js/app.js
@@ -0,0 +1,35 @@
+// app.js β application bootstrap
+
+import { getState, setState } from "./state.js";
+import { loadState } from "./storage.js";
+import { recalcTotals, checkWinner } from "./scoring.js";
+import { render } from "./renderer.js";
+import { bindEvents, initInputValues } from "./ui.js";
+
+function bootstrap() {
+ // Attempt to restore persisted state
+ const saved = loadState();
+ if (saved) {
+ // Merge saved data into state carefully
+ const current = getState();
+ const restored = {
+ teams: saved.teams || current.teams,
+ entries: saved.entries || [],
+ totals: { A: 0, B: 0 },
+ targetScore:
+ typeof saved.targetScore === "number" ? saved.targetScore : current.targetScore,
+ flipped: typeof saved.flipped === "boolean" ? saved.flipped : false,
+ winner: saved.winner || null,
+ gameFinished: typeof saved.gameFinished === "boolean" ? saved.gameFinished : false
+ };
+ setState(restored);
+ recalcTotals();
+ checkWinner();
+ }
+
+ initInputValues();
+ bindEvents();
+ render();
+}
+
+document.addEventListener("DOMContentLoaded", bootstrap);
diff --git a/jass-scoreboard/js/renderer.js b/jass-scoreboard/js/renderer.js
new file mode 100644
index 0000000..de79c2c
--- /dev/null
+++ b/jass-scoreboard/js/renderer.js
@@ -0,0 +1,114 @@
+// renderer.js β render board, tallies, totals, win state
+
+import { getState } from "./state.js";
+
+/**
+ * Converts a point value into tally mark strings.
+ * Groups of 5 become "||||\" and remainders are "|".
+ */
+function renderTallies(points) {
+ const groups = Math.floor(points / 5);
+ const remainder = points % 5;
+ let result = "";
+ for (let i = 0; i < groups; i++) {
+ result += "||||\\ ";
+ }
+ for (let i = 0; i < remainder; i++) {
+ result += "| ";
+ }
+ return result.trim();
+}
+
+/**
+ * Build the HTML for one team's entry list.
+ */
+function buildEntriesHTML(teamId) {
+ const state = getState();
+ const teamEntries = state.entries.filter(e => e.teamId === teamId);
+ if (teamEntries.length === 0) {
+ return 'β';
+ }
+ return teamEntries
+ .map(e => `${renderTallies(e.points)}
`)
+ .join("");
+}
+
+/**
+ * Full render of the scoreboard.
+ */
+function render() {
+ const state = getState();
+
+ const [leftTeam, rightTeam] = state.flipped
+ ? [state.teams[1], state.teams[0]]
+ : [state.teams[0], state.teams[1]];
+
+ // Team name headers
+ document.getElementById("team-a-name").textContent = leftTeam.name;
+ document.getElementById("team-b-name").textContent = rightTeam.name;
+
+ // Entries (tally marks)
+ document.getElementById("entries-a").innerHTML = buildEntriesHTML(leftTeam.id);
+ document.getElementById("entries-b").innerHTML = buildEntriesHTML(rightTeam.id);
+
+ // Totals
+ document.getElementById("total-a").textContent = state.totals[leftTeam.id] || 0;
+ document.getElementById("total-b").textContent = state.totals[rightTeam.id] || 0;
+
+ // Target score display
+ const targetEl = document.getElementById("target-score-display");
+ if (targetEl) targetEl.textContent = state.targetScore;
+
+ // Win state
+ renderWinState(leftTeam, rightTeam);
+
+ // Update team select options to reflect current names
+ const teamSelect = document.getElementById("select-team");
+ if (teamSelect) {
+ for (const opt of teamSelect.options) {
+ const team = state.teams.find(t => t.id === opt.value);
+ if (team) opt.textContent = team.name;
+ }
+ }
+
+ // Input area enable/disable (undo and reset always stay enabled)
+ const inputArea = document.getElementById("score-input-area");
+ if (inputArea) {
+ inputArea.classList.toggle("disabled", state.gameFinished);
+ const inputs = inputArea.querySelectorAll("input, select");
+ inputs.forEach(el => { el.disabled = state.gameFinished; });
+ const addBtn = document.getElementById("btn-add");
+ if (addBtn) addBtn.disabled = state.gameFinished;
+ }
+
+ // Highlight winning panel
+ const panelA = document.getElementById("panel-a");
+ const panelB = document.getElementById("panel-b");
+ if (panelA && panelB) {
+ panelA.classList.remove("winner-panel");
+ panelB.classList.remove("winner-panel");
+ if (state.gameFinished && state.winner) {
+ const winnerId = state.winner;
+ const winnerIsLeft = leftTeam.id === winnerId;
+ if (winnerIsLeft) panelA.classList.add("winner-panel");
+ else panelB.classList.add("winner-panel");
+ }
+ }
+}
+
+function renderWinState(leftTeam, rightTeam) {
+ const state = getState();
+ const overlay = document.getElementById("win-overlay");
+ if (!overlay) return;
+
+ if (state.gameFinished && state.winner) {
+ const winningTeam =
+ state.winner === leftTeam.id ? leftTeam : rightTeam;
+ document.getElementById("win-team-name").textContent = winningTeam.name;
+ overlay.classList.add("active");
+ } else {
+ overlay.classList.remove("active");
+ }
+}
+
+export { render, renderTallies };
diff --git a/jass-scoreboard/js/scoring.js b/jass-scoreboard/js/scoring.js
new file mode 100644
index 0000000..1cb5451
--- /dev/null
+++ b/jass-scoreboard/js/scoring.js
@@ -0,0 +1,55 @@
+// scoring.js β add score, calculate totals, check winner
+
+import { getState } from "./state.js";
+
+function addEntry(teamId, points) {
+ const state = getState();
+ if (state.gameFinished) return false;
+
+ const p = parseInt(points, 10);
+ if (isNaN(p) || p <= 0 || p > 500) return false;
+ if (!state.teams.find(t => t.id === teamId)) return false;
+
+ state.entries.push({
+ teamId,
+ points: p,
+ timestamp: Date.now()
+ });
+
+ recalcTotals();
+ checkWinner();
+ return true;
+}
+
+function undoLastEntry() {
+ const state = getState();
+ if (state.entries.length === 0) return false;
+ state.entries.pop();
+ state.winner = null;
+ state.gameFinished = false;
+ recalcTotals();
+ checkWinner();
+ return true;
+}
+
+function recalcTotals() {
+ const state = getState();
+ state.totals = { A: 0, B: 0 };
+ for (const entry of state.entries) {
+ state.totals[entry.teamId] = (state.totals[entry.teamId] || 0) + entry.points;
+ }
+}
+
+function checkWinner() {
+ const state = getState();
+ if (state.gameFinished) return;
+ for (const team of state.teams) {
+ if ((state.totals[team.id] || 0) > state.targetScore) {
+ state.winner = team.id;
+ state.gameFinished = true;
+ return;
+ }
+ }
+}
+
+export { addEntry, undoLastEntry, recalcTotals, checkWinner };
diff --git a/jass-scoreboard/js/state.js b/jass-scoreboard/js/state.js
new file mode 100644
index 0000000..758b6cd
--- /dev/null
+++ b/jass-scoreboard/js/state.js
@@ -0,0 +1,63 @@
+// state.js β global state definition and mutations
+
+const DEFAULT_STATE = {
+ teams: [
+ { id: "A", name: "Team A" },
+ { id: "B", name: "Team B" }
+ ],
+ entries: [],
+ totals: {
+ A: 0,
+ B: 0
+ },
+ targetScore: 2500,
+ flipped: false,
+ winner: null,
+ gameFinished: false
+};
+
+let state = JSON.parse(JSON.stringify(DEFAULT_STATE));
+
+function getState() {
+ return state;
+}
+
+function setState(newState) {
+ state = newState;
+}
+
+function resetState() {
+ const preserved = {
+ teams: state.teams.map(t => ({ ...t })),
+ targetScore: state.targetScore,
+ flipped: state.flipped
+ };
+ state = {
+ ...JSON.parse(JSON.stringify(DEFAULT_STATE)),
+ teams: preserved.teams,
+ targetScore: preserved.targetScore,
+ flipped: preserved.flipped
+ };
+}
+
+function setTeamName(teamId, name) {
+ const trimmed = name.trim();
+ if (trimmed.length < 1 || trimmed.length > 30) return false;
+ const team = state.teams.find(t => t.id === teamId);
+ if (!team) return false;
+ team.name = trimmed;
+ return true;
+}
+
+function setTargetScore(value) {
+ const n = parseInt(value, 10);
+ if (isNaN(n) || n < 100 || n > 10000) return false;
+ state.targetScore = n;
+ return true;
+}
+
+function flipBoard() {
+ state.flipped = !state.flipped;
+}
+
+export { getState, setState, resetState, setTeamName, setTargetScore, flipBoard };
diff --git a/jass-scoreboard/js/storage.js b/jass-scoreboard/js/storage.js
new file mode 100644
index 0000000..baa8857
--- /dev/null
+++ b/jass-scoreboard/js/storage.js
@@ -0,0 +1,41 @@
+// storage.js β localStorage persistence
+
+const STORAGE_KEY = "jassScoreboardState";
+
+function saveState(state) {
+ const toSave = {
+ teams: state.teams,
+ entries: state.entries,
+ targetScore: state.targetScore,
+ flipped: state.flipped,
+ winner: state.winner,
+ gameFinished: state.gameFinished
+ };
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
+ } catch (e) {
+ // storage may be unavailable (e.g. private mode quota)
+ console.warn("Could not save state:", e);
+ }
+}
+
+function loadState() {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return null;
+ return JSON.parse(raw);
+ } catch (e) {
+ console.warn("Could not load state:", e);
+ return null;
+ }
+}
+
+function clearState() {
+ try {
+ localStorage.removeItem(STORAGE_KEY);
+ } catch (e) {
+ console.warn("Could not clear state:", e);
+ }
+}
+
+export { saveState, loadState, clearState };
diff --git a/jass-scoreboard/js/ui.js b/jass-scoreboard/js/ui.js
new file mode 100644
index 0000000..a74a883
--- /dev/null
+++ b/jass-scoreboard/js/ui.js
@@ -0,0 +1,169 @@
+// ui.js β handle user input, bind event listeners, validate inputs
+
+import { getState, setTeamName, setTargetScore, flipBoard, resetState } from "./state.js";
+import { addEntry, undoLastEntry } from "./scoring.js";
+import { render } from "./renderer.js";
+import { saveState } from "./storage.js";
+
+let errorTimeout = null;
+
+function persist() {
+ saveState(getState());
+}
+
+function bindEvents() {
+ // Score entry form
+ const addBtn = document.getElementById("btn-add");
+ if (addBtn) {
+ addBtn.addEventListener("click", handleAddScore);
+ }
+
+ // Allow pressing Enter in the points input
+ const pointsInput = document.getElementById("input-points");
+ if (pointsInput) {
+ pointsInput.addEventListener("keydown", e => {
+ if (e.key === "Enter") handleAddScore();
+ });
+ }
+
+ // Undo button
+ const undoBtn = document.getElementById("btn-undo");
+ if (undoBtn) {
+ undoBtn.addEventListener("click", () => {
+ undoLastEntry();
+ render();
+ persist();
+ });
+ }
+
+ // Reset button
+ const resetBtn = document.getElementById("btn-reset");
+ if (resetBtn) {
+ resetBtn.addEventListener("click", () => {
+ if (confirm("Reset the game? Team names and target score will be kept.")) {
+ resetState();
+ render();
+ persist();
+ // Re-enable score input
+ const inputArea = document.getElementById("score-input-area");
+ if (inputArea) {
+ const inputs = inputArea.querySelectorAll("input, button, select");
+ inputs.forEach(el => { el.disabled = false; });
+ inputArea.classList.remove("disabled");
+ }
+ document.getElementById("win-overlay")?.classList.remove("active");
+ }
+ });
+ }
+
+ // Flip button
+ const flipBtn = document.getElementById("btn-flip");
+ if (flipBtn) {
+ flipBtn.addEventListener("click", () => {
+ flipBoard();
+ render();
+ persist();
+ });
+ }
+
+ // Team name edits (blur to save)
+ const nameA = document.getElementById("edit-name-a");
+ if (nameA) {
+ nameA.addEventListener("change", () => {
+ const ok = setTeamName("A", nameA.value);
+ if (!ok) nameA.value = getState().teams.find(t => t.id === "A").name;
+ render();
+ persist();
+ });
+ }
+
+ const nameB = document.getElementById("edit-name-b");
+ if (nameB) {
+ nameB.addEventListener("change", () => {
+ const ok = setTeamName("B", nameB.value);
+ if (!ok) nameB.value = getState().teams.find(t => t.id === "B").name;
+ render();
+ persist();
+ });
+ }
+
+ // Target score edit
+ const targetInput = document.getElementById("input-target");
+ if (targetInput) {
+ targetInput.addEventListener("change", () => {
+ const ok = setTargetScore(targetInput.value);
+ if (!ok) {
+ targetInput.value = getState().targetScore;
+ showError("Target score must be between 100 and 10000.");
+ } else {
+ render();
+ persist();
+ }
+ });
+ }
+
+ // Close win overlay (click anywhere on it)
+ const winOverlay = document.getElementById("win-overlay");
+ if (winOverlay) {
+ winOverlay.addEventListener("click", () => {
+ winOverlay.classList.remove("active");
+ });
+ }
+}
+
+function handleAddScore() {
+ const state = getState();
+ if (state.gameFinished) return;
+
+ const teamSelect = document.getElementById("select-team");
+ const pointsInput = document.getElementById("input-points");
+
+ const teamId = teamSelect ? teamSelect.value : null;
+ const pointsRaw = pointsInput ? pointsInput.value : "";
+
+ if (!teamId) {
+ showError("Please select a team.");
+ return;
+ }
+
+ const points = parseInt(pointsRaw, 10);
+ if (isNaN(points) || points <= 0 || points > 500) {
+ showError("Points must be a whole number between 1 and 500.");
+ pointsInput && pointsInput.focus();
+ return;
+ }
+
+ const ok = addEntry(teamId, points);
+ if (ok) {
+ if (pointsInput) {
+ pointsInput.value = "";
+ pointsInput.focus();
+ }
+ render();
+ persist();
+ }
+}
+
+function showError(msg) {
+ const errEl = document.getElementById("error-msg");
+ if (!errEl) return;
+ errEl.textContent = msg;
+ errEl.classList.add("visible");
+ clearTimeout(errorTimeout);
+ errorTimeout = setTimeout(() => {
+ errEl.classList.remove("visible");
+ }, 3000);
+}
+
+function initInputValues() {
+ const state = getState();
+ const nameA = document.getElementById("edit-name-a");
+ const nameB = document.getElementById("edit-name-b");
+ const targetInput = document.getElementById("input-target");
+
+ if (nameA) nameA.value = state.teams[0].name;
+ if (nameB) nameB.value = state.teams[1].name;
+ if (targetInput) targetInput.value = state.targetScore;
+}
+
+export { bindEvents, initInputValues };
From f59767782953275974788280a0841aa159b481a7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 21:27:45 +0000
Subject: [PATCH 3/4] =?UTF-8?q?feat:=20Z/Z=20layout=20=E2=80=94=20two=20Z?=
=?UTF-8?q?=20shapes,=20one=20per=20player=20(top/bottom)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: LFDave <36726874+LFDave@users.noreply.github.com>
---
jass-scoreboard/css/styles.css | 55 ++++++++++++++++++++--------------
jass-scoreboard/index.html | 39 ++++++++++++++----------
jass-scoreboard/js/renderer.js | 24 +++++++--------
3 files changed, 68 insertions(+), 50 deletions(-)
diff --git a/jass-scoreboard/css/styles.css b/jass-scoreboard/css/styles.css
index 5b9d632..5e474ab 100644
--- a/jass-scoreboard/css/styles.css
+++ b/jass-scoreboard/css/styles.css
@@ -146,16 +146,15 @@ body {
flex-shrink: 0;
}
-/* βββ Board (Z/Z grid) ββββββββββββββββββββββββββββββββββββββββββ */
+/* βββ Board (Z/Z grid β two stacked panels) ββββββββββββββββββββ */
.board {
display: grid;
- grid-template-columns: 1fr 60px 1fr;
- min-height: 340px;
+ grid-template-rows: 1fr auto 1fr;
+ min-height: 420px;
background: var(--slate-surface);
border: 1px solid var(--panel-border);
border-radius: var(--radius);
overflow: hidden;
- /* Subtle noise texture overlay via box-shadow */
box-shadow:
inset 0 0 60px rgba(0,0,0,0.35),
0 4px 20px rgba(0,0,0,0.5);
@@ -171,12 +170,21 @@ body {
transition: box-shadow 0.4s ease;
}
-.panel-left {
- border-right: none;
+/* Bottom panel rotated 180Β° so the opposing player can read it */
+.panel-bottom {
+ transform: rotate(180deg);
}
-.panel-right {
- border-left: none;
+/* Horizontal red chalk line between the two Z panels */
+.board-h-divider {
+ height: 3px;
+ background: linear-gradient(
+ to right,
+ transparent 0%,
+ rgba(224, 80, 80, 0.7) 15%,
+ rgba(224, 80, 80, 0.7) 85%,
+ transparent 100%
+ );
}
/* Winner highlight */
@@ -219,11 +227,11 @@ body {
font-size: 0.95rem;
line-height: 1.7;
letter-spacing: 0.05em;
- overflow-y: auto;
word-break: break-word;
- min-height: 120px;
- padding: 4px 2px;
+ padding: 6px 4px;
color: var(--chalk-dim);
+ position: relative;
+ z-index: 1;
}
.tally-row {
@@ -259,16 +267,20 @@ body {
text-shadow: 0 0 10px rgba(240,237,224,0.4);
}
-/* βββ Z/Z Divider βββββββββββββββββββββββββββββββββββββββββββββββ */
-.board-divider {
- display: flex;
- align-items: stretch;
+/* βββ Z wrapper β contains SVG guide lines + tally entries βββββ */
+.z-wrapper {
position: relative;
+ flex: 1;
+ min-height: 110px;
+ overflow-y: auto;
}
-.z-divider-svg {
+.z-lines {
+ position: absolute;
+ inset: 0;
width: 100%;
height: 100%;
+ pointer-events: none;
}
/* βββ Score input area ββββββββββββββββββββββββββββββββββββββββββ */
@@ -513,7 +525,6 @@ body {
@media (max-width: 480px) {
.app-wrapper { padding: 8px 8px 20px; }
.header-title { font-size: 1.3rem; }
- .board { grid-template-columns: 1fr 44px 1fr; }
.panel-team-name { font-size: 0.95rem; }
.entries-area { font-size: 0.82rem; }
.total-value { font-size: 1.15rem; }
@@ -524,11 +535,11 @@ body {
}
@media (min-width: 600px) {
- .board { min-height: 400px; }
+ .board { min-height: 480px; }
.entries-area { font-size: 1rem; }
}
-/* βββ Scrollbar styling βββββββββββββββββββββββββββββββββββββββββ */
-.entries-area::-webkit-scrollbar { width: 4px; }
-.entries-area::-webkit-scrollbar-track { background: transparent; }
-.entries-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 4px; }
+/* βββ Scrollbar styling (z-wrapper) ββββββββββββββββββββββββββββ */
+.z-wrapper::-webkit-scrollbar { width: 4px; }
+.z-wrapper::-webkit-scrollbar-track { background: transparent; }
+.z-wrapper::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 4px; }
diff --git a/jass-scoreboard/index.html b/jass-scoreboard/index.html
index 250f723..3c0b6dd 100644
--- a/jass-scoreboard/index.html
+++ b/jass-scoreboard/index.html
@@ -37,7 +37,7 @@
step="50"
aria-label="Target score"
/>
-
+
@@ -56,30 +56,37 @@
-
-
+
+
-
-
-
-
+
+
-
-
+
+
-
+
Total:
0
diff --git a/jass-scoreboard/js/renderer.js b/jass-scoreboard/js/renderer.js
index de79c2c..5285ee3 100644
--- a/jass-scoreboard/js/renderer.js
+++ b/jass-scoreboard/js/renderer.js
@@ -39,28 +39,28 @@ function buildEntriesHTML(teamId) {
function render() {
const state = getState();
- const [leftTeam, rightTeam] = state.flipped
+ const [topTeam, bottomTeam] = state.flipped
? [state.teams[1], state.teams[0]]
: [state.teams[0], state.teams[1]];
// Team name headers
- document.getElementById("team-a-name").textContent = leftTeam.name;
- document.getElementById("team-b-name").textContent = rightTeam.name;
+ document.getElementById("team-a-name").textContent = topTeam.name;
+ document.getElementById("team-b-name").textContent = bottomTeam.name;
// Entries (tally marks)
- document.getElementById("entries-a").innerHTML = buildEntriesHTML(leftTeam.id);
- document.getElementById("entries-b").innerHTML = buildEntriesHTML(rightTeam.id);
+ document.getElementById("entries-a").innerHTML = buildEntriesHTML(topTeam.id);
+ document.getElementById("entries-b").innerHTML = buildEntriesHTML(bottomTeam.id);
// Totals
- document.getElementById("total-a").textContent = state.totals[leftTeam.id] || 0;
- document.getElementById("total-b").textContent = state.totals[rightTeam.id] || 0;
+ document.getElementById("total-a").textContent = state.totals[topTeam.id] || 0;
+ document.getElementById("total-b").textContent = state.totals[bottomTeam.id] || 0;
// Target score display
const targetEl = document.getElementById("target-score-display");
if (targetEl) targetEl.textContent = state.targetScore;
// Win state
- renderWinState(leftTeam, rightTeam);
+ renderWinState(topTeam, bottomTeam);
// Update team select options to reflect current names
const teamSelect = document.getElementById("select-team");
@@ -89,21 +89,21 @@ function render() {
panelB.classList.remove("winner-panel");
if (state.gameFinished && state.winner) {
const winnerId = state.winner;
- const winnerIsLeft = leftTeam.id === winnerId;
- if (winnerIsLeft) panelA.classList.add("winner-panel");
+ const winnerIsTop = topTeam.id === winnerId;
+ if (winnerIsTop) panelA.classList.add("winner-panel");
else panelB.classList.add("winner-panel");
}
}
}
-function renderWinState(leftTeam, rightTeam) {
+function renderWinState(topTeam, bottomTeam) {
const state = getState();
const overlay = document.getElementById("win-overlay");
if (!overlay) return;
if (state.gameFinished && state.winner) {
const winningTeam =
- state.winner === leftTeam.id ? leftTeam : rightTeam;
+ state.winner === topTeam.id ? topTeam : bottomTeam;
document.getElementById("win-team-name").textContent = winningTeam.name;
overlay.classList.add("active");
} else {
From 5c01474d9b14b89d1adbd55f9e56d8d0a4d003bc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Mar 2026 21:54:41 +0000
Subject: [PATCH 4/4] =?UTF-8?q?feat:=20SVG=20Z=20marks=20on=20lines=20?=
=?UTF-8?q?=E2=80=94=20top=3D100,=20diagonal=3D50,=20bottom=3D20?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: LFDave <36726874+LFDave@users.noreply.github.com>
---
jass-scoreboard/css/styles.css | 126 +++++++++++++++++-------------
jass-scoreboard/index.html | 55 +++++---------
jass-scoreboard/js/renderer.js | 135 ++++++++++++++++++++++-----------
jass-scoreboard/js/scoring.js | 16 ++--
jass-scoreboard/js/ui.js | 83 +++++++++-----------
5 files changed, 231 insertions(+), 184 deletions(-)
diff --git a/jass-scoreboard/css/styles.css b/jass-scoreboard/css/styles.css
index 5e474ab..a57d9b5 100644
--- a/jass-scoreboard/css/styles.css
+++ b/jass-scoreboard/css/styles.css
@@ -220,28 +220,17 @@ body {
white-space: nowrap;
}
-/* βββ Entries area (tally marks) βββββββββββββββββββββββββββββββ */
-.entries-area {
+/* βββ Z SVG area (filled by renderer.js) βββββββββββββββββββββββ */
+.z-svg-area {
flex: 1;
- font-family: 'Courier New', Courier, monospace;
- font-size: 0.95rem;
- line-height: 1.7;
- letter-spacing: 0.05em;
- word-break: break-word;
- padding: 6px 4px;
- color: var(--chalk-dim);
- position: relative;
- z-index: 1;
-}
-
-.tally-row {
- padding: 1px 0;
- white-space: nowrap;
+ min-height: 110px;
+ overflow: hidden;
}
-.no-entries {
- color: rgba(240,237,224,0.2);
- font-size: 1.2rem;
+.z-svg-area svg {
+ display: block;
+ width: 100%;
+ height: 100%;
}
/* βββ Total row βββββββββββββββββββββββββββββββββββββββββββββββββ */
@@ -282,7 +271,6 @@ body {
height: 100%;
pointer-events: none;
}
-
/* βββ Score input area ββββββββββββββββββββββββββββββββββββββββββ */
.score-input-area {
background: var(--input-bg);
@@ -299,46 +287,88 @@ body {
pointer-events: none;
}
-.input-row {
+/* βββ Team toggle row βββββββββββββββββββββββββββββββββββββββββββ */
+.team-toggle-row {
display: flex;
gap: 8px;
- align-items: center;
- flex-wrap: wrap;
}
-.select-team {
+.btn-team {
flex: 1;
- min-width: 100px;
- padding: 10px 10px;
- background: var(--slate-bg);
+ padding: 10px 8px;
+ background: var(--input-bg);
border: 1px solid var(--input-border);
border-radius: var(--radius);
- color: var(--chalk-white);
+ color: var(--chalk-dim);
font-size: 0.95rem;
+ font-weight: 600;
cursor: pointer;
+ transition: background 0.18s, color 0.18s, border-color 0.18s;
+ letter-spacing: 0.04em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.select-team:focus {
- outline: 2px solid var(--chalk-dim);
- outline-offset: 1px;
+.btn-team.is-active {
+ background: var(--btn-primary);
+ color: #fff;
+ border-color: var(--btn-primary-h);
}
-.points-input {
- flex: 2;
- min-width: 80px;
- padding: 10px 12px;
- background: var(--slate-bg);
- border: 1px solid var(--input-border);
+.btn-team:hover:not(.is-active) {
+ background: rgba(255,255,255,0.06);
+ color: var(--chalk-white);
+}
+
+/* βββ Mark buttons (+100 / +50 / +20) ββββββββββββββββββββββββββ */
+.mark-row {
+ display: flex;
+ gap: 8px;
+}
+
+.btn-mark {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 3px;
+ padding: 12px 6px;
+ background: var(--slate-surface);
+ border: 1px solid var(--panel-border);
border-radius: var(--radius);
+ cursor: pointer;
+ transition: background 0.18s, transform 0.1s, border-color 0.18s;
+}
+
+.btn-mark:hover {
+ background: rgba(255,255,255,0.07);
+ border-color: rgba(240,237,224,0.3);
+}
+
+.btn-mark:active {
+ transform: scale(0.95);
+ background: rgba(255,255,255,0.12);
+}
+
+.btn-mark:focus-visible {
+ outline: 2px solid var(--chalk-white);
+ outline-offset: 2px;
+}
+
+.mark-pts {
+ font-size: 1.3rem;
+ font-weight: 700;
color: var(--chalk-white);
- font-size: 1rem;
- text-align: center;
- transition: border-color 0.2s;
+ text-shadow: 0 0 8px rgba(240,237,224,0.3);
+ letter-spacing: 0.04em;
}
-.points-input:focus {
- outline: none;
- border-color: var(--chalk-white);
+.mark-lbl {
+ font-size: 0.7rem;
+ color: var(--chalk-dim);
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
}
/* βββ Buttons βββββββββββββββββββββββββββββββββββββββββββββββββββ */
@@ -526,20 +556,12 @@ body {
.app-wrapper { padding: 8px 8px 20px; }
.header-title { font-size: 1.3rem; }
.panel-team-name { font-size: 0.95rem; }
- .entries-area { font-size: 0.82rem; }
.total-value { font-size: 1.15rem; }
- .input-row { flex-direction: column; }
- .select-team, .points-input, .btn-primary { width: 100%; }
+ .mark-pts { font-size: 1.1rem; }
#win-overlay h2 { font-size: 2rem; }
.win-team { font-size: 1.3rem; }
}
@media (min-width: 600px) {
.board { min-height: 480px; }
- .entries-area { font-size: 1rem; }
}
-
-/* βββ Scrollbar styling (z-wrapper) ββββββββββββββββββββββββββββ */
-.z-wrapper::-webkit-scrollbar { width: 4px; }
-.z-wrapper::-webkit-scrollbar-track { background: transparent; }
-.z-wrapper::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 4px; }
diff --git a/jass-scoreboard/index.html b/jass-scoreboard/index.html
index 3c0b6dd..15f27ec 100644
--- a/jass-scoreboard/index.html
+++ b/jass-scoreboard/index.html
@@ -59,14 +59,7 @@
-
+
Total:
0
@@ -79,14 +72,7 @@
-
+
Total:
0
@@ -97,26 +83,27 @@