Compare commits

..

No commits in common. "master" and "luca" have entirely different histories.
master ... luca

13 changed files with 737 additions and 655 deletions

116
README.md
View File

@ -1,4 +1,8 @@
# GeoDraw — FS 2026 Frontend-Entwicklung
Todos:
- für Backend und weitere Änderungen anpassen
- evtl. kurze Self-Reflection
# GeoDraw - FS 2026 Frontendentwicklung
**[Link to our presentation](https://fhgraubuenden-my.sharepoint.com/:p:/g/personal/pengniklas_fhgr_ch/IQDz4D0YTsysR7oDcVnon2lnAYEj1K0Ie75lc4-guWgusns?e=gjZJ58)**
@ -6,9 +10,9 @@
## Project Description
GeoDraw is a competitive browser-based geography game. Players are shown the name of a country and must draw its border from memory on a blank canvas. City markers serve as spatial hints. After each round the drawing is scored by comparing the player's polygon against the real GeoJSON border using an Intersection over Union (IoU) algorithm. The goal is to achieve the highest total score across three rounds.
GeoDraw is a competitive browser-based geography game. Players are shown the name of a country and must draw its border from memory on a blank canvas. City markers serve as spatial hints. After each round the drawing is scored based on accuracy. The goal is to achieve the highest total score across three rounds.
Players create or join a shared lobby. Scores are stored per lobby on the backend, so multiple players can compete and compare results on a shared leaderboard.
The game supports single-player sessions with a local leaderboard tracking top scores across multiple players on the same device.
---
@ -16,34 +20,33 @@ Players create or join a shared lobby. Scores are stored per lobby on the backen
From the project root, run:
```bash
```powershell
php serve.php
```
Then open `http://localhost:8000/` in your browser. PHP 8.0+ is required.
Then open `http://localhost:8000/` in a browser.
You can override the port, but the default will be `8000`.
---
## How to play
1. **Create or join a lobby** — create a new lobby and share its name with friends, or enter an existing lobby name to join
1. **Create a lobby** — enter a lobby name and start the session
2. **Enter your username** — your name will appear on the leaderboard
3. **Draw** — sketch the country border from memory within 60 seconds; city dots are your hints
4. **Submit** — submit your drawing to see the real border overlaid on yours
5. **Score** — your drawing is graded on accuracy (grades S / A / B / C / D)
6. **Results** — after 3 rounds, see your total score and round breakdown
7. **Leaderboard** — compare your score with other players in the same lobby
4. **Score** — your drawing is graded on accuracy (grades S / A / B / C / D)
5. **Results** — after 3 rounds, see your total score and round breakdown
6. **Leaderboard** — compare your score with previous sessions
---
## Special Features
- **Freehand canvas drawing** using the Pointer Events API (mouse & touch support)
- **Real polygon scoring** via rasterized IoU (96×96 grid) against GeoJSON country outlines
- **Reference outline overlay** revealed on the canvas after each round submission
- **City hint markers** rendered directly on the canvas with labelled pins
- **Live countdown timer** with colour-coded urgency states (green → yellow → red)
- **Per-lobby leaderboard** stored on the PHP backend, top 20 entries per lobby
- **Score feedback overlay** displayed on the canvas after each round
- **Animated results page** with per-round bar chart breakdown
- **Persistent leaderboard** stored in `localStorage`, top 20 entries ranked by total score
- **Scroll reveal animations** on the landing page via IntersectionObserver
- **Fully responsive** layout across desktop, tablet and mobile
- **WCAG Level A accessibility** — semantic HTML, `aria-label`, `aria-hidden`, `prefers-reduced-motion`
@ -55,38 +58,60 @@ Then open `http://localhost:8000/` in your browser. PHP 8.0+ is required.
### Stack
```
frontend/
├── HTML5
├── Vanilla CSS3
└── Vanilla ES6+ (ES modules)
└── Canvas 2D API + Pointer Events (drawing & scoring)
backend/
└── PHP 8.5
└── JSON file storage (backend/data/lobbies.json)
frontend/data/
├── countries.json — country metadata and city coordinates
└── outlines/*.json — GeoJSON polygon outlines (10 countries)
tools/
└── convert-geojson-outlines.js — data pipeline: Natural Earth GeoJSON → game outlines
frontend
├── HTML
├── Vanilla CSS
└── Vanilla JavaScript
└── Pixi.js (TBD)
backend
└── PHP (TBD)
└── Postgres (TBD)
```
The backend is planned to be written in PHP, with a database using PostgreSQL. Language is pending.
The drawing part will likely be handled by WebGL and a canvas.
### Coding aspects
- Evaluating arbitrary shapes and determining their surface area
- Comparing two arbitrary shapes to calculate a score
- Storing and receiving data in the database
- Handling user events (drawing, clicks, navigation)
### Technologies, Libraries & Frameworks
| Layer | Technology | Reason |
|---|---|---|
| HTML | Vanilla HTML5 | Semantic markup, no abstraction needed |
| CSS | Vanilla CSS3 (Flexbox + Grid) | Full control; CSS fundamentals are a learning objective |
| JavaScript | Vanilla ES6+ (ES modules) | No build step required; core learning goal of the module |
| Drawing & Scoring | Canvas 2D API + Pointer Events | Native browser APIs; IoU scoring via 96×96 raster grid |
| Country data | GeoJSON outlines | Standard geospatial format, lightweight and precise |
| Backend | PHP 8.5 | Server-side REST API |
| Storage | JSON file (lobbies.json) | Dependency-free; no database server needed for this scope |
| Testing | Node.js built-in test runner | Unit tests for IoU scoring algorithm (scoring.test.mjs) |
| Linting / Formatting | Biome | Fast unified linter and formatter |
| Package Manager | npm | Dev-only dependency (Biome) |
| CSS | Vanilla CSS3 (Flexbox + Grid) | Full control over design, no framework overhead |
| JavaScript | Vanilla ES6+ | Core learning goal of the module; no build step required |
| Drawing | Canvas 2D API + Pointer Events | Native browser API, no library dependency |
| Backend | PHP | Server-side logic and REST API |
| Database | PostgreSQL | Relational data storage for scores and lobbies |
| Linting / Formatting | Biome | Fast, unified linter and formatter replacing ESLint + Prettier |
| Package Manager | npm | Manages dev dependencies (Biome only) |
---
## Setup
### Frontend
Your browser must support **WebGL**. If you are uncertain, [check this website](https://get.webgl.org/).
Start the app from the project root:
```powershell
php serve.php
```
Then open `http://localhost:8000/` in a browser.
### Backend
Setup for backend is WIP.
---
@ -97,3 +122,16 @@ tools/
| **Ivan Szabo** | Frontend development (initial/base: pages, scripts, styles...) |
| **Luca Jakob** | Frontend review; Backend development |
| **Niklas Peng** | Frontend improvements (accessibility, code quality, CSS consolidation, JSDoc...); Backend review |
---
## Self-Reflection
**Ivan Szabo:**
tbd
**Luca Jakob:**
tbd
**Niklas Peng:**
tbd

View File

@ -97,7 +97,7 @@
<div class="sidebar-card">
<p class="sidebar-card__label">How to play</p>
<p class="sidebar-hint">
<p style="font-size:.82rem;color:var(--ink-soft);line-height:1.55;">
Draw the country's outline from memory. City dots are hints. Press <strong>Submit</strong> when done or wait for the timer.
</p>
</div>
@ -147,4 +147,4 @@
<script type="module" src="scripts/game.js"></script>
</body>
</html>
</html>

View File

@ -12,7 +12,6 @@
<p class="noscript-msg">GeoDraw requires JavaScript. Please enable it in your browser settings.</p>
</noscript>
<div class="wrap">
<header class="header">
<div class="container header__inner">
<a href="#hero" class="logo">
@ -27,15 +26,14 @@
</a>
<nav class="nav" aria-label="Main navigation">
<a href="#hero" class="nav__link">Play</a>
<a href="#about" class="nav__link">How to play</a>
<a href="#register" class="nav__cta">Play</a>
<a href="#register" class="nav__cta">Create lobby</a>
</nav>
</div>
</header>
<main>
<!-- ─── HERO ─── -->
<section class="hero" id="hero">
<div class="container hero__inner">
@ -58,83 +56,62 @@
Start Playing
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</a>
<a href="#about" class="btn btn--ghost">How to play</a>
<a href="#about" class="btn btn--ghost">How it works</a>
</div>
</div>
<div class="globe-card reveal reveal-delay-1">
<div class="globe">
<div class="continent continent--a"></div>
<div class="continent continent--b"></div>
<div class="continent continent--c"></div>
<div class="continent continent--d"></div>
<div class="globe-line globe-line--meridian-sm"></div>
<div class="globe-line globe-line--meridian-lg"></div>
<div class="globe-line globe-line--parallel-sm"></div>
<div class="globe-line globe-line--parallel-lg"></div>
<div class="continent c1"></div>
<div class="continent c2"></div>
<div class="continent c3"></div>
<div class="continent c4"></div>
<div class="globe-line gl1"></div>
<div class="globe-line gl2"></div>
<div class="globe-line gl3"></div>
<div class="globe-line gl4"></div>
</div>
<div class="globe-badge globe-badge--score">
<span class="globe-badge__dot globe-badge__dot--green"></span>
<div class="globe-badge gb--score">
<span class="gb__dot gb__dot--green"></span>
<span>Score: 94%</span>
</div>
<div class="globe-badge globe-badge--country">
<span class="globe-badge__dot globe-badge__dot--blue"></span>
<div class="globe-badge gb--country">
<span class="gb__dot gb__dot--blue"></span>
<span>Norway — Round 3</span>
</div>
</div>
</div>
</section>
<!-- ─── ABOUT ─── -->
<section class="about" id="about">
<div class="container about__inner">
<div class="about__left">
<p class="section-label reveal">How to play</p>
<h2 class="section-title reveal reveal-delay-1">About<br>the Game</h2>
<p class="section-label reveal">About the game</p>
<h2 class="section-title reveal reveal-delay-1">What it is &<br>How to play</h2>
<p class="about__desc reveal reveal-delay-2">
GeoDraw is a competitive browser game built on memory and precision. You get a country name, sketch its outline, and the system calculates how close you were to the real border.
</p>
<div class="game-preview reveal reveal-delay-3">
<svg class="game-preview__svg" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<!-- Grid lines -->
<line x1="0" y1="66" x2="200" y2="66" stroke="rgba(11,31,42,.06)" stroke-width="1"/>
<line x1="0" y1="133" x2="200" y2="133" stroke="rgba(11,31,42,.06)" stroke-width="1"/>
<line x1="66" y1="0" x2="66" y2="200" stroke="rgba(11,31,42,.06)" stroke-width="1"/>
<line x1="133" y1="0" x2="133" y2="200" stroke="rgba(11,31,42,.06)" stroke-width="1"/>
<!-- Reference outline (filled) -->
<path d="M100 22 L148 40 L162 76 L150 112 L124 148 L80 142 L52 108 L54 66 Z"
fill="rgba(26,127,196,.10)"
stroke="rgba(26,127,196,.5)"
stroke-width="2"
stroke-linejoin="round"/>
<!-- Player's drawn path -->
<path d="M96 26 L152 44 L164 82 L146 118 L120 152 L76 144 L48 106 L52 62 Z"
fill="none"
stroke="#1a7fc4"
stroke-width="2.5"
stroke-linejoin="round"
stroke-linecap="round"/>
<!-- City dot: Bern -->
<circle cx="96" cy="100" r="4" fill="rgba(240,180,40,.9)"/>
<circle cx="96" cy="100" r="4" fill="none" stroke="white" stroke-width="1.5"/>
<!-- City dot: Zurich -->
<circle cx="118" cy="72" r="4" fill="rgba(240,180,40,.9)"/>
<circle cx="118" cy="72" r="4" fill="none" stroke="white" stroke-width="1.5"/>
<!-- Score badge -->
<rect x="118" y="22" width="62" height="26" rx="8" fill="white" filter="url(#shadow)"/>
<defs>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="rgba(11,31,42,.12)"/>
</filter>
</defs>
<circle cx="131" cy="35" r="4" fill="#41b869"/>
<text x="139" y="39" font-family="Syne, sans-serif" font-weight="700" font-size="11" fill="#0b1f2a">82%</text>
</svg>
<div class="mini-globe-wrap reveal reveal-delay-3">
<div class="map-outline">
<svg viewBox="0 0 100 130" xmlns="http://www.w3.org/2000/svg">
<path d="M50 4 L78 14 L90 34 L82 56 L88 82 L62 110 L36 100 L16 76 L18 46 Z"
fill="rgba(26,127,196,.12)"
stroke="var(--sea)"
stroke-width="2.5"
stroke-linejoin="round"/>
<path d="M50 4 L78 14 L90 34 L82 56 L88 82 L62 110 L36 100 L16 76 L18 46 Z"
fill="none"
stroke="var(--leaf)"
stroke-width="1.5"
stroke-dasharray="5 4"
stroke-linejoin="round"
transform="translate(4,-2) scale(0.96) translate(-2,2)"/>
</svg>
</div>
</div>
</div>
@ -172,50 +149,29 @@
<div class="container register__inner">
<div class="register__intro reveal">
<p class="section-label">Play</p>
<h2 class="section-title">Create or join<br>a lobby</h2>
<p class="section-label">Join the game</p>
<h2 class="section-title">Create your<br>lobby</h2>
<p class="register__desc">
Create your own lobby and share the name with friends, or enter an existing lobby name to join their game.
Create a private lobby to challenge friends or play solo.
</p>
</div>
<div class="register__card reveal reveal-delay-1">
<p class="card-title">Create a game</p>
<div class="lobby-toggle" role="group" aria-label="Lobby action">
<button type="button" class="lobby-toggle__btn lobby-toggle__btn--active" id="toggle-create-btn">Create</button>
<button type="button" class="lobby-toggle__btn" id="toggle-join-btn">Join</button>
</div>
<div id="form-create">
<p class="card-title">Create a game</p>
<div class="form">
<div class="field">
<label for="lobby-create">Lobby name</label>
<input id="lobby-create" type="text" placeholder="myLobby" />
</div>
<button type="button" class="btn btn--primary btn--full" id="reg-btn">
Create game
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="field-error" id="lobby-error" role="alert"></span>
<form class="form">
<div class="field">
<label for="username">Lobby name</label>
<input id="username" type="text" placeholder="myLobby" />
</div>
</div>
<div id="form-join" hidden>
<p class="card-title">Join a game</p>
<div class="form">
<div class="field">
<label for="lobby-join">Lobby name</label>
<input id="lobby-join" type="text" placeholder="myLobby" />
</div>
<button type="button" class="btn btn--primary btn--full" id="join-btn">
Join game
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="field-error" id="join-error" role="alert"></span>
</div>
</div>
<button type="button" class="btn btn--primary btn--full" id="reg-btn">
Create game
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<p class="form-footer"><a href="#" style="color:var(--sea);font-weight:600;">Join a game instead</a></p>
</form>
</div>
</div>
@ -225,10 +181,19 @@
<footer class="footer">
<div class="container footer__inner">
<div class="footer__left">
</div>
<div class="footer__center">
<a href="#hero" class="footer__brand">GeoDraw</a>
<p class="footer__sub">Browser geography game</p>
</div>
<div class="footer__right">
</div>
</div>
</footer>
@ -236,4 +201,4 @@
<script type="module" src="scripts/index.js"></script>
</body>
</html>
</html>

View File

@ -91,4 +91,4 @@
<script type="module" src="scripts/leaderboard.js"></script>
</body>
</html>
</html>

View File

@ -116,4 +116,4 @@
<script type="module" src="scripts/lobby.js"></script>
</body>
</html>
</html>

View File

@ -60,8 +60,12 @@
</div>
<div class="results-actions">
<a href="lobby.html" class="btn btn--primary">Play Again →</a>
<a href="leaderboard.html" class="btn btn--ghost">🏆 Leaderboard</a>
<a href="lobby.html" class="btn btn--primary">
Play Again →
</a>
<a href="leaderboard.html" class="btn btn--ghost">
🏆 Leaderboard
</a>
</div>
</div>
@ -70,7 +74,7 @@
<footer class="footer">
<div class="container footer__inner">
<div class="footer__left">
<div>
<a href="index.html" class="footer__brand">GeoDraw</a>
<p class="footer__sub">© 2026 · Browser geography game</p>
</div>
@ -94,4 +98,4 @@
<script type="module" src="scripts/results.js"></script>
</body>
</html>
</html>

View File

@ -3,83 +3,37 @@
import { createLobby } from "./api.js";
import { saveLobbyName } from "./storage.js";
// ── DOM refs
const formCreate = document.getElementById("form-create");
const formJoin = document.getElementById("form-join");
const createInput = /** @type {HTMLInputElement|null} */ (document.getElementById("lobby-create"));
const joinInput = /** @type {HTMLInputElement|null} */ (document.getElementById("lobby-join"));
const createBtn = /** @type {HTMLButtonElement|null} */ (document.getElementById("reg-btn"));
const joinBtn = /** @type {HTMLButtonElement|null} */ (document.getElementById("join-btn"));
const createError = document.getElementById("lobby-error");
const joinError = document.getElementById("join-error");
const toggleCreateBtn = /** @type {HTMLButtonElement|null} */ (document.getElementById("toggle-create-btn"));
const toggleJoinBtn = /** @type {HTMLButtonElement|null} */ (document.getElementById("toggle-join-btn"));
// ── Toggle switch
function showCreate() {
formCreate?.removeAttribute("hidden");
formJoin?.setAttribute("hidden", "");
toggleCreateBtn?.classList.add("lobby-toggle__btn--active");
toggleJoinBtn?.classList.remove("lobby-toggle__btn--active");
createInput?.focus();
}
function showJoin() {
formJoin?.removeAttribute("hidden");
formCreate?.setAttribute("hidden", "");
toggleJoinBtn?.classList.add("lobby-toggle__btn--active");
toggleCreateBtn?.classList.remove("lobby-toggle__btn--active");
joinInput?.focus();
}
toggleCreateBtn?.addEventListener("click", showCreate);
toggleJoinBtn?.addEventListener("click", showJoin);
// ── Create lobby
createBtn?.addEventListener("click", async () => {
if (!createInput || !createError) return;
const lobbyName = createInput.value.trim();
createError.textContent = "";
/**
* Handle the "Create game" button click.
* Validates the lobby name, persists it, and navigates to the lobby page.
*/
document.getElementById("reg-btn")?.addEventListener("click", async () => {
const lobbyInput = /** @type {HTMLInputElement|null} */ (
document.getElementById("username")
);
const button = /** @type {HTMLButtonElement|null} */ (
document.getElementById("reg-btn")
);
const lobbyName = lobbyInput ? lobbyInput.value.trim() : "";
if (!lobbyName) {
createInput.focus();
lobbyInput?.focus();
return;
}
try {
createBtn.disabled = true;
if (button) button.disabled = true;
await createLobby(lobbyName);
saveLobbyName(lobbyName);
window.location.href = "lobby.html";
} catch (error) {
createError.textContent =
error instanceof Error ? error.message : "Could not create lobby.";
createBtn.disabled = false;
alert(error instanceof Error ? error.message : "Could not create lobby.");
if (button) button.disabled = false;
}
});
// ── Join lobby — actual joinLobby() API call happens in lobby.js
joinBtn?.addEventListener("click", () => {
if (!joinInput || !joinError) return;
const lobbyName = joinInput.value.trim();
joinError.textContent = "";
if (!lobbyName) {
joinInput.focus();
return;
}
saveLobbyName(lobbyName);
window.location.href = "lobby.html";
});
// Allow Enter key on both inputs
createInput?.addEventListener("keydown", (e) => { if (e.key === "Enter") createBtn?.click(); });
joinInput?.addEventListener("keydown", (e) => { if (e.key === "Enter") joinBtn?.click(); });
// ── Scroll reveal
// Scroll reveal - animate elements into view as they enter the viewport
const reveals = document.querySelectorAll(".reveal");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
@ -92,4 +46,6 @@ const observer = new IntersectionObserver(
{ threshold: 0.12 },
);
document.querySelectorAll(".reveal").forEach((el) => observer.observe(el));
reveals.forEach((el) => {
observer.observe(el);
});

View File

@ -1,8 +1,5 @@
/* game.css */
/*
GAME LAYOUT
*/
.game-layout {
min-height: calc(100vh - var(--hh));
display: grid;
@ -11,10 +8,7 @@
gap: 16px;
}
/*
TOP BAR
*/
/* ── Top bar */
.game-topbar {
display: grid;
grid-template-columns: 1fr auto 1fr;
@ -22,7 +16,6 @@
gap: 16px;
}
/* Round pips */
.game-round {
display: flex;
align-items: center;
@ -37,8 +30,12 @@
transition: background 0.3s;
}
.round-pip--active { background: var(--sea); }
.round-pip--done { background: var(--leaf); }
.round-pip.active {
background: var(--sea);
}
.round-pip.done {
background: var(--leaf);
}
.round-label {
font-family: "Syne", sans-serif;
@ -116,20 +113,29 @@
width: 100%;
background: linear-gradient(90deg, var(--sea), var(--leaf));
border-radius: 999px;
transition: width 1s linear, background 0.4s ease;
transition:
width 1s linear,
background 0.4s ease;
}
/* Timer urgency states — toggled via JS */
.timer--warning .timer-num { color: var(--gold); }
.timer--warning .timer-bar { background: var(--gold); }
/* Timer urgency states — applied via JS classList */
.timer--warning .timer-bar,
.timer--warning .timer-num {
color: var(--gold);
}
.timer--warning .timer-bar {
background: var(--gold);
}
.timer--danger .timer-num { color: var(--danger); }
.timer--danger .timer-bar { background: var(--danger); }
.timer--danger .timer-bar,
.timer--danger .timer-num {
color: var(--danger);
}
.timer--danger .timer-bar {
background: var(--danger);
}
/*
CANVAS AREA
*/
/* ── Canvas area */
.canvas-area {
display: grid;
grid-template-columns: 1fr 220px;
@ -187,16 +193,15 @@
font-size: 2rem;
font-weight: 800;
opacity: 0;
transition: opacity 0.4s ease, transform 0.4s ease;
transition:
opacity 0.4s ease,
transform 0.4s ease;
pointer-events: none;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
white-space: nowrap;
}
/*
SIDEBAR
*/
/* Sidebar */
.game-sidebar {
display: flex;
flex-direction: column;
@ -221,7 +226,7 @@
margin-bottom: 10px;
}
/* Round score rows */
/* Round scores */
.round-scores {
display: grid;
gap: 8px;
@ -251,7 +256,7 @@
color: var(--sea);
}
/* Player info */
/* Player name in sidebar */
.player-name-display {
font-family: "Syne", sans-serif;
font-weight: 700;
@ -260,25 +265,13 @@
word-break: break-word;
}
.sidebar-hint {
font-size: 0.82rem;
color: var(--ink-soft);
line-height: 1.55;
}
/*
CONTROLS
*/
/* Controls */
.game-controls {
display: flex;
gap: 10px;
}
/*
RESPONSIVE
*/
/* ─── Responsive ─── */
@media (max-width: 900px) {
.canvas-area {
grid-template-columns: 1fr;
@ -290,9 +283,17 @@
flex-wrap: wrap;
}
.sidebar-card { flex: 1 1 140px; }
.canvas-wrap { width: min(100%, 72vh); }
.game-topbar { grid-template-columns: auto 1fr auto; }
.sidebar-card {
flex: 1 1 140px;
}
.canvas-wrap {
width: min(100%, 72vh);
}
.game-topbar {
grid-template-columns: auto 1fr auto;
}
}
@media (max-width: 600px) {
@ -307,17 +308,36 @@
gap: 10px;
}
.game-country { text-align: left; }
.game-timer { align-items: flex-start; }
.timer-track { width: 100%; }
.canvas-wrap { width: 100%; }
.game-country {
text-align: left;
}
.game-timer {
align-items: flex-start;
}
.timer-track {
width: 100%;
}
.game-controls { flex-direction: column; }
.game-controls .btn { width: 100%; }
.canvas-wrap {
width: 100%;
}
.game-controls {
flex-direction: column;
}
.game-controls .btn {
width: 100%;
}
}
@media (max-width: 360px) {
.country-name { font-size: 1.4rem; }
.timer-num { font-size: 1.6rem; }
.canvas-wrap { min-height: 0; }
}
.country-name {
font-size: 1.4rem;
}
.timer-num {
font-size: 1.6rem;
}
.canvas-wrap {
min-height: 0;
}
}

View File

@ -1,8 +1,6 @@
/* index.css — landing page specific styles */
/*
EYEBROW BADGE
*/
/* ─── EYEBROW ─── */
.eyebrow {
display: inline-flex;
align-items: center;
@ -28,7 +26,8 @@
}
@keyframes pulse {
0%, 100% {
0%,
100% {
transform: scale(1);
opacity: 1;
}
@ -38,10 +37,7 @@
}
}
/*
HERO SECTION
*/
/* ─── HERO ─── */
.hero {
min-height: calc(100vh - var(--hh));
display: grid;
@ -88,10 +84,7 @@
flex-wrap: wrap;
}
/*
GLOBE CARD
*/
/* ─── GLOBE CARD ─── */
.globe-card {
background: var(--glass);
backdrop-filter: blur(12px);
@ -113,11 +106,18 @@
inset: 0;
border-radius: var(--r-xl);
background:
radial-gradient(circle at 30% 25%, rgba(65, 184, 105, 0.18), transparent 50%),
radial-gradient(circle at 75% 75%, rgba(26, 127, 196, 0.15), transparent 50%);
radial-gradient(
circle at 30% 25%,
rgba(65, 184, 105, 0.18),
transparent 50%
),
radial-gradient(
circle at 75% 75%,
rgba(26, 127, 196, 0.15),
transparent 50%
);
}
/* Globe sphere */
.globe {
position: relative;
z-index: 1;
@ -137,22 +137,20 @@
0 30px 60px rgba(7, 47, 82, 0.28);
}
/* Continents */
.continent {
position: absolute;
background: #52c870;
border-radius: 46% 54% 58% 42%;
}
.continent--a {
.c1 {
width: 30%;
height: 22%;
top: 18%;
left: 12%;
transform: rotate(-14deg);
}
.continent--b {
.c2 {
width: 18%;
height: 28%;
top: 30%;
@ -160,16 +158,14 @@
transform: rotate(8deg);
border-radius: 40% 60% 50% 50%;
}
.continent--c {
.c3 {
width: 22%;
height: 16%;
top: 56%;
right: 15%;
transform: rotate(22deg);
}
.continent--d {
.c4 {
width: 12%;
height: 18%;
top: 68%;
@ -178,7 +174,6 @@
border-radius: 50%;
}
/* Globe grid lines */
.globe-line {
position: absolute;
border: 1px solid rgba(255, 255, 255, 0.12);
@ -188,12 +183,25 @@
transform: translate(-50%, -50%);
}
.globe-line--meridian-sm { width: 60%; height: 100%; }
.globe-line--meridian-lg { width: 88%; height: 100%; }
.globe-line--parallel-sm { width: 100%; height: 55%; top: 50%; }
.globe-line--parallel-lg { width: 100%; height: 80%; top: 50%; }
.gl1 {
width: 60%;
height: 100%;
}
.gl2 {
width: 88%;
height: 100%;
}
.gl3 {
width: 100%;
height: 55%;
top: 50%;
}
.gl4 {
width: 100%;
height: 80%;
top: 50%;
}
/* Globe floating badges */
.globe-badge {
position: absolute;
z-index: 2;
@ -206,30 +214,43 @@
display: flex;
align-items: center;
gap: 8px;
animation: float-badge 3s ease-in-out infinite;
animation: floatBadge 3s ease-in-out infinite;
}
.globe-badge--score { top: 14%; right: 6%; animation-delay: 0s; }
.globe-badge--country { bottom: 14%; left: 4%; animation-delay: 1.2s; }
@keyframes float-badge {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
.gb--score {
top: 14%;
right: 6%;
animation-delay: 0s;
}
.gb--country {
bottom: 14%;
left: 4%;
animation-delay: 1.2s;
}
.globe-badge__dot {
@keyframes floatBadge {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
.gb__dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.gb__dot--green {
background: var(--leaf);
}
.gb__dot--blue {
background: var(--sea);
}
.globe-badge__dot--green { background: var(--leaf); }
.globe-badge__dot--blue { background: var(--sea); }
/*
ABOUT SECTION
*/
/* ─── ABOUT ─── */
.about {
padding: 100px 0;
}
@ -248,8 +269,7 @@
margin-bottom: 36px;
}
/* Game preview card */
.game-preview {
.mini-globe-wrap {
background: var(--glass);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.8);
@ -265,25 +285,19 @@
overflow: hidden;
}
.game-preview::before {
.mini-globe-wrap::before {
content: "";
position: absolute;
inset: 0;
border-radius: var(--r-xl);
background: radial-gradient(circle at 60% 40%, rgba(26, 127, 196, 0.1), transparent 50%);
background: radial-gradient(
circle at 60% 40%,
rgba(26, 127, 196, 0.1),
transparent 50%
);
}
.game-preview__svg {
width: 100%;
height: auto;
position: relative;
z-index: 1;
}
/*
STEPS LIST
*/
/* ─── STEPS ─── */
.steps {
display: grid;
gap: 16px;
@ -300,7 +314,9 @@
border-radius: var(--r-lg);
padding: 22px 26px;
box-shadow: 0 4px 20px rgba(11, 31, 42, 0.06);
transition: transform 0.2s, box-shadow 0.2s;
transition:
transform 0.2s,
box-shadow 0.2s;
}
.step:hover {
@ -336,10 +352,7 @@
line-height: 1.5;
}
/*
REGISTER SECTION
*/
/* ─── REGISTER ─── */
.register {
padding: 100px 0;
}
@ -381,40 +394,7 @@
text-align: center;
}
/* Lobby toggle */
.lobby-toggle {
display: flex;
background: var(--cream);
border: 1.5px solid var(--line);
border-radius: 999px;
padding: 4px;
gap: 4px;
margin-bottom: 24px;
}
.lobby-toggle__btn {
flex: 1;
padding: 9px 16px;
border-radius: 999px;
border: none;
background: transparent;
font-family: "Syne", sans-serif;
font-weight: 700;
font-size: 0.88rem;
color: var(--ink-muted);
cursor: pointer;
transition: background 0.18s, color 0.18s;
}
.lobby-toggle__btn--active {
background: var(--ink);
color: var(--white);
}
/*
RESPONSIVE
*/
/* ─── RESPONSIVE ─── */
@media (max-width: 960px) {
.hero__inner,
.about__inner,
@ -423,20 +403,35 @@
gap: 40px;
}
.globe-card { min-height: 360px; }
.game-preview { max-width: 260px; }
.globe-card {
min-height: 360px;
}
.mini-globe-wrap {
max-width: 260px;
}
}
@media (max-width: 680px) {
.hero { padding: 50px 0 40px; }
.hero {
padding: 50px 0 40px;
}
.about,
.register { padding: 70px 0; }
.register__card { padding: 28px 22px; }
.globe-card { padding: 24px; min-height: 280px; }
.register {
padding: 70px 0;
}
.register__card {
padding: 28px 22px;
}
.globe-card {
padding: 24px;
min-height: 280px;
}
}
@media (max-width: 420px) {
.hero__title { font-size: 3rem; }
.hero__title {
font-size: 3rem;
}
}
@media (max-width: 360px) {
@ -444,28 +439,49 @@
padding: 36px 0 32px;
min-height: auto;
}
.hero__title {
font-size: 2.6rem;
letter-spacing: -0.03em;
}
.hero__desc {
font-size: 0.95rem;
margin-bottom: 28px;
}
.globe-card { padding: 16px; min-height: 240px; }
.globe-badge { padding: 7px 10px; font-size: 0.72rem; }
.globe-card {
padding: 16px;
min-height: 240px;
}
.globe-badge {
padding: 7px 10px;
font-size: 0.72rem;
}
.about,
.register { padding: 52px 0; }
.about__desc { font-size: 0.95rem; }
.step { padding: 16px 18px; gap: 14px; }
.step__num { width: 46px; height: 46px; font-size: 0.88rem; border-radius: 12px; }
.register__card { padding: 22px 16px; border-radius: var(--r-lg); }
.card-title { font-size: 1.25rem; margin-bottom: 22px; }
.btn--full { padding: 14px 18px; font-size: 0.95rem; }
}
.register {
padding: 52px 0;
}
.about__desc {
font-size: 0.95rem;
}
.step {
padding: 16px 18px;
gap: 14px;
}
.step__num {
width: 46px;
height: 46px;
font-size: 0.88rem;
border-radius: 12px;
}
.register__card {
padding: 22px 16px;
border-radius: var(--r-lg);
}
.card-title {
font-size: 1.25rem;
margin-bottom: 22px;
}
.btn--full {
padding: 14px 18px;
font-size: 0.95rem;
}
}

View File

@ -1,8 +1,5 @@
/* leader.css — leaderboard page styles */
/*
PAGE LAYOUT
*/
.lb-page {
min-height: calc(100vh - var(--hh));
padding: 60px 0;
@ -44,10 +41,7 @@
flex-wrap: wrap;
}
/*
TABLE
*/
/* ─── TABLE ─── */
.lb-table {
background: var(--white);
border: 1px solid var(--line);
@ -72,7 +66,9 @@
color: var(--ink-muted);
}
.lb-th--right { text-align: right; }
.lb-th.right {
text-align: right;
}
.lb-body {
display: grid;
@ -85,23 +81,38 @@
padding: 16px 24px;
border-bottom: 1px solid var(--line);
transition: background 0.15s;
animation: fade-row 0.4s ease both;
animation: fadeRow 0.4s ease both;
}
.lb-row:last-child { border-bottom: none; }
.lb-row:hover { background: var(--cream); }
@keyframes fade-row {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
.lb-row:last-child {
border-bottom: none;
}
.lb-row:hover {
background: var(--cream);
}
/* Top 3 row highlights */
.lb-row--rank-1 { background: rgba(240, 180, 40, 0.07); }
.lb-row--rank-2 { background: rgba(180, 180, 195, 0.06); }
.lb-row--rank-3 { background: rgba(205, 130, 70, 0.05); }
@keyframes fadeRow {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ─── TOP 3 HIGHLIGHT ─── */
.lb-row.rank-1 {
background: rgba(240, 180, 40, 0.07);
}
.lb-row.rank-2 {
background: rgba(180, 180, 195, 0.06);
}
.lb-row.rank-3 {
background: rgba(205, 130, 70, 0.05);
}
/* Rank number */
.lb-rank {
font-family: "Syne", sans-serif;
font-weight: 800;
@ -109,13 +120,20 @@
color: var(--ink-muted);
}
.lb-row--rank-1 .lb-rank { color: var(--gold); }
.lb-row--rank-2 .lb-rank { color: #a0a0b0; }
.lb-row--rank-3 .lb-rank { color: #c07840; }
.lb-row.rank-1 .lb-rank {
color: var(--gold);
}
.lb-row.rank-2 .lb-rank {
color: #a0a0b0;
}
.lb-row.rank-3 .lb-rank {
color: #c07840;
}
.lb-medal { font-size: 1.1rem; }
.lb-medal {
font-size: 1.1rem;
}
/* Player name */
.lb-name {
font-family: "Syne", sans-serif;
font-weight: 700;
@ -126,7 +144,7 @@
white-space: nowrap;
}
.lb-name--you::after {
.lb-name.is-you::after {
content: " (you)";
font-weight: 400;
font-family: "DM Sans", sans-serif;
@ -146,7 +164,6 @@
text-align: right;
}
/* Score value */
.lb-score {
font-family: "Syne", sans-serif;
font-weight: 800;
@ -155,14 +172,17 @@
text-align: right;
}
.lb-score--gold { color: var(--gold); }
.lb-score--silver { color: #888898; }
.lb-score--bronze { color: #c07840; }
.lb-score.gold {
color: var(--gold);
}
.lb-score.silver {
color: #888898;
}
.lb-score.bronze {
color: #c07840;
}
/*
EMPTY STATE
*/
/* ─── EMPTY STATE ─── */
.lb-empty {
padding: 64px 24px;
text-align: center;
@ -179,10 +199,7 @@
margin-bottom: 20px;
}
/*
CURRENT PLAYER BAR
*/
/* ─── CURRENT PLAYER BAR ─── */
.your-score-bar {
margin-top: 20px;
padding: 16px 24px;
@ -200,26 +217,31 @@
font-size: 0.9rem;
color: var(--ink-soft);
}
.your-score-bar__text strong {
color: var(--ink);
}
.your-score-bar__text strong { color: var(--ink); }
.your-score-bar__text--score strong { color: var(--sea); }
/*
RESPONSIVE
*/
/* ─── RESPONSIVE ─── */
@media (max-width: 640px) {
.lb-page { padding: 40px 0; }
.lb-page {
padding: 40px 0;
}
.lb-table-head,
.lb-row { grid-template-columns: 44px 1fr 72px; }
.lb-row {
grid-template-columns: 44px 1fr 72px;
}
.lb-th--hide-sm,
.lb-th.hide-sm,
.lb-rounds,
.lb-date { display: none; }
.lb-date {
display: none;
}
}
@media (max-width: 360px) {
.lb-table-head,
.lb-row { padding: 12px 16px; }
}
.lb-row {
padding: 12px 16px;
}
}

View File

@ -1,8 +1,5 @@
/* lobby.css */
/*
LOBBY LAYOUT
*/
.lobby {
min-height: calc(100vh - var(--hh));
display: grid;
@ -18,12 +15,33 @@
width: 100%;
}
/* ─── Lobby name badge ─── */
.lobby__name-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border-radius: var(--r-lg);
background: var(--glass);
border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: var(--shadow-sm);
backdrop-filter: blur(10px);
margin-bottom: 20px;
}
/*
PROMO (LEFT COLUMN)
*/
.lobby__name-icon {
font-size: 1.1rem;
}
/* Eyebrow badge — mirrors index.css for standalone page loading */
#lobby-name-display {
font-family: "Syne", sans-serif;
font-weight: 700;
font-size: 1rem;
color: var(--ink);
letter-spacing: -0.01em;
}
/* ─── EYEBROW ─── */
.eyebrow {
display: inline-flex;
align-items: center;
@ -49,35 +67,17 @@
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.45); opacity: 0.7; }
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.45);
opacity: 0.7;
}
}
/* Lobby name badge */
.lobby__name-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border-radius: var(--r-lg);
background: var(--glass);
border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: var(--shadow-sm);
backdrop-filter: blur(10px);
margin-bottom: 20px;
}
.lobby__name-icon { font-size: 1.1rem; }
#lobby-name-display {
font-family: "Syne", sans-serif;
font-weight: 700;
font-size: 1rem;
color: var(--ink);
letter-spacing: -0.01em;
}
/* Title */
.lobby__title {
font-family: "Syne", sans-serif;
font-weight: 800;
@ -95,7 +95,6 @@
background-clip: text;
}
/* Description */
.lobby__desc {
font-size: 1.02rem;
line-height: 1.72;
@ -104,7 +103,6 @@
margin-bottom: 32px;
}
/* Info badges */
.lobby__badges {
display: flex;
flex-wrap: wrap;
@ -126,12 +124,11 @@
backdrop-filter: blur(8px);
}
.badge__icon { font-size: 1rem; }
.badge__icon {
font-size: 1rem;
}
/*
FORM CARD (RIGHT COLUMN)
*/
/* Right side card */
.lobby__card {
background: var(--white);
border: 1px solid var(--line);
@ -154,22 +151,30 @@
margin-bottom: 30px;
}
/* Form and field styles live in main.css */
/* Form and field styles are defined in main.css */
/*
RESPONSIVE
*/
/* ─── Responsive ─── */
@media (max-width: 900px) {
.lobby__inner { grid-template-columns: 1fr; gap: 40px; }
.lobby__inner {
grid-template-columns: 1fr;
gap: 40px;
}
}
@media (max-width: 680px) {
.lobby { padding: 44px 0; }
.lobby__card { padding: 28px 22px; }
.lobby {
padding: 44px 0;
}
.lobby__card {
padding: 28px 22px;
}
}
@media (max-width: 360px) {
.lobby__card { padding: 22px 16px; }
.lobby__title { font-size: 2.4rem; }
}
.lobby__card {
padding: 22px 16px;
}
.lobby__title {
font-size: 2.4rem;
}
}

View File

@ -2,49 +2,30 @@
@import url("https://fonts.googleapis.com/css2?family=Syne:wght@400;500;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600&display=swap");
/*
DESIGN TOKENS
*/
:root {
/* Colors */
--ink: #0b1f2a;
--ink-soft: #3d5563;
--ink: #0b1f2a;
--ink-soft: #3d5563;
--ink-muted: #7a9aaa;
--sea: #1a7fc4;
--sea: #1a7fc4;
--sea-light: #4faae0;
--sea-dim: rgba(26, 127, 196, 0.12);
--leaf: #41b869;
--leaf-dim: rgba(65, 184, 105, 0.13);
--gold: #f0b429;
--danger: #e05c5c;
--cream: #f4f9f6;
--white: #ffffff;
--sea-dim: rgba(26, 127, 196, 0.12);
--leaf: #41b869;
--leaf-dim: rgba(65, 184, 105, 0.13);
--gold: #f0b429;
--danger: #e05c5c;
--cream: #f4f9f6;
--white: #ffffff;
--glass: rgba(255, 255, 255, 0.72);
--line: rgba(11, 31, 42, 0.09);
/* Shadows */
--shadow: 0 24px 60px rgba(11, 31, 42, 0.10);
--shadow-sm: 0 4px 20px rgba(11, 31, 42, 0.07);
/* Radii */
--line: rgba(11, 31, 42, 0.09);
--shadow: 0 24px 60px rgba(11, 31, 42, 0.1);
--shadow-sm: 0 4px 20px rgba(11, 31, 42, 0.07);
--r-xl: 32px;
--r-lg: 22px;
--r-md: 14px;
--r-sm: 8px;
/* Layout */
--hh: 72px;
}
/*
RESET
*/
*,
*::before,
*::after {
@ -53,7 +34,9 @@
padding: 0;
}
html { scroll-behavior: smooth; }
html {
scroll-behavior: smooth;
}
body {
font-family: "DM Sans", sans-serif;
@ -64,28 +47,25 @@ body {
min-width: 320px;
}
a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
button, input { font: inherit; }
/*
BACKGROUND DECORATIONS
*/
/* Ambient colour blobs */
body::before {
content: "";
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 60% 50% at 10% 0%, rgba(65, 184, 105, 0.1), transparent 60%),
radial-gradient(ellipse 50% 60% at 90% 100%, rgba(26, 127, 196, 0.1), transparent 60%);
radial-gradient(
ellipse 60% 50% at 10% 0%,
rgba(65, 184, 105, 0.1) 0%,
transparent 60%
),
radial-gradient(
ellipse 50% 60% at 90% 100%,
rgba(26, 127, 196, 0.1) 0%,
transparent 60%
);
pointer-events: none;
z-index: 0;
}
/* Grid overlay */
body::after {
content: "";
position: fixed;
@ -98,15 +78,25 @@ body::after {
z-index: 0;
}
a {
color: inherit;
text-decoration: none;
}
img,
svg {
display: block;
max-width: 100%;
}
button,
input {
font: inherit;
}
.wrap {
position: relative;
z-index: 1;
}
/*
LAYOUT
*/
.container {
width: 100%;
max-width: 1280px;
@ -114,10 +104,6 @@ body::after {
padding: 0 28px;
}
/*
HEADER
*/
.header {
position: sticky;
top: 0;
@ -138,7 +124,6 @@ body::after {
gap: 24px;
}
/* Logo */
.logo {
display: inline-flex;
align-items: center;
@ -170,7 +155,6 @@ body::after {
letter-spacing: -0.02em;
}
/* Nav */
.nav {
display: flex;
align-items: center;
@ -183,7 +167,9 @@ body::after {
font-size: 0.92rem;
font-weight: 500;
color: var(--ink-soft);
transition: background 0.18s, color 0.18s;
transition:
background 0.18s,
color 0.18s;
}
.nav__link:hover {
@ -199,7 +185,9 @@ body::after {
font-weight: 600;
color: var(--white);
background: var(--ink);
transition: opacity 0.18s, transform 0.18s;
transition:
opacity 0.18s,
transform 0.18s;
}
.nav__cta:hover {
@ -207,10 +195,7 @@ body::after {
transform: translateY(-1px);
}
/*
BUTTONS
*/
/* ─── BUTTONS ─── */
.btn {
display: inline-flex;
align-items: center;
@ -223,29 +208,48 @@ body::after {
font-weight: 600;
border: none;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s;
transition:
transform 0.2s,
box-shadow 0.2s,
opacity 0.2s;
line-height: 1;
}
.btn:hover:not(:disabled) { transform: translateY(-2px); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn:hover:not(:disabled) {
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn--primary {
color: var(--white);
background: linear-gradient(135deg, var(--sea) 0%, #159fd4 50%, var(--leaf) 100%);
background: linear-gradient(
135deg,
var(--sea) 0%,
#159fd4 50%,
var(--leaf) 100%
);
background-size: 200% 200%;
box-shadow: 0 8px 24px rgba(26, 127, 196, 0.28);
animation: grad-shift 4s ease infinite;
animation: gradShift 4s ease infinite;
}
.btn--primary:hover:not(:disabled) {
box-shadow: 0 12px 32px rgba(26, 127, 196, 0.4);
}
@keyframes grad-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
@keyframes gradShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.btn--ghost {
@ -265,13 +269,17 @@ body::after {
box-shadow: 0 6px 18px rgba(224, 92, 92, 0.25);
}
.btn--full { width: 100%; }
.btn--sm { padding: 10px 20px; font-size: 0.88rem; border-radius: var(--r-md); }
.btn--full {
width: 100%;
}
.btn--sm {
padding: 10px 20px;
font-size: 0.88rem;
border-radius: var(--r-md);
}
/*
CARD
*/
/* ─── CARD ─── */
.card {
background: var(--glass);
backdrop-filter: blur(12px);
@ -280,10 +288,7 @@ body::after {
box-shadow: var(--shadow);
}
/*
SECTION TYPOGRAPHY
*/
/* ─── SECTION LABELS ─── */
.section-label {
display: inline-block;
padding: 5px 13px;
@ -306,12 +311,16 @@ body::after {
margin-bottom: 16px;
}
/* ─── FORM ─── */
.form {
display: grid;
gap: 20px;
}
/*
FORM
*/
.form { display: grid; gap: 20px; }
.field { display: grid; gap: 8px; }
.field {
display: grid;
gap: 8px;
}
.field label {
font-size: 0.84rem;
@ -329,10 +338,14 @@ body::after {
background: var(--cream);
color: var(--ink);
font-size: 0.98rem;
transition: border-color 0.18s, box-shadow 0.18s;
transition:
border-color 0.18s,
box-shadow 0.18s;
}
.field input::placeholder { color: var(--ink-muted); }
.field input::placeholder {
color: var(--ink-muted);
}
.field input:focus {
outline: none;
@ -352,10 +365,7 @@ body::after {
min-height: 1.2em;
}
/*
FOOTER
*/
/* ─── FOOTER ─── */
.footer {
border-top: 1px solid var(--line);
background: rgba(255, 255, 255, 0.5);
@ -383,7 +393,9 @@ body::after {
margin-top: 4px;
}
.footer__center { text-align: center; }
.footer__center {
text-align: center;
}
.footer__label {
font-size: 0.7rem;
@ -408,27 +420,34 @@ body::after {
transition: color 0.18s;
}
.footer__link:hover { color: var(--ink); }
.footer__link:hover {
color: var(--ink);
}
/*
SCROLL REVEAL
*/
/* ─── SCROLL REVEAL ─── */
.reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.55s ease, transform 0.55s ease;
transition:
opacity 0.55s ease,
transform 0.55s ease;
}
.reveal.visible { opacity: 1; transform: translateY(0); }
.reveal-delay-1 { transition-delay: 0.10s; }
.reveal-delay-2 { transition-delay: 0.20s; }
.reveal-delay-3 { transition-delay: 0.32s; }
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
.reveal-delay-1 {
transition-delay: 0.1s;
}
.reveal-delay-2 {
transition-delay: 0.2s;
}
.reveal-delay-3 {
transition-delay: 0.32s;
}
/*
NOSCRIPT
*/
/* ─── NOSCRIPT ─── */
.noscript-msg {
padding: 1.5rem 2rem;
background: var(--danger);
@ -437,31 +456,39 @@ body::after {
text-align: center;
}
/*
REDUCED MOTION
*/
/* ─── REDUCED MOTION ─── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/*
RESPONSIVE
*/
/* ─── RESPONSIVE BASE ─── */
@media (max-width: 680px) {
.container { padding: 0 18px; }
.nav__link { display: none; }
.footer__inner { grid-template-columns: 1fr; }
.container {
padding: 0 18px;
}
.nav__link {
display: none;
}
.footer__inner {
grid-template-columns: 1fr;
}
.footer__center,
.footer__right { justify-self: start; text-align: left; }
.socials { justify-content: flex-start; }
.footer__right { justify-content: flex-start; }
.footer__right {
justify-self: start;
text-align: left;
}
.socials {
justify-content: flex-start;
}
.footer__right {
justify-content: flex-start;
}
}
@media (max-width: 360px) {
@ -469,9 +496,18 @@ body::after {
--r-xl: 22px;
--r-lg: 16px;
}
.container { padding: 0 14px; }
.logo__mark { width: 34px; height: 34px; }
.logo__text { font-size: 1.05rem; }
.nav__cta { padding: 7px 12px; font-size: 0.82rem; }
}
.container {
padding: 0 14px;
}
.logo__mark {
width: 34px;
height: 34px;
}
.logo__text {
font-size: 1.05rem;
}
.nav__cta {
padding: 7px 12px;
font-size: 0.82rem;
}
}

View File

@ -1,8 +1,5 @@
/* results.css — results page styles */
/*
PAGE LAYOUT
*/
.results-page {
min-height: calc(100vh - var(--hh));
display: grid;
@ -17,10 +14,7 @@
gap: 24px;
}
/*
HEADER
*/
/* ─── HEADER ─── */
.results-header {
text-align: center;
}
@ -29,12 +23,18 @@
font-size: 3.5rem;
margin-bottom: 16px;
display: block;
animation: pop-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
animation: popIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes pop-in {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
@keyframes popIn {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.results-title {
@ -46,26 +46,40 @@
margin-bottom: 8px;
}
.results-player { font-size: 1rem; color: var(--ink-soft); }
.results-player strong { color: var(--ink); }
.results-player {
font-size: 1rem;
color: var(--ink-soft);
}
.results-player strong {
color: var(--ink);
}
/*
TOTAL SCORE CARD
*/
/* ─── TOTAL SCORE ─── */
.score-total-card {
background: linear-gradient(135deg, var(--sea) 0%, #159fd4 50%, var(--leaf) 100%);
background: linear-gradient(
135deg,
var(--sea) 0%,
#159fd4 50%,
var(--leaf) 100%
);
border-radius: var(--r-xl);
padding: 36px 40px;
text-align: center;
color: var(--white);
box-shadow: 0 16px 40px rgba(26, 127, 196, 0.3);
animation: slide-up 0.5s 0.1s cubic-bezier(0.34, 1.56, 0.64, 1) both;
animation: slideUp 0.5s 0.1s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes slide-up {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
@keyframes slideUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.score-total-label {
@ -103,17 +117,14 @@
letter-spacing: 0.04em;
}
/*
ROUND BREAKDOWN CARD
*/
/* ─── ROUND BREAKDOWN ─── */
.rounds-card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-xl);
padding: 28px 32px;
box-shadow: var(--shadow-sm);
animation: slide-up 0.5s 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) both;
animation: slideUp 0.5s 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
.rounds-card-title {
@ -125,7 +136,10 @@
color: var(--ink-soft);
}
.round-rows { display: grid; gap: 14px; }
.round-rows {
display: grid;
gap: 14px;
}
.round-row {
display: grid;
@ -164,7 +178,7 @@
text-align: right;
}
/* Country tags */
/* ─── COUNTRY TAGS ─── */
.countries-row {
display: flex;
flex-wrap: wrap;
@ -181,30 +195,36 @@
font-weight: 600;
}
/*
ACTIONS
*/
/* ─── ACTIONS ─── */
.results-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
animation: slide-up 0.5s 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
animation: slideUp 0.5s 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
.results-actions .btn { flex: 1 1 180px; }
.results-actions .btn {
flex: 1 1 180px;
}
/*
RESPONSIVE
*/
/* ─── RESPONSIVE ─── */
@media (max-width: 600px) {
.results-page { padding: 40px 0; }
.score-total-card { padding: 28px 24px; }
.rounds-card { padding: 22px 20px; }
.results-actions .btn { flex-basis: 100%; }
.results-page {
padding: 40px 0;
}
.score-total-card {
padding: 28px 24px;
}
.rounds-card {
padding: 22px 20px;
}
.results-actions .btn {
flex-basis: 100%;
}
}
@media (max-width: 360px) {
.score-total-num { font-size: 3.4rem; }
}
.score-total-num {
font-size: 3.4rem;
}
}