diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..c6c4b69 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "heredita-site", + "runtimeExecutable": "node", + "runtimeArgs": ["scripts/dev-server.js", "8765"], + "port": 8765 + } + ] +} diff --git a/.gitignore b/.gitignore index 97aad37..ac2ea16 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ node_modules/ # Local Netlify folder .netlify + +# Machine-local Claude Code permission state (launch.json is shared, this isn't) +.claude/settings.local.json diff --git a/ADMIN-CMS.md b/ADMIN-CMS.md new file mode 100644 index 0000000..608fb29 --- /dev/null +++ b/ADMIN-CMS.md @@ -0,0 +1,94 @@ +# Editing the Updates page — guide for admins + +The patch notes at **heredita.net/updates** are not written in code. They live in +one file that a small web editor writes for you: **Decap CMS**. + +You do not need Git, an editor, or any developer tooling. You need a browser and +an invite. + +--- + +## 1. Getting an account + +Registration is **invite only** — there is no sign-up form, by design. The site +owner sends you an invite from the Netlify dashboard: + +> Netlify → the Heredita site → **Identity** → **Invite users** → your email + +You get an email titled roughly *"You've been invited to join …"*. Click +**Accept the invite**, set a password, and you're done. That link works once, so +if it expires just ask for another. + +## 2. Signing in + +1. Go to **https://heredita.net/admin/** (bookmark it — nothing on the site links + to it). +2. Enter the email and password from step 1. +3. You land on **Updates / Patch Notes**. + +## 3. Writing a patch note + +Open **All patch notes**. You get a list of entries; the **top one is what +visitors see first**, and it's also the one the home page pulls into its "Latest +patch" strip. + +Click **Add patch note**, then drag it to the top of the list. Fields: + +| Field | What it is | Example | +| --- | --- | --- | +| **Version tag** | Short label on the left of the card | `v0.5` or `Pre-alpha` | +| **Date** | Shown next to the version | `2026-08-14` | +| **Tagline** | Optional short suffix after the date | `Map Pack` | +| **Headline** | The big handwritten title on the card | `Loading screens and dice tweaks` | +| **Bullet list** | One bullet per change | see below | + +For bullets, wrap words in `**` to bold them — that's the only formatting the +page renders: + +``` +**New:** six hand-drawn loading screens. +**Fix:** chat no longer crashes with 4+ players. +``` + +renders as **New:** six hand-drawn loading screens. + +## 4. Publishing + +Press **Publish** (top right). That's it — the change is committed to the +`main` branch on GitHub, which triggers a rebuild, and the new note is live in +roughly a minute. Hard-refresh if you still see the old one. + +**Publishing goes straight live.** There is no draft or review step +(`publish_mode: simple`), so read it once before you press the button. + +## 5. Deleting or reordering + +Same screen. Use the trash icon on an entry to remove it, or drag entries to +reorder. Newest belongs at the top. + +--- + +## Notes for whoever maintains this + +- **What the CMS actually writes:** `site/content/updates.json`. Both + `updates.html` and the home page's "Latest patch" strip fetch that file at + runtime, so a published note updates both. Nothing else needs editing. +- **Schema:** `site/admin/config.yml`. Add a field there and it appears in the + editor. +- **Auth:** Netlify Identity + Git Gateway. The live site is on **Vercel**; + the Netlify project exists only to run Identity and Git Gateway + (see `netlify.toml`) and never serves pages. +- **Two things to confirm in the Netlify dashboard** if login misbehaves: + - Identity → **Registration preferences → Invite only** (the admin page also + hides the sign-up tab in CSS, but that is cosmetic — the server-side + setting is what actually stops a forged sign-up). + - Identity → **Services → Git Gateway** enabled, with repo access to + `Zeyy21/HERE`. +- **Split-hosting setup:** the public site is served from Vercel, while Identity + and Git Gateway live at `capable-sawine-7fa143.netlify.app`. The Decap + backend declares both services explicitly via `backend.identity_url` and + `backend.gateway_url` in `site/admin/config.yml`; the admin page deliberately + does not load the legacy Identity widget. The public landing page still + initializes the widget against the same Identity endpoint so invitation and + recovery links continue to work. Keep these values in sync if the Netlify + project is ever renamed. diff --git a/README.md b/README.md index c157755..59b73b3 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,15 @@ Plain HTML + CSS + vanilla JS. No build step. ## Local dev ```bash -cd site -python -m http.server 8000 -# open http://localhost:8000/ +cd site && python3 -m http.server 8000 +``` + +Then open . There's also a dependency-free Node +equivalent, which serves with caching disabled so edits show up on a plain +reload: + +```bash +node scripts/dev-server.js 8765 ``` ## Deploy @@ -34,9 +40,35 @@ site/ ├── home.html dashboard ├── play.html launch page + preview footage carousel ├── about.html mission + features -├── updates.html patch notes -├── terms.html placeholder T&C +├── updates.html patch notes, rendered from content/updates.json +├── terms.html T&C ├── css/chalkboard.css -├── js/app.js PLAY_URL points to app.heredita.net +├── js/app.js session, play CTAs, carousel; PLAY_URL → app.heredita.net +├── js/nav.js top-nav behaviour (sliding marker, menus, mobile drawer) └── assets/ logos + 5 screenshots ``` + +## Design conventions + +The chalkboard theme leans on one rule that keeps the whole site legible at a +glance: + +| Border | Means | +| --- | --- | +| Solid hairline | A real, finished thing | +| **Dashed** | A sketch — locked, unbuilt, coming soon | +| Accent solid | The thing you're on right now | + +So dashes are never decoration. If something is dashed, it isn't shipped yet. + +Type has three roles: **Caveat** for display headings and the brand, **Special +Elite** for typewritten metadata (dates, versions, stamps, kickers), and +**Cabin** for body copy and every control. + +The header markup is duplicated verbatim across pages (no build step, by +design) — `js/nav.js` only enhances it, so the nav still works with JS off. If +you change a nav item, change it in every page. + +## Legal + +Heredita is operated by Anywhere Connection Corporation. diff --git a/scripts/dev-server.js b/scripts/dev-server.js new file mode 100644 index 0000000..0ffa7e7 --- /dev/null +++ b/scripts/dev-server.js @@ -0,0 +1,62 @@ +/* Static file server for local development. + * + * `python -m http.server` is the simpler option and is what the README + * suggests, but Python 3.9 resolves os.getcwd() while parsing its own + * arguments, so it refuses to start when the launcher's working directory + * isn't readable. This has no such problem and needs no dependencies. + * + * node scripts/dev-server.js [port] + */ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.join(__dirname, '..', 'site'); +const PORT = Number(process.argv[2]) || 8765; + +const TYPES = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.webp': 'image/webp', + '.ico': 'image/x-icon', + '.txt': 'text/plain; charset=utf-8', + '.yml': 'text/yaml; charset=utf-8' +}; + +http.createServer((req, res) => { + let urlPath; + try { + urlPath = decodeURIComponent(req.url.split('?')[0]); + } catch { + res.writeHead(400).end('bad request'); + return; + } + if (urlPath.endsWith('/')) urlPath += 'index.html'; + + const file = path.join(ROOT, path.normalize(urlPath)); + // path.normalize collapses ".." — re-check so a crafted URL can't escape. + if (file !== ROOT && !file.startsWith(ROOT + path.sep)) { + res.writeHead(403).end('forbidden'); + return; + } + + fs.readFile(file, (err, buf) => { + if (err) { + res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.end('404 ' + urlPath); + return; + } + res.writeHead(200, { + 'Content-Type': TYPES[path.extname(file).toLowerCase()] || 'application/octet-stream', + // No caching, so edits show up on reload without a hard refresh. + 'Cache-Control': 'no-store' + }); + res.end(buf); + }); +}).listen(PORT, () => { + console.log('Heredita dev server → http://localhost:' + PORT + '/'); +}); diff --git a/site/about.html b/site/about.html index 5616419..4968e4a 100644 --- a/site/about.html +++ b/site/about.html @@ -5,46 +5,63 @@ Heredita — About Us - +
-
-
- - + -
-
- - 0 - coming soon + Pre-alpha + + + + + + + +
+ + 0 + + + - - - - - -
@@ -54,67 +71,78 @@
Heredita

About Heredita

-

- A playground for geography, history and political enjoyers — built on chalk, dice, and the - stubborn belief that everyone deserves to redraw a border at least once. +

+ A chalkboard for people who argue about borders. Four of us build it.

-

Our Mission

+

Why it exists

+

+ Plenty of map games let you move armies. Very few let you sit down with five other people, + talk for twenty minutes about whether Poland gets to keep Silesia, and then settle it with + a roll everyone can see. +

- Heredita exists so people who love maps can do something with that love. - We want a place where a 1936 Poland enjoyer, a Roman restorationist and an - alt-history Byzantine fan can sit at the same chalkboard and argue with dice in their hand. + That conversation is the game. The map is just where it gets recorded.

-

What is Heredita?

+

What you actually do

-

Roleplay, not RTS

-

You don't move units. You paint borders, declare intent, and let the chat and the dice decide who lives and who annexes.

+

Paint, don't march

+

There are no units to shuffle. You take a brush to the border and redraw it, found a city, or put a nation back on the map that hasn't been there since 1795.

-

Paint the map

-

Brush tools let you redraw, found cities, restore old nations, or invent ones that never existed. The map is the canvas.

+

Rolls stay honest

+

Annexations are 2d6 in front of everyone. Nobody loses a province to a formula they can't see, and nobody wins one either.

-

Custom nations & cultures

-

Design your own flag, language, government, founding myth. Drop them on the map and see how the neighbors react.

+

Invent the rest

+

Flag, government, founding year, the myth you tell about it. Drop it on the map and see how long the neighbours pretend to take it seriously.

-

Alternate timelines

-

Pick a year, change one thing, and play the cascade. Restore Constantinople. Keep Poland off the partition table. Or invent something the textbooks never saw.

+

Change one thing

+

Pick 1936, 1945 or 1991, alter a single decision, and play out what follows. Most of our best games came from someone refusing to sign something.

-

Dice + Chat

-

Outcomes lean on dice rolls and live conversation with other players, governed by a clear set of rules. No black-box combat formulas.

+

Talk it out

+

Chat is in the same window as the map, so treaties, threats and outright lying all happen while you're looking at the thing you're fighting over.

-

Communities & servers

-

Spin up your own server, write your own rules, host your own timeline. Form subgroups, hold congresses, sign treaties.

+

Run your own

+

Host a server, write house rules, hold a congress, keep a hundred-year campaign going. We'd rather you did that than played ours.

-

The team

+

The team

- Heredita is being built by a tiny crew of map nerds. Art and worldbuilding by - the Heredita team; design and code by the rest of us. We're not Roblox — yet — but - we're aiming at the same kind of "open table" for our corner of the internet. + Heredita is made by four people — Whirl, Digi, Legi and Hilmi. We met on the internet + arguing about maps, which is roughly how everyone here ends up meeting.

- Want to follow along, complain about a brush, or pitch a feature? - Reach out through our official support channel, review the Terms & Conditions, - or just keep checking the Updates page. + We wanted somewhere history, geography and politics people could run a country without + it turning into a spreadsheet. Mappers get to draw, historians get to be pedantic, and + the politics enjoyers get an audience. +

+

+ The part we care most about isn't the conquest. It's the group of people who keep showing + up to the same server on a Sunday. +

+

+ Heredita is operated by Anywhere Connection Corporation.

-

Ready to draw?

-

No download. No commitment. Just a chalkboard and a continent.

- ▶ Open the playtest +

Come draw something

+

The playtest is free and open, and you don't need an account to try it.

+ + + Open the playtest +
@@ -122,18 +150,18 @@

Ready to draw?

- - - - - + + + + + diff --git a/site/admin/config.yml b/site/admin/config.yml index 2fb5652..4084b2c 100644 --- a/site/admin/config.yml +++ b/site/admin/config.yml @@ -21,13 +21,18 @@ backend: name: git-gateway branch: main + # Identity and Git Gateway live on Netlify; the public site stays on Vercel. + # Both URLs must be explicit or Decap falls back to heredita.net/.netlify/*, + # which Vercel answers with a non-JSON 404. + identity_url: https://capable-sawine-7fa143.netlify.app/.netlify/identity + gateway_url: https://capable-sawine-7fa143.netlify.app/.netlify/git # Where Decap writes media uploads (we don't use any yet, but it's required). media_folder: "site/assets/uploads" public_folder: "/assets/uploads" -site_url: https://here-plum.vercel.app -display_url: https://here-plum.vercel.app +site_url: https://heredita.net +display_url: https://heredita.net publish_mode: simple diff --git a/site/admin/index.html b/site/admin/index.html index 07d21e4..1e5d576 100644 --- a/site/admin/index.html +++ b/site/admin/index.html @@ -20,46 +20,8 @@ - Backend: Netlify Git Gateway → commits to Zeyy21/HERE on GitHub Unlinked from the rest of the site; bookmark /admin/ to reach it. --> - + - diff --git a/site/avatar.html b/site/avatar.html index d4d23de..52b66d3 100644 --- a/site/avatar.html +++ b/site/avatar.html @@ -5,46 +5,63 @@ Heredita — Avatar - +
-
-
- - + -
-
- - 0 - coming soon + Pre-alpha + + + + + + + +
+ + 0 + + + - - - - - -
@@ -52,8 +69,8 @@
-

Avatar Customizer 🔒 Coming Soon

- preview only — saving disabled +

Your countryball

+ Preview only — saving is off
@@ -74,12 +91,12 @@

Country

- - + + reset

- Your countryball appears in the top-right of every page across Heredita. + This is what sits in the top-right corner of every page, and next to your name in chat.

@@ -109,12 +126,12 @@

Prop

-

To Be Added Soon

- more flags, hats & props arriving with the Store +

Still on the drawing board

+ Arriving with the store
-

Flags in the pipeline

+

Flags in the queue

?
ArgentinaSoon™
?
South AfricaSoon™
@@ -127,10 +144,10 @@

Fla
?
PakistanSoon™
?
NigeriaSoon™
?
ChileSoon™
-
+ many more
v0.5
+
+ many more
Soon™

-

Alt-history pipeline

+

Alt-history queue

?
Republic of VeniceSoon™
?
Mongol EmpireSoon™
@@ -140,23 +157,23 @@

?
Aztec EmpireSoon™

-

New accessories

+

New accessories

-
🎓
Graduation capSoon™
-
Pith helmetSoon™
-
📜
Treaty scrollSoon™
-
🗡
Saber propSoon™
-
🪖
Beret variantsSoon™
+
?
Graduation capSoon™
+
?
Pith helmetSoon™
+
?
Treaty scrollSoon™
+
?
Saber propSoon™
+
?
Beret variantsSoon™
+
Frames & bannersStore launch

- Want a specific country or accessory next? Pitch us via the contact link. + Want a particular flag next? Ask in the support channel and it usually gets drawn.

-

- Browse what's planned in the Store — items unlock as they ship. +

+ Everything planned is listed in the store. Items unlock as they ship.

@@ -164,18 +181,18 @@

- - - - - + + + + + diff --git a/site/chat.html b/site/chat.html index 27eaed9..4314716 100644 --- a/site/chat.html +++ b/site/chat.html @@ -5,46 +5,63 @@ Heredita — Chat - +
-
-
- - + -
-
- - 0 - coming soon + Pre-alpha + + + + + + + +
+ + 0 + + + - - - - - -
@@ -59,19 +76,19 @@ - - - - - - + + + + + + diff --git a/site/content/updates.json b/site/content/updates.json index 2ec6a19..d8524c4 100644 --- a/site/content/updates.json +++ b/site/content/updates.json @@ -1,57 +1,15 @@ { "updates": [ { - "version": "v0.5", - "date": "2026-05-25", - "tagline": "test", - "title": "testing" - }, - { - "version": "v0.4", - "date": "2026-05-18", - "title": "Loading screens, brush balancing, dice tweaks", - "tagline": "Map Pack", - "items": [ - "**New:** 6 hand-drawn loading screens (Europe 1936, 1945, 1991, plus 3 alt-history maps).", - "**New:** \"Free Paint\" sandbox mode — no dice, no chat, just chalk.", - "**Balance:** brush size now caps at 13 (was 20) to discourage continent-wipes.", - "**Balance:** annexation dice now 2d6 instead of 1d12.", - "**Fix:** Poland chat no longer crashes when 4+ players spam \"I LOVE POLAND\"." - ] - }, - { - "version": "v0.3", - "date": "2026-04-02", - "title": "Found your own country", - "tagline": "Custom Nations beta", - "items": [ - "**New:** custom nation creator — flag, name, government, founding year.", - "**New:** place historical cities on the map (Constantinople, Carthage, etc.).", - "**Balance:** culture diffusion now rolls every 5 in-game years instead of every turn.", - "**Fix:** Whirling no longer renders behind the ocean tile." - ] - }, - { - "version": "v0.2", - "date": "2026-02-14", - "title": "Talk to your neighbors before annexing them", - "tagline": "Chat & community drop", - "items": [ - "**New:** in-game chat panel with player avatars.", - "**New:** \"communities\" — form subgroups inside a server.", - "**Fix:** calendar now actually advances (no more \"Calendar still soon™\")." - ] - }, - { - "version": "v0.1", - "date": "2026-05-25", - "title": "It's alive", - "tagline": "First playable", + "version": "Pre-alpha", + "date": "2026-07-29", + "tagline": "First entry", + "title": "The board goes up", "items": [ - "**New:** the map.", - "**New:** a brush.", - "**New:** a dice.", - "**Known issue:** everything else." + "Heredita is in **pre-alpha** and runs in the browser at app.heredita.net — no launcher, no download, nothing to patch.", + "Play as a guest, or make an account to keep your profile between devices.", + "Chat, friends and countryball avatars are in. The store and Heredita Coins are not open yet.", + "Real patch notes start landing here with the next build." ] } ] diff --git a/site/css/chalkboard.css b/site/css/chalkboard.css index 12f74a8..4874543 100644 --- a/site/css/chalkboard.css +++ b/site/css/chalkboard.css @@ -1,166 +1,217 @@ /* ========================================================= - HEREDITA — Chalkboard Theme - Shared across all pages. + HEREDITA — Chalkboard + ----------------------------------------------------------- + One rule runs through this whole file: + + SOLID line = a real, finished thing + DASHED line = a sketch — locked, unbuilt, coming soon + ACCENT line = the thing you're on right now + + Dashes carry meaning here, so they are never decoration. ========================================================= */ -@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@500;700&family=Patrick+Hand&family=Architects+Daughter&family=Kalam:wght@300;400;700&family=Special+Elite&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@600;700&family=Cabin:wght@400;500;600;700&family=Special+Elite&display=swap'); :root { - /* Chalkboard greens */ + /* Board */ --board-1: #1e3a2a; --board-2: #16291f; --board-3: #0f2018; - --board-edge: #0a160f; + --board-4: #0a160f; + --board-ink: #07110b; /* Chalk */ - --chalk: #f4f1e6; - --chalk-soft: #e8e3d2; - --chalk-faded: rgba(244, 241, 230, 0.55); - --chalk-dust: rgba(244, 241, 230, 0.10); - - /* Accent (from logo) */ - --accent: #d97a3d; - --accent-soft: #e89968; - --accent-deep: #b35d28; - - /* Tier colors */ - --tier-guest-fg: #c9d1c2; - --tier-guest-bg: rgba(180, 192, 170, 0.18); - --tier-guest-bd: rgba(201, 209, 194, 0.55); - - --tier-regular-fg: #f4f1e6; - --tier-regular-bg: rgba(244, 241, 230, 0.14); - --tier-regular-bd: rgba(244, 241, 230, 0.65); - - --tier-emperor-fg: #fff0c6; - --tier-emperor-bg: linear-gradient(135deg, rgba(255,210,80,0.30), rgba(217,122,61,0.30)); - --tier-emperor-bd: #f4c95d; - - /* Wood / frame */ - --wood-1: #5b3a1f; - --wood-2: #3a2510; - - /* Sizing */ - --radius: 14px; + --chalk: #f4f1e6; + --chalk-soft: #ddd8c6; + --chalk-dim: #a8b0a0; + --chalk-ghost: rgba(244, 241, 230, 0.42); + + /* Coloured chalk — used sparingly, like the real box of it */ + --accent: #d97a3d; + --accent-soft: #ea9a63; + --accent-deep: #a8511f; + --chalk-blue: #8fc2e0; + --chalk-red: #e08272; + --chalk-gold: #f0c661; + + /* Chalk tray */ + --wood-1: #6b4526; + --wood-2: #3d2612; + + /* Line hierarchy */ + --rule: 1px solid rgba(244, 241, 230, 0.13); + --rule-strong: 1px solid rgba(244, 241, 230, 0.26); + --sketch: 2px dashed rgba(244, 241, 230, 0.38); + + /* Type */ + --font-display: 'Caveat', 'Segoe Script', cursive; + --font-stamp: 'Special Elite', 'Courier New', monospace; + --font-body: 'Cabin', system-ui, -apple-system, 'Segoe UI', sans-serif; + + /* Shape */ + --r-sm: 8px; + --r: 12px; + --r-lg: 18px; --maxw: 1180px; - --shadow-chalk: 0 0 0 2px rgba(244,241,230,0.06), - 0 12px 32px rgba(0,0,0,0.45); - /* Z scale */ - --z-nav: 30; + /* Depth */ + --lift-1: 0 1px 0 rgba(244,241,230,0.05) inset, 0 10px 24px -16px rgba(0,0,0,0.9); + --lift-2: 0 1px 0 rgba(244,241,230,0.07) inset, 0 22px 44px -22px rgba(0,0,0,0.95); + --lift-3: 0 28px 60px -24px rgba(0,0,0,0.95); + + /* Rhythm */ + --gap: 16px; + --sec: clamp(38px, 6vw, 66px); + + /* Stacking */ + --z-nav: 40; + --z-menu: 60; --z-overlay: 80; --z-toast: 90; + + --ease: cubic-bezier(.22, .8, .2, 1); } -* { box-sizing: border-box; } +*, *::before, *::after { box-sizing: border-box; } -html, body { - margin: 0; - padding: 0; - min-height: 100%; - font-family: 'Architects Daughter', 'Kalam', 'Patrick Hand', system-ui, sans-serif; - color: var(--chalk); - background-color: var(--board-2); - font-size: 19px; - line-height: 1.6; - letter-spacing: 0.15px; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; -} +html { -webkit-text-size-adjust: 100%; } + +/* Components below set `display` on elements that JS also toggles with the + `hidden` attribute — which would otherwise lose to any class selector. */ +[hidden] { display: none !important; } -/* Chalkboard background — layered greens + SVG noise */ body { + margin: 0; + font-family: var(--font-body); + font-size: 17px; + line-height: 1.62; + color: var(--chalk-soft); + background-color: var(--board-2); background-image: - radial-gradient(ellipse 120% 80% at 20% 10%, rgba(255,255,255,0.05), transparent 60%), - radial-gradient(ellipse 90% 60% at 80% 90%, rgba(0,0,0,0.45), transparent 70%), - radial-gradient(ellipse 60% 40% at 50% 50%, var(--board-1), var(--board-2) 60%, var(--board-3) 100%); - position: relative; + radial-gradient(ellipse 120% 80% at 22% 6%, rgba(255,255,255,0.055), transparent 58%), + radial-gradient(ellipse 90% 65% at 82% 96%, rgba(0,0,0,0.5), transparent 70%), + radial-gradient(ellipse 65% 45% at 50% 45%, var(--board-1), var(--board-2) 58%, var(--board-3) 100%); overflow-x: hidden; + position: relative; + -webkit-font-smoothing: antialiased; + + /* Sticky footer: short pages (chat, friends, settings) would otherwise + leave the footer stranded halfway down the viewport. */ + min-height: 100vh; + display: flex; + flex-direction: column; } -/* Chalk-dust grain overlay */ +/* width:100% matters — as a column flex item, `.container`'s `margin: 0 auto` + would otherwise shrink main to its content width and collapse the grids. */ +body > main { flex: 1 0 auto; width: 100%; } +body > .site-footer { flex: 0 0 auto; } + +/* Slate grain. Fixed so it reads as the surface, not the content. */ body::before { content: ""; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 1; - opacity: 0.35; + position: fixed; inset: 0; + pointer-events: none; z-index: 0; + opacity: 0.3; mix-blend-mode: overlay; - background-image: url("data:image/svg+xml;utf8,"); + background-image: url("data:image/svg+xml;utf8,"); } -/* Subtle chalk smudges */ +/* Ghosts of things wiped off the board */ body::after { content: ""; - position: fixed; - inset: 0; - pointer-events: none; - z-index: 1; + position: fixed; inset: 0; + pointer-events: none; z-index: 0; background: - radial-gradient(circle at 12% 22%, rgba(244,241,230,0.05) 0, transparent 18%), - radial-gradient(circle at 78% 12%, rgba(244,241,230,0.04) 0, transparent 14%), - radial-gradient(circle at 30% 88%, rgba(244,241,230,0.05) 0, transparent 22%), - radial-gradient(circle at 90% 78%, rgba(244,241,230,0.04) 0, transparent 18%); + radial-gradient(ellipse 22% 9% at 14% 24%, rgba(244,241,230,0.045), transparent 70%), + radial-gradient(ellipse 16% 7% at 79% 13%, rgba(244,241,230,0.035), transparent 70%), + radial-gradient(ellipse 26% 10% at 32% 86%, rgba(244,241,230,0.04), transparent 70%), + radial-gradient(ellipse 18% 8% at 91% 74%, rgba(244,241,230,0.03), transparent 70%); } -main, header, footer, section, .container { - position: relative; - z-index: 2; -} +main, header, footer, section, .container { position: relative; z-index: 1; } + + +/* ========================================================= + TYPE + Caveat writes. Special Elite stamps. Cabin explains. + ========================================================= */ -/* ============= Typography ============= */ h1, h2, h3, h4 { - font-family: 'Caveat', 'Patrick Hand', cursive; - color: var(--chalk); - text-shadow: - 0 1px 0 rgba(0,0,0,0.25), - 0 0 14px rgba(244,241,230,0.10); - margin: 0 0 .4em; - letter-spacing: 0.5px; + font-family: var(--font-display); font-weight: 700; + color: var(--chalk); + margin: 0 0 .38em; + line-height: 1.08; + letter-spacing: 0.4px; + text-shadow: 0 1px 0 rgba(0,0,0,0.3), 0 0 18px rgba(244,241,230,0.09); } -h1 { font-size: clamp(2.2rem, 4.8vw, 3.6rem); line-height: 1.05; } -h2 { font-size: clamp(1.7rem, 3vw, 2.4rem); } -h3 { font-size: clamp(1.3rem, 2vw, 1.6rem); } +h1 { font-size: clamp(2.4rem, 5.2vw, 3.9rem); } +h2 { font-size: clamp(1.85rem, 3.2vw, 2.6rem); } +h3 { font-size: clamp(1.3rem, 2vw, 1.65rem); } +h4 { font-size: 1.2rem; } -h1.tilt-l, h2.tilt-l, h3.tilt-l { transform: rotate(-1.2deg); } -h1.tilt-r, h2.tilt-r, h3.tilt-r { transform: rotate(1deg); } - -p, li, label, input, span, div, td, th { - font-family: 'Architects Daughter', 'Kalam', cursive; -} +/* Exactly one hand-tilt per page, on the h1. Any more and it looks + like a filter rather than a hand. */ +h1.tilt-l { transform: rotate(-0.7deg); } +h1.tilt-r { transform: rotate(0.5deg); } +h2.tilt-l, h2.tilt-r, h3.tilt-l, h3.tilt-r { transform: none; } -p { color: var(--chalk-soft); max-width: 70ch; } -/* When a paragraph sits inside a centered block, also center the - paragraph element itself so the 70ch max-width doesn't pin it left. */ -.text-center > p, -.text-center p { - margin-left: auto; - margin-right: auto; -} -li { color: var(--chalk-soft); } +p { margin: 0 0 1em; max-width: 68ch; } +p:last-child { margin-bottom: 0; } -/* Buttons + tags keep the playful Caveat for chunky chalk feel */ -button, .btn, .tag, .version, .coin-chip, .user-chip, .auth-tabs button, .brand-name { - font-family: 'Caveat', 'Architects Daughter', cursive; -} +/* A centred paragraph still has a 68ch measure, so the *block* has to be + centred too — otherwise the text centres inside a box that's sitting off + to the left. Covers both `.text-center p` and `p.text-center`. */ +.text-center p, +p.text-center, +.text-center > * { margin-left: auto; margin-right: auto; } -/* Labels: Patrick Hand reads slightly chunkier — good for form labels */ -.field label { font-family: 'Patrick Hand', 'Caveat', cursive; } +strong { color: var(--chalk); font-weight: 600; } +em { color: var(--chalk); } a { color: var(--chalk); text-decoration: none; - border-bottom: 1.5px dashed var(--chalk-faded); - padding-bottom: 1px; - transition: color .2s, border-color .2s; + border-bottom: 1px solid rgba(244,241,230,0.35); + transition: color .18s, border-color .18s; } a:hover { color: var(--accent-soft); border-bottom-color: var(--accent); } -a:focus-visible { outline: 2px dashed var(--accent); outline-offset: 3px; border-radius: 4px; } +a:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; + border-radius: 3px; +} + +code { + font-family: var(--font-stamp); + font-size: 0.92em; + color: var(--accent-soft); +} + +.muted { color: var(--chalk-dim); } +.accent { color: var(--accent-soft); } + +/* Typewritten metadata: dates, versions, statuses, section kickers. */ +.kicker, .stamp, .version, .date, .badge, .cs-eta, .brand-tag { + font-family: var(--font-stamp); + letter-spacing: 0.08em; + text-transform: uppercase; + font-size: 0.72rem; +} + +.kicker { + display: block; + color: var(--accent-soft); + margin-bottom: 6px; +} + + +/* ========================================================= + LAYOUT + ========================================================= */ -/* ============= Layout helpers ============= */ .container { max-width: var(--maxw); margin: 0 auto; @@ -172,908 +223,1204 @@ a:focus-visible { outline: 2px dashed var(--accent); outline-offset: 3px; border .spacer { flex: 1; } .center { display: flex; align-items: center; justify-content: center; } .text-center { text-align: center; } -.muted { color: var(--chalk-faded); } -.accent { color: var(--accent-soft); } -/* ============= Chalk cards ============= */ +.section-title { + display: flex; + align-items: baseline; + gap: 16px; + flex-wrap: wrap; + margin: var(--sec) 0 4px; +} +.section-title h1, .section-title h2 { margin: 0; } +.section-title .muted, +.section-title .see-all { + font-family: var(--font-stamp); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.section-title .see-all { margin-left: auto; border-bottom-color: transparent; } +.section-title .see-all:hover { border-bottom-color: var(--accent); } + +/* A chalk line drawn under section headings — the one signature flourish. */ +.section-title::after { + content: ""; + flex: 0 0 100%; + height: 2px; + margin-top: 10px; + background: linear-gradient(90deg, + rgba(244,241,230,0.34) 0%, + rgba(244,241,230,0.14) 42%, + transparent 78%); + transform-origin: left center; + transform: scaleX(var(--rule-scale, 1)); + transition: transform .7s var(--ease); +} +.section-title.reveal::after { --rule-scale: 0; } +.section-title.reveal.in::after { --rule-scale: 1; } + + +/* ========================================================= + CARDS + Solid edge = finished. `.sketch` = not built yet. + ========================================================= */ + .chalk-card { position: relative; + padding: 26px; + border: var(--rule); + border-radius: var(--r); background: - linear-gradient(180deg, rgba(0,0,0,0.18), rgba(0,0,0,0.32)), - radial-gradient(ellipse at top, rgba(244,241,230,0.04), transparent 60%); - border-radius: var(--radius); - padding: 28px; - box-shadow: var(--shadow-chalk); + linear-gradient(180deg, rgba(244,241,230,0.05), rgba(0,0,0,0.10) 40%, rgba(0,0,0,0.24)), + rgba(10, 22, 15, 0.34); + box-shadow: var(--lift-1); + transition: border-color .25s var(--ease), + box-shadow .25s var(--ease), + transform .25s var(--ease), + background .25s var(--ease); +} + +.chalk-card.flat { background: rgba(0,0,0,0.22); box-shadow: none; } + +/* Not built yet. Chalk outline, nothing filled in. */ +.chalk-card.sketch, +.mode-card.disabled, +.coming-soon-tile, +.store-item.sketch { + border: var(--sketch); + background: + repeating-linear-gradient(-45deg, rgba(244,241,230,0.028) 0 9px, transparent 9px 18px), + rgba(0,0,0,0.2); + box-shadow: none; } -.chalk-card::before { - content: ""; - position: absolute; - inset: 6px; - border: 2px dashed rgba(244,241,230,0.55); - border-radius: calc(var(--radius) - 4px); - pointer-events: none; - opacity: .85; +/* Interactive cards. Note the border-bottom reset — these are anchors, and + the global link underline would otherwise cut across the card edge. */ +a.chalk-card, button.chalk-card, .mode-card:not(.disabled) { + cursor: pointer; + border-bottom: var(--rule); + text-align: left; + color: inherit; +} +a.chalk-card { display: block; } +a.chalk-card:hover, +button.chalk-card:hover, +.mode-card:not(.disabled):hover, +.about-grid .chalk-card:hover, +.faq-grid .chalk-card:hover, +.update-entry:hover { + transform: translateY(-3px); + border-color: rgba(244,241,230,0.3); + box-shadow: var(--lift-2); } +a.chalk-card:active, .mode-card:not(.disabled):active { + transform: translateY(-1px); + transition-duration: .08s; +} +a.chalk-card:focus-visible, button.chalk-card:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} + -.chalk-card.tight { padding: 18px; } -.chalk-card.flat::before { border-style: solid; opacity: .35; } +/* ========================================================= + BUTTONS + ========================================================= */ -/* ============= Chalk buttons ============= */ .btn { - --bg: transparent; - --fg: var(--chalk); - --bd: var(--chalk); display: inline-flex; align-items: center; justify-content: center; - gap: 10px; - font-family: 'Caveat', cursive; + gap: 9px; + font-family: var(--font-body); font-weight: 700; - font-size: 1.4rem; - letter-spacing: 0.5px; - color: var(--fg); - background: var(--bg); - border: 2.5px dashed var(--bd); - border-radius: 12px; - padding: 12px 26px; + font-size: 0.95rem; + letter-spacing: 0.015em; + color: var(--chalk); + background: rgba(244,241,230,0.05); + border: var(--rule-strong); + border-radius: 10px; + padding: 11px 22px; cursor: pointer; text-decoration: none; - transition: background .18s ease, color .18s ease, transform .18s ease, border-style .18s ease; - text-shadow: 0 1px 0 rgba(0,0,0,0.18); - position: relative; user-select: none; + transition: background .16s, border-color .16s, color .16s, transform .12s, box-shadow .16s; } - -.btn:hover:not(:disabled) { - background: var(--chalk); - color: var(--board-2); - border-style: solid; - transform: rotate(-0.5deg); - text-shadow: none; +.btn:hover:not(:disabled):not([aria-disabled="true"]) { + background: rgba(244,241,230,0.12); + border-color: rgba(244,241,230,0.5); + color: #fff; } +.btn:active:not(:disabled) { transform: translateY(1px); } +.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; } +.btn:disabled { opacity: 0.4; cursor: not-allowed; } -.btn:active:not(:disabled) { transform: rotate(0.4deg) scale(0.98); } - -.btn:focus-visible { outline: 3px dashed var(--accent); outline-offset: 4px; } - -.btn:disabled { - opacity: 0.45; - cursor: not-allowed; - filter: grayscale(0.4); -} +.btn svg { width: 17px; height: 17px; flex: 0 0 auto; } +/* Primary — a painted key with a hard edge underneath, like a game button */ .btn-primary { - --bg: var(--accent); - --fg: var(--board-3); - --bd: var(--chalk); - border-style: solid; - box-shadow: 0 4px 0 var(--accent-deep), 0 8px 18px rgba(0,0,0,0.35); -} -.btn-primary:hover:not(:disabled) { - background: var(--accent-soft); - color: var(--board-3); - transform: translateY(-2px) rotate(-0.4deg); - box-shadow: 0 6px 0 var(--accent-deep), 0 12px 22px rgba(0,0,0,0.45); + color: var(--board-ink); + background: linear-gradient(180deg, var(--accent-soft), var(--accent)); + border: 1px solid rgba(255, 220, 180, 0.55); + box-shadow: 0 3px 0 var(--accent-deep), 0 12px 26px -12px rgba(217,122,61,0.85); +} +.btn-primary:hover:not(:disabled):not([aria-disabled="true"]) { + color: var(--board-ink); + background: linear-gradient(180deg, #f2ab77, var(--accent-soft)); + transform: translateY(-2px); + box-shadow: 0 5px 0 var(--accent-deep), 0 18px 34px -12px rgba(217,122,61,0.95); } .btn-primary:active:not(:disabled) { - transform: translateY(1px); - box-shadow: 0 2px 0 var(--accent-deep), 0 4px 10px rgba(0,0,0,0.35); -} - -.btn-ghost { font-size: 1.1rem; padding: 8px 16px; border-width: 2px; } - -.btn-huge { - font-size: 2.6rem; - padding: 28px 72px; - border-radius: 18px; + transform: translateY(2px); + box-shadow: 0 1px 0 var(--accent-deep), 0 6px 14px -8px rgba(0,0,0,0.8); } -/* ============= Inputs ============= */ -.field { - display: flex; - flex-direction: column; - gap: 6px; - margin-bottom: 14px; -} -.field label { - font-family: 'Caveat', cursive; - font-size: 1.25rem; - color: var(--chalk); +.btn-ghost { + background: transparent; + border-color: transparent; + color: var(--chalk-dim); + padding: 9px 14px; } -.field input, .field select { - font-family: 'Patrick Hand', cursive; - font-size: 1.1rem; +.btn-ghost:hover:not(:disabled) { + background: rgba(244,241,230,0.07); + border-color: transparent; color: var(--chalk); - background: rgba(0,0,0,0.28); - border: 2px dashed rgba(244,241,230,0.55); - border-radius: 10px; - padding: 10px 14px; - outline: none; - transition: border-color .2s, background .2s; } -.field input::placeholder { color: var(--chalk-faded); } -.field input:focus, .field select:focus { - border-color: var(--accent); - border-style: solid; - background: rgba(0,0,0,0.40); + + + +/* Locked controls read as sketches too */ +.btn.locked, .btn.emperor-locked, .tier-switch button.locked { + border-style: dashed; + opacity: 0.62; + cursor: not-allowed; } -.field input:invalid:not(:placeholder-shown) { - border-color: #e07a7a; +.btn.locked:hover, .btn.emperor-locked:hover, .tier-switch button.locked:hover { + transform: none; + background: rgba(244,241,230,0.05); } -/* Checkbox */ -.checkbox { - display: flex; - align-items: flex-start; - gap: 10px; - cursor: pointer; - font-size: 1rem; + +/* ========================================================= + CHIPS, BADGES, STAMPS + ========================================================= */ + +.badge, .lock-badge, .price-pill, .lock-pill { + display: inline-flex; + align-items: center; + gap: 6px; + width: max-content; + padding: 3px 10px; + border-radius: 999px; + line-height: 1.5; } -.checkbox input { appearance: none; -webkit-appearance: none; margin: 0; } -.checkbox .box { - width: 22px; height: 22px; flex: 0 0 22px; - border: 2.5px dashed var(--chalk); - border-radius: 4px; - display: inline-flex; align-items: center; justify-content: center; - margin-top: 2px; - transition: background .15s, border-color .15s; + +/* "Coming soon" everywhere reads as a sketch */ +.badge, .lock-badge, .lock-pill { + font-family: var(--font-stamp); + font-size: 0.66rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--accent-soft); + background: rgba(217,122,61,0.12); + border: 1px dashed rgba(217,122,61,0.6); } -.checkbox input:checked + .box { - background: var(--accent); - border-color: var(--chalk); - border-style: solid; +.lock-pill { margin-left: 10px; vertical-align: middle; } + + +.price-pill { + font-family: var(--font-stamp); + font-size: 0.72rem; + color: var(--chalk-dim); + border: var(--rule); } -.checkbox input:checked + .box::after { - content: "✓"; - font-family: 'Caveat', cursive; - color: var(--board-3); - font-weight: 700; - font-size: 18px; - line-height: 1; +.price-pill .coin, .coin-chip .coin { + width: 13px; height: 13px; + border-radius: 50%; + background: radial-gradient(circle at 34% 32%, #ffe9a8, var(--chalk-gold) 52%, #b8862a); + box-shadow: 0 0 0 1px rgba(0,0,0,0.4); + flex: 0 0 auto; } -.checkbox input:focus-visible + .box { outline: 2px dashed var(--accent); outline-offset: 2px; } -/* ============= Top Navigation ============= */ + + +/* ========================================================= + TOP BAR + The nav is the spine of the site, so it gets real chrome: + icon tabs, and a chalk dash that slides to whatever you point at. + ========================================================= */ + .topbar { position: sticky; top: 0; z-index: var(--z-nav); - background: - linear-gradient(180deg, rgba(10,22,15,0.92), rgba(10,22,15,0.78)); - backdrop-filter: blur(6px); - border-bottom: 2px dashed rgba(244,241,230,0.25); + background: linear-gradient(180deg, rgba(7,17,11,0.96), rgba(7,17,11,0.86)); + backdrop-filter: blur(12px) saturate(1.15); + -webkit-backdrop-filter: blur(12px) saturate(1.15); + border-bottom: 1px solid rgba(244,241,230,0.12); + box-shadow: 0 1px 0 rgba(244,241,230,0.045), 0 14px 34px -24px #000; } -.topbar .inner { + +.topbar-inner, .topbar .inner { max-width: var(--maxw); margin: 0 auto; + padding: 9px 24px; display: flex; align-items: center; - gap: 14px; - padding: 10px 20px; -} -.topbar .brand { - display: flex; align-items: center; gap: 10px; - border: none; -} -.topbar .brand img { width: 38px; height: 38px; object-fit: contain; } -.topbar .brand .brand-name { - font-family: 'Caveat', cursive; font-weight: 700; font-size: 1.5rem; color: var(--chalk); -} -.topbar nav { display: flex; gap: 14px; } -.topbar nav a { - border: none; - font-size: 1.05rem; - padding: 6px 10px; - border-radius: 8px; - color: var(--chalk-soft); + gap: 16px; } -.topbar nav a:hover { background: rgba(244,241,230,0.08); color: var(--chalk); } -.topbar nav a.active { color: var(--accent-soft); } -.coin-chip { - display: inline-flex; align-items: center; gap: 8px; - background: linear-gradient(180deg, var(--accent), var(--accent-deep)); - color: var(--board-3); - font-family: 'Caveat', cursive; font-weight: 700; - padding: 6px 12px; border-radius: 999px; - border: 2px solid rgba(244,241,230,0.85); - font-size: 1.05rem; - box-shadow: 0 4px 0 rgba(0,0,0,0.35); -} -.coin-chip .coin { - display: inline-block; width: 18px; height: 18px; - background: radial-gradient(circle at 35% 35%, #fff2b3, var(--accent) 60%, var(--accent-deep)); - border-radius: 50%; - border: 1.5px solid var(--board-3); +/* --- Brand --- */ +.brand { + display: flex; + align-items: center; + gap: 10px; + border-bottom: none; + flex: 0 0 auto; } -.coin-chip .soon { - font-size: .75rem; color: rgba(15,32,24,0.7); margin-left: 6px; - font-family: 'Patrick Hand', cursive; +.brand:hover { color: var(--chalk); } +.brand-mark, .topbar .brand img { + width: 34px; height: 34px; + object-fit: contain; + transition: transform .4s var(--ease); } - -.user-chip { - display: inline-flex; align-items: center; gap: 8px; - padding: 5px 12px; - border: 2px dashed rgba(244,241,230,0.55); - border-radius: 999px; +.brand:hover .brand-mark { transform: rotate(-7deg) scale(1.07); } +.brand-text { display: flex; flex-direction: column; line-height: 1; gap: 3px; } +.brand-name { + font-family: var(--font-display); + font-weight: 700; + font-size: 1.55rem; color: var(--chalk); - font-family: 'Caveat', cursive; - font-size: 1.15rem; + line-height: 0.9; } -.user-chip .avatar { - width: 26px; height: 26px; border-radius: 50%; - background: linear-gradient(135deg, var(--accent), var(--accent-deep)); - border: 1.5px solid var(--chalk); - display: inline-flex; align-items: center; justify-content: center; - color: var(--board-3); font-weight: 700; font-size: .9rem; +.brand-tag { + font-size: 0.55rem; + color: var(--accent-soft); + opacity: 0.85; } -/* ============= Footer ============= */ -.site-footer { - margin-top: 60px; - padding: 26px 24px 40px; - border-top: 2px dashed rgba(244,241,230,0.25); - background: rgba(10,22,15,0.6); - color: var(--chalk-faded); - font-size: 0.95rem; +/* --- Tabs --- */ +.primary-nav { + position: relative; + display: flex; + align-items: center; + gap: 1px; + flex: 1 1 auto; + min-width: 0; } -.site-footer .footer-inner { - max-width: var(--maxw); - margin: 0 auto; - display: flex; flex-wrap: wrap; gap: 14px; align-items: center; + +.nav-tab { + position: relative; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 13px; + border: none; + border-bottom: none; + border-radius: var(--r-sm); + background: transparent; + font-family: var(--font-body); + font-weight: 600; + font-size: 0.94rem; + letter-spacing: 0.01em; + color: var(--chalk-dim); + cursor: pointer; + white-space: nowrap; + transition: color .18s, background .18s; } -.site-footer a { color: var(--chalk-soft); font-size: 0.95rem; } -.site-footer .footer-links { display: flex; gap: 18px; flex-wrap: wrap; } -.site-footer .footer-meta { margin-left: auto; opacity: 0.7; } +.nav-tab svg { width: 16px; height: 16px; flex: 0 0 auto; opacity: 0.55; transition: opacity .18s var(--ease), transform .3s var(--ease); } +.nav-tab:hover, .nav-tab:focus-visible { color: var(--chalk); background: rgba(244,241,230,0.07); } +.nav-tab:hover svg, .nav-tab:focus-visible svg { opacity: 1; transform: translateY(-1px); } +.nav-tab.active { color: var(--chalk); } +.nav-tab.active svg { opacity: 1; color: var(--accent-soft); } +.nav-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } -/* ============= Chalk dust floating particles ============= */ -.chalk-dust { - position: fixed; inset: 0; pointer-events: none; z-index: 1; overflow: hidden; +.nav-tab .caret { + width: 11px; height: 11px; + opacity: 0.55; + transition: transform .28s var(--ease), opacity .18s; } -.chalk-dust span { +.nav-group.open > .nav-tab .caret { transform: rotate(180deg); } +.nav-group.open > .nav-tab { color: var(--chalk); background: rgba(244,241,230,0.07); } + +/* The sliding chalk dash. Follows the pointer, snaps back to the + current page when you stop pointing at anything. */ +.nav-marker { position: absolute; - bottom: -20px; - width: 4px; height: 4px; border-radius: 50%; - background: var(--chalk); - opacity: 0.25; - animation: drift linear infinite; - filter: blur(0.5px); + left: 0; bottom: -4px; + height: 3px; + width: 0; + border-radius: 2px; + pointer-events: none; + opacity: 0; + background: repeating-linear-gradient(90deg, + var(--accent) 0 7px, rgba(217,122,61,0.22) 7px 11px); + transition: transform .34s var(--ease), + width .34s var(--ease), + opacity .2s ease, + background .2s ease; } -@keyframes drift { - 0% { transform: translate(0, 0); opacity: 0; } - 10% { opacity: 0.35; } - 100% { transform: translate(var(--dx, 40px), -110vh); opacity: 0; } +.primary-nav.marker-live .nav-marker { opacity: 1; } +.primary-nav.marker-hover .nav-marker { + background: repeating-linear-gradient(90deg, + var(--chalk) 0 7px, rgba(244,241,230,0.18) 7px 11px); } -/* ============= Loading overlay ============= */ -.loading-overlay { - position: fixed; inset: 0; - background: rgba(10,22,15,0.92); - z-index: var(--z-overlay); - display: none; - align-items: center; justify-content: center; - flex-direction: column; gap: 24px; - text-align: center; +/* --- Dropdown panels --- */ +.nav-group { position: relative; display: flex; align-items: center; } + +.nav-menu { + position: absolute; + top: calc(100% + 16px); + left: 50%; + min-width: 250px; + padding: 7px; + display: flex; + flex-direction: column; + gap: 1px; + border: var(--rule-strong); + border-radius: var(--r-lg); + background: linear-gradient(180deg, #172c20, #0b1a11); + box-shadow: var(--lift-3), 0 0 0 1px rgba(0,0,0,0.5); + z-index: var(--z-menu); + opacity: 0; + visibility: hidden; + transform: translate(-50%, -8px) scale(0.97); + transform-origin: top center; + transition: opacity .18s ease, transform .26s var(--ease), visibility .26s; } -.loading-overlay.show { display: flex; } -.loading-overlay .spinner { - width: 64px; height: 64px; - border: 4px dashed var(--chalk); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 1.4s linear infinite; +.nav-group.open > .nav-menu { + opacity: 1; + visibility: visible; + transform: translate(-50%, 0) scale(1); } -@keyframes spin { to { transform: rotate(360deg); } } -.loading-overlay h2 { - font-family: 'Caveat', cursive; - font-size: 2.2rem; +.nav-menu.align-right { + left: auto; right: 0; + transform: translate(0, -8px) scale(0.97); + transform-origin: top right; } +.nav-group.open > .nav-menu.align-right { transform: translate(0, 0) scale(1); } -/* ============= Auth specific ============= */ -.auth-wrap { - min-height: calc(100vh - 100px); - display: flex; flex-direction: column; align-items: center; - padding: 28px 20px; -} -.auth-logo { - width: clamp(220px, 30vw, 360px); - margin: 12px auto 18px; - filter: drop-shadow(0 10px 22px rgba(0,0,0,0.55)); - user-select: none; +/* chalk notch pointing back at the tab */ +.nav-menu::before { + content: ""; + position: absolute; + top: -7px; + left: var(--notch, 50%); + margin-left: -6px; + width: 12px; height: 12px; + transform: rotate(45deg); + background: #172c20; + border-left: var(--rule-strong); + border-top: var(--rule-strong); + border-top-left-radius: 3px; } -.auth-tagline { - text-align: center; - font-family: 'Caveat', cursive; - font-size: clamp(1.4rem, 2.5vw, 2rem); + +.nav-menu a, .nav-menu button { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 10px 12px; + border: 1px solid transparent; + border-radius: 10px; + background: transparent; + font-family: var(--font-body); + font-size: 0.93rem; + font-weight: 500; color: var(--chalk-soft); - margin: 0 0 22px; - transform: rotate(-0.6deg); + text-align: left; + cursor: pointer; + white-space: nowrap; + transition: background .15s, color .15s, border-color .15s, transform .15s var(--ease); } -.auth-card { - width: 100%; - max-width: 460px; +.nav-menu a:hover, .nav-menu a:focus-visible, +.nav-menu button:hover, .nav-menu button:focus-visible { + background: rgba(244,241,230,0.08); + border-color: rgba(244,241,230,0.16); + color: var(--chalk); + outline: none; + transform: translateX(2px); +} +.nav-menu a.active { + color: var(--accent-soft); + background: rgba(217,122,61,0.14); + border-color: rgba(217,122,61,0.5); +} +.nav-menu .sp-icon, .nav-menu .mi-icon { + width: 19px; height: 19px; + flex: 0 0 19px; + display: inline-flex; align-items: center; justify-content: center; + opacity: 0.75; } -.auth-tabs { +.nav-menu .sp-icon svg, .nav-menu .mi-icon svg { width: 19px; height: 19px; } +.nav-menu a:hover .sp-icon, .nav-menu a:hover .mi-icon { opacity: 1; } +.nav-menu .sp-label { flex: 1; min-width: 0; line-height: 1.2; } +.nav-menu .sp-sub { + display: block; + font-size: 0.74rem; + color: var(--chalk-dim); + margin-top: 2px; +} +.nav-menu .menu-rule { + height: 1px; + margin: 5px 8px; + background: rgba(244,241,230,0.12); +} +.nav-menu .menu-head { + font-family: var(--font-stamp); + font-size: 0.62rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--chalk-dim); + padding: 7px 12px 4px; +} + +/* --- Right-hand cluster --- */ +.topbar-right { display: flex; - gap: 8px; - margin-bottom: 18px; - border-bottom: 2px dashed rgba(244,241,230,0.35); - padding-bottom: 6px; + align-items: center; + gap: 9px; + flex: 0 0 auto; + margin-left: auto; } -.auth-tabs button { - flex: 1; - background: transparent; - border: none; - color: var(--chalk-faded); - font-family: 'Caveat', cursive; - font-size: 1.5rem; - font-weight: 700; - padding: 6px 10px; + +.coin-chip { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 5px 12px; + border-radius: 999px; + border: 1px dashed rgba(240, 198, 97, 0.45); + background: rgba(240, 198, 97, 0.09); + color: var(--chalk-gold); + font-family: var(--font-stamp); + font-size: 0.78rem; + cursor: default; +} +.coin-chip .soon { + font-size: 0.6rem; + letter-spacing: 0.1em; + text-transform: uppercase; + opacity: 0.6; +} + +/* Name + tier + avatar, one control */ +.user-chip, .player-chip { + display: inline-flex; + align-items: center; + gap: 9px; + padding: 4px 10px 4px 5px; + border: var(--rule); + border-radius: 999px; + background: rgba(244,241,230,0.04); cursor: pointer; - border-radius: 8px; + font-family: var(--font-body); + transition: background .16s, border-color .16s; +} +.user-chip:hover, .nav-group.open > .user-chip { + background: rgba(244,241,230,0.1); + border-color: rgba(244,241,230,0.28); +} +.user-chip .avatar { + width: 29px; height: 29px; + flex: 0 0 29px; + border-radius: 50%; + display: inline-flex; align-items: center; justify-content: center; + background: linear-gradient(140deg, var(--accent-soft), var(--accent-deep)); + color: var(--board-ink); + font-family: var(--font-display); + font-weight: 700; + font-size: 1rem; + border: 1px solid rgba(244,241,230,0.5); + overflow: hidden; +} +.user-chip .avatar.countryball { + background: none; border: none; padding: 0; + width: 30px; height: 30px; flex-basis: 30px; + overflow: visible; +} +.user-chip .avatar.countryball svg { width: 100%; height: 100%; display: block; } + +.user-block { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: 1px; + line-height: 1.1; } -.auth-tabs button.active { +.user-block .name { + font-size: 0.86rem; + font-weight: 600; color: var(--chalk); - background: rgba(244,241,230,0.08); + max-width: 130px; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.form-actions { display: flex; flex-direction: column; gap: 12px; margin-top: 8px; } -.form-actions .row-or { - text-align: center; - font-family: 'Caveat', cursive; - color: var(--chalk-faded); - position: relative; - margin: 4px 0; +.tier-badge { + display: inline-flex; + align-items: center; + gap: 4px; + font-family: var(--font-stamp); + font-size: 0.6rem; + letter-spacing: 0.1em; + text-transform: uppercase; + line-height: 1.4; + padding: 0 7px; + border-radius: 999px; + border: 1px solid rgba(201,209,194,0.4); + background: rgba(180,192,170,0.14); + color: var(--chalk-dim); + text-decoration: none; + border-bottom-width: 1px; +} +.tier-badge[data-tier="regular"] { + border-color: rgba(244,241,230,0.5); + background: rgba(244,241,230,0.12); + color: var(--chalk); } -.form-actions .row-or::before, -.form-actions .row-or::after { - content: ""; position: absolute; top: 50%; width: 38%; height: 1px; - border-top: 2px dashed rgba(244,241,230,0.30); +.tier-badge[data-tier="emperor"] { + border-color: var(--chalk-gold); + background: linear-gradient(135deg, rgba(240,198,97,0.3), rgba(217,122,61,0.3)); + color: #ffeec2; + box-shadow: 0 0 14px rgba(240,198,97,0.28); +} +.tier-badge .crown { + width: 11px; height: 11px; + background: currentColor; + -webkit-mask: url("data:image/svg+xml;utf8,") center/contain no-repeat; + mask: url("data:image/svg+xml;utf8,") center/contain no-repeat; } -.form-actions .row-or::before { left: 0; } -.form-actions .row-or::after { right: 0; } -/* ============= Hero on home / play ============= */ -.hero-card { +/* --- Hamburger --- */ +.nav-toggle { + display: none; + width: 38px; height: 34px; + padding: 0; + border: var(--rule-strong); + border-radius: var(--r-sm); + background: transparent; + cursor: pointer; position: relative; - overflow: hidden; - padding: 36px; - display: flex; gap: 28px; align-items: center; - background: - linear-gradient(120deg, rgba(217,122,61,0.18), rgba(0,0,0,0.35)), - radial-gradient(ellipse at left, rgba(244,241,230,0.06), transparent 60%); + flex: 0 0 auto; } -.hero-card .hero-text { flex: 1; min-width: 0; } -.hero-card .hero-text h1 { margin-bottom: 8px; } -.hero-card .hero-art { - flex: 0 0 280px; - height: 180px; - border-radius: 12px; - background: url('../assets/screenshot-1945.png') center/cover no-repeat; - border: 2px dashed rgba(244,241,230,0.55); - box-shadow: 0 10px 24px rgba(0,0,0,0.5); +.nav-toggle span, .nav-toggle::before, .nav-toggle::after { + content: ""; + position: absolute; + left: 9px; right: 9px; + height: 2px; + border-radius: 2px; + background: var(--chalk); + transition: transform .3s var(--ease), opacity .18s; } +.nav-toggle::before { top: 10px; } +.nav-toggle span { top: 16px; } +.nav-toggle::after { top: 22px; } +.topbar.menu-open .nav-toggle::before { transform: translateY(6px) rotate(45deg); } +.topbar.menu-open .nav-toggle span { opacity: 0; } +.topbar.menu-open .nav-toggle::after { transform: translateY(-6px) rotate(-45deg); } -/* Mode cards */ -.mode-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 18px; - margin-top: 18px; + +/* ========================================================= + FOOTER + ========================================================= */ + +.site-footer { + margin-top: var(--sec); + padding: 26px 24px 38px; + border-top: var(--rule); + background: rgba(7,17,11,0.55); + color: var(--chalk-dim); + font-size: 0.88rem; } -.mode-card { - padding: 22px; - cursor: pointer; - transition: transform .2s ease, border-color .2s; +.site-footer .footer-inner { + max-width: var(--maxw); + margin: 0 auto; + display: flex; flex-wrap: wrap; gap: 14px 24px; align-items: center; } -.mode-card:hover { transform: translateY(-3px) rotate(-0.4deg); } -.mode-card h3 { margin: 0 0 6px; } -.mode-card .tag { - display: inline-block; - font-size: .85rem; - padding: 2px 10px; - border: 1.5px dashed var(--chalk-faded); - border-radius: 999px; +.site-footer .footer-links { display: flex; gap: 22px; flex-wrap: wrap; } +.site-footer a { color: var(--chalk-soft); - margin-bottom: 8px; + font-size: 0.88rem; + border-bottom-color: transparent; } -.mode-card.disabled { opacity: 0.65; cursor: not-allowed; } - -/* Section heading */ -.section-title { - display: flex; align-items: baseline; gap: 14px; margin: 36px 0 6px; +.site-footer a:hover { border-bottom-color: var(--accent); } +.site-footer .footer-meta { + margin-left: auto; + font-family: var(--font-stamp); + font-size: 0.72rem; + letter-spacing: 0.04em; + opacity: 0.75; } -.section-title h2 { margin: 0; } -.section-title .see-all { font-size: 1rem; } -/* Gallery */ -.gallery { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 14px; - margin-top: 18px; -} -.gallery .shot { - position: relative; - height: 160px; + +/* ========================================================= + FORMS + ========================================================= */ + +input, select, textarea, button { font-family: inherit; font-size: inherit; } + +.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 15px; } +.field label { + font-family: var(--font-stamp); + font-size: 0.7rem; + letter-spacing: 0.09em; + text-transform: uppercase; + color: var(--chalk-dim); +} + +.field input, .field select, +.country-search, .settings-row select, .settings-row input[type="text"], +.chat-new-dm input, .chat-composer input, .text-input { + font-family: var(--font-body); + font-size: 0.98rem; + color: var(--chalk); + background: rgba(0,0,0,0.32); + border: var(--rule-strong); border-radius: 10px; - background-size: cover; - background-position: center; - border: 2px dashed rgba(244,241,230,0.5); - box-shadow: 0 6px 16px rgba(0,0,0,0.45); - transition: transform .2s; - cursor: zoom-in; + padding: 11px 14px; + outline: none; + width: 100%; + transition: border-color .18s, background .18s, box-shadow .18s; } -.gallery .shot:hover { transform: rotate(-0.6deg) scale(1.02); } -.gallery .shot.tall { height: 220px; } +.field input::placeholder, .text-input::placeholder, +.country-search::placeholder, .chat-composer input::placeholder { color: var(--chalk-ghost); } -/* Updates strip */ -.updates-strip { - display: flex; align-items: center; gap: 14px; - padding: 14px 20px; - margin-top: 12px; -} -.updates-strip .label { - font-family: 'Caveat', cursive; - font-size: 1.4rem; - color: var(--accent-soft); +.field input:focus, .field select:focus, +.country-search:focus, .settings-row select:focus, .settings-row input:focus, +.chat-new-dm input:focus, .chat-composer input:focus, .text-input:focus { + border-color: var(--accent); + background: rgba(0,0,0,0.44); + box-shadow: 0 0 0 3px rgba(217,122,61,0.2); } +/* :user-invalid, not :invalid — an untouched empty required field (the date + picker, "confirm password") is invalid from page load and shouldn't be + scolding anyone before they've typed. */ +.field input:user-invalid { border-color: var(--chalk-red); } +.field input:disabled, .chat-composer input:disabled { opacity: 0.5; cursor: not-allowed; } -/* Updates timeline */ -.timeline { display: flex; flex-direction: column; gap: 22px; margin-top: 18px; } -.update-entry { padding: 24px; } -.update-entry .meta { - display: flex; align-items: center; gap: 12px; margin-bottom: 8px; flex-wrap: wrap; +.checkbox { + display: flex; align-items: flex-start; gap: 10px; + cursor: pointer; + font-size: 0.92rem; + color: var(--chalk-soft); } -.update-entry .version { - font-family: 'Caveat', cursive; font-size: 1.5rem; - color: var(--chalk); padding: 2px 12px; - background: rgba(217,122,61,0.25); - border: 2px dashed var(--accent); - border-radius: 8px; -} -.update-entry .date { color: var(--chalk-faded); font-size: 1rem; } -.update-entry ul { padding-left: 22px; margin: 8px 0 0; } -.update-entry li { margin-bottom: 4px; } - -/* Terms */ -.tos-doc { max-width: 820px; margin: 0 auto; } -.tos-doc h2 { margin-top: 30px; } -.tos-doc h3 { - margin-top: 20px; - font-size: clamp(1.15rem, 1.7vw, 1.4rem); - color: var(--accent-soft); +.checkbox input { appearance: none; -webkit-appearance: none; margin: 0; } +.checkbox .box { + width: 21px; height: 21px; flex: 0 0 21px; + margin-top: 1px; + border: var(--rule-strong); + border-radius: 5px; + display: inline-flex; align-items: center; justify-content: center; + transition: background .15s, border-color .15s; } -.tos-doc .placeholder { font-style: italic; color: var(--chalk-faded); } -.tos-doc p, .tos-doc li { max-width: none; } -.tos-doc ul { padding-left: 22px; margin: 8px 0 0; } -.tos-doc ul li { margin-bottom: 6px; } - -/* Effective-date / lead line under the Terms title */ -.tos-doc .tos-meta { - display: flex; flex-wrap: wrap; gap: 6px 16px; - margin: 0 0 6px; color: var(--chalk-faded); +.checkbox input:checked + .box { background: var(--accent); border-color: var(--accent-soft); } +.checkbox input:checked + .box::after { + content: ""; + width: 11px; height: 6px; + border-left: 2.5px solid var(--board-ink); + border-bottom: 2.5px solid var(--board-ink); + transform: rotate(-45deg) translate(1px, -1px); } +.checkbox input:focus-visible + .box { outline: 2px solid var(--accent); outline-offset: 2px; } -/* In-page table of contents */ -.tos-toc { - margin-top: 18px; - padding: 16px 20px; +/* Segmented controls — auth tabs, country tabs, tier switch */ +.auth-tabs, .country-tabs, .tier-switch { + display: flex; + gap: 4px; + padding: 4px; + border: var(--rule); + border-radius: 12px; + background: rgba(0,0,0,0.28); + margin-bottom: 18px; +} +.auth-tabs button, .country-tabs button, .tier-switch button { + flex: 1; + padding: 9px 12px; + border: 1px solid transparent; + border-radius: 9px; + background: transparent; + font-family: var(--font-body); + font-weight: 600; + font-size: 0.92rem; + color: var(--chalk-dim); + cursor: pointer; + transition: background .18s var(--ease), color .18s, border-color .18s; } -.tos-toc h2 { margin: 0 0 8px; font-size: 1.5rem; } -.tos-toc ol { - columns: 2; column-gap: 28px; - padding-left: 20px; margin: 0; +.auth-tabs button:hover, .country-tabs button:hover, .tier-switch button:hover { + color: var(--chalk); + background: rgba(244,241,230,0.06); } -.tos-toc li { margin-bottom: 4px; break-inside: avoid; } -@media (max-width: 560px) { .tos-toc ol { columns: 1; } } - -/* Highlighted legal callout (e.g. GDPR note) */ -.tos-doc .legal-note { - margin-top: 26px; - padding: 16px 20px; - border: 2px dashed var(--accent); - border-radius: 10px; - background: rgba(217,122,61,0.10); +.auth-tabs button.active, .country-tabs button.active, .tier-switch button.active { + color: var(--chalk); + background: rgba(244,241,230,0.11); + border-color: rgba(244,241,230,0.2); + box-shadow: 0 2px 8px -4px #000; } -.tos-doc .legal-note .legal-tag { - display: inline-block; - font-family: 'Caveat', cursive; - font-size: 1.3rem; - color: var(--accent-soft); - margin-bottom: 4px; +.tier-switch { margin: 8px 0 0; } +.tier-switch button[data-tier="emperor"].active { + background: linear-gradient(135deg, rgba(240,198,97,0.28), rgba(217,122,61,0.28)); + border-color: var(--chalk-gold); + color: #ffeec2; } -/* About page */ -.about-hero { - text-align: center; padding: 40px 24px 28px; -} -.about-hero h1 { font-size: clamp(2.6rem, 6vw, 4rem); } -.about-grid { - display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 18px; margin-top: 20px; +.form-actions { display: flex; flex-direction: column; gap: 11px; margin-top: 4px; } +.form-actions .btn { width: 100%; } +.form-actions .row-or { + position: relative; + text-align: center; + font-family: var(--font-stamp); + font-size: 0.64rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--chalk-dim); + margin: 2px 0; +} +.form-actions .row-or::before, .form-actions .row-or::after { + content: ""; + position: absolute; top: 50%; + width: calc(50% - 26px); height: 1px; + background: rgba(244,241,230,0.16); } +.form-actions .row-or::before { left: 0; } +.form-actions .row-or::after { right: 0; } -/* Play page */ -.play-screen { - min-height: calc(100vh - 100px); - display: flex; flex-direction: column; - align-items: center; justify-content: center; - text-align: center; gap: 24px; - padding: 40px 24px; +/* Toggle */ +.toggle { position: relative; width: 48px; height: 26px; display: inline-block; flex: 0 0 auto; } +.toggle input { opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; } +.toggle .track { + position: absolute; inset: 0; + background: rgba(0,0,0,0.5); + border: var(--rule-strong); + border-radius: 999px; + transition: background .2s, border-color .2s; +} +.toggle .thumb { + position: absolute; top: 3px; left: 3px; + width: 18px; height: 18px; + background: var(--chalk-dim); + border-radius: 50%; + transition: transform .26s var(--ease), background .2s; } -.play-screen h1 { font-size: clamp(2.5rem, 6vw, 4.5rem); margin-bottom: 0; } -.play-screen .subtitle { font-size: 1.25rem; color: var(--chalk-faded); } +.toggle input:checked + .track { background: rgba(217,122,61,0.4); border-color: var(--accent); } +.toggle input:checked + .track + .thumb { transform: translateX(22px); background: var(--accent-soft); } +.toggle input:focus-visible + .track { outline: 2px solid var(--accent); outline-offset: 3px; } -/* Toast */ -.toast { - position: fixed; left: 50%; bottom: 30px; transform: translateX(-50%); - background: rgba(0,0,0,0.85); - color: var(--chalk); - font-family: 'Patrick Hand', cursive; - font-size: 1.05rem; - padding: 12px 22px; - border: 2px dashed var(--chalk); - border-radius: 10px; - z-index: var(--z-toast); - opacity: 0; pointer-events: none; - transition: opacity .25s ease, transform .25s ease; -} -.toast.show { opacity: 1; transform: translateX(-50%) translateY(-6px); } - -/* ============= Responsive ============= */ -@media (max-width: 760px) { - body { font-size: 17px; } - .topbar .inner { padding: 8px 12px; gap: 6px; } - .topbar nav { display: none; } - .topbar.mobile-open nav { - display: flex; flex-direction: column; align-items: flex-start; - position: absolute; top: 100%; left: 0; right: 0; - padding: 12px 16px; - background: rgba(10,22,15,0.97); - border-bottom: 2px dashed rgba(244,241,230,0.25); - } - .nav-toggle { display: inline-flex !important; } - /* Tighten the right cluster — much smaller chips + icon button */ - .topbar .topbar-left { gap: 8px; } - .topbar .topbar-right { gap: 6px; } - .topbar .topbar-right::before { display: none; } +/* ========================================================= + AUTH LANDING + ========================================================= */ - .topbar .brand img { width: 32px; height: 32px; } - .topbar .brand .brand-name { font-size: 1.2rem; } +.auth-wrap { + display: grid; + grid-template-columns: 1fr 460px; + align-items: center; + gap: clamp(28px, 6vw, 76px); + padding: 48px 24px 40px; + max-width: 1080px; +} - .coin-chip { - padding: 3px 8px; - font-size: 0.85rem; - border-width: 1.5px; - box-shadow: 0 2px 0 rgba(0,0,0,0.3); - } - .coin-chip .coin { width: 14px; height: 14px; } - .coin-chip .soon { display: none; } +.auth-pitch { max-width: 520px; } +.auth-logo { + width: clamp(230px, 26vw, 340px); + display: block; + margin: 0 0 6px -6px; + filter: drop-shadow(0 12px 26px rgba(0,0,0,0.6)); + user-select: none; +} +.auth-tagline { + font-family: var(--font-display); + font-size: clamp(1.5rem, 2.6vw, 2.05rem); + color: var(--chalk); + margin: 0 0 22px; + line-height: 1.2; +} +.auth-facts { + list-style: none; + padding: 0; + margin: 22px 0 0; + display: flex; + flex-direction: column; + gap: 11px; + border-top: var(--rule); + padding-top: 20px; +} +.auth-facts li { + display: flex; + align-items: flex-start; + gap: 12px; + color: var(--chalk-soft); + font-size: 0.95rem; +} +.auth-facts svg { + width: 19px; height: 19px; + flex: 0 0 19px; + margin-top: 2px; + color: var(--accent-soft); +} +.auth-facts strong { display: block; color: var(--chalk); font-weight: 600; } - .user-chip { - padding: 3px 8px; - gap: 6px; - border-width: 1.5px; - } - .user-chip .avatar { - width: 22px; height: 22px; - font-size: 0.75rem; - } - .user-chip .avatar.countryball { width: 24px; height: 24px; } - .user-block .name { font-size: 0.95rem; } - .tier-badge { font-size: 0.75rem; padding: 0 6px; } - - /* The settings gear was 38×38 on desktop — way too much on mobile. */ - .btn-icon { - width: 30px; height: 30px; - border-width: 1.5px; - } - .btn-icon svg { width: 14px; height: 14px; } +.auth-card { width: 100%; padding: 30px 28px; } +.auth-card h2 { font-size: 1.9rem; } +.auth-panel[hidden] { display: none; } - .hero-card { flex-direction: column; padding: 24px; } - .hero-card .hero-art { width: 100%; flex: none; } - .auth-card { padding: 20px; } - .btn-huge { font-size: 2rem; padding: 22px 48px; } - .site-footer .footer-inner { flex-direction: column; align-items: flex-start; gap: 8px; } - .site-footer .footer-meta { margin-left: 0; } +.auth-shot { + margin-top: 26px; + border: var(--rule-strong); + border-radius: var(--r); + overflow: hidden; + box-shadow: var(--lift-2); + aspect-ratio: 16 / 8; + background: url('../assets/screenshot-1945.png') center/cover no-repeat; + position: relative; } - -/* Even tighter for very narrow screens */ -@media (max-width: 420px) { - .topbar .inner { padding: 6px 10px; gap: 4px; } - .topbar .brand .brand-name { display: none; } - .coin-chip { padding: 2px 7px; } - .user-block .name { max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.auth-shot::after { + content: ""; + position: absolute; inset: 0; + background: linear-gradient(180deg, transparent 40%, rgba(7,17,11,0.75)); } -.nav-toggle { - display: none; - background: transparent; - color: var(--chalk); - border: 2px dashed var(--chalk); - border-radius: 8px; - padding: 4px 10px; - font-family: 'Caveat', cursive; - font-size: 1.2rem; - cursor: pointer; +@media (max-width: 900px) { + .auth-wrap { + grid-template-columns: 1fr; + justify-items: center; + text-align: center; + padding-top: 34px; + } + .auth-pitch { max-width: 560px; } + .auth-logo { margin: 0 auto 6px; } + .auth-facts { text-align: left; } + .auth-shot { display: none; } + .auth-card { max-width: 460px; } } -/* ============================================================ - POLISH LAYER — micro-interactions, hover states, scroll fades - ============================================================ */ -/* Cards: lift + soft glow on every chalk-card by default - (mode-card, about-grid items, update entries all inherit this) */ -.chalk-card { - transition: transform .25s cubic-bezier(.2,.7,.2,1), - box-shadow .25s ease, - border-color .25s ease; - will-change: transform; -} -.chalk-card.interactive, -a.chalk-card, -button.chalk-card { +/* ========================================================= + HERO + ========================================================= */ + +.hero-card { + display: grid; + grid-template-columns: 1fr 300px; + gap: 30px; + align-items: center; + padding: 34px; + overflow: hidden; + background: + linear-gradient(115deg, rgba(217,122,61,0.16), rgba(0,0,0,0.28) 55%), + linear-gradient(180deg, rgba(244,241,230,0.05), rgba(0,0,0,0.24)), + rgba(10,22,15,0.4); +} +.hero-card .hero-text { min-width: 0; } +.hero-card .hero-text h1 { margin-bottom: 10px; } +.hero-card .hero-art { + display: block; + height: 178px; + border: var(--rule-strong); + border-bottom: none; + border-radius: var(--r); + background: url('../assets/screenshot-1945.png') center/cover no-repeat; + box-shadow: var(--lift-2); cursor: pointer; + transition: transform .35s var(--ease), box-shadow .35s var(--ease); } -.chalk-card.interactive:hover, -a.chalk-card:hover, -button.chalk-card:hover, -.mode-card:not(.disabled):hover, -.about-grid .chalk-card:hover, -.update-entry:hover { - transform: translateY(-4px) rotate(-0.35deg); - box-shadow: - 0 0 0 2px rgba(244,241,230,0.10), - 0 18px 40px rgba(0,0,0,0.55), - 0 0 24px rgba(217,122,61,0.18); -} -.chalk-card.interactive:hover::before, -a.chalk-card:hover::before, -.mode-card:not(.disabled):hover::before, -.about-grid .chalk-card:hover::before, -.update-entry:hover::before { - border-color: var(--accent); - animation: chalk-jitter 0.5s ease-in-out; +.hero-card .hero-art:hover { + transform: scale(1.025) rotate(-0.8deg); + box-shadow: var(--lift-3); } -@keyframes chalk-jitter { - 0%, 100% { transform: rotate(0); } - 25% { transform: rotate(0.4deg); } - 75% { transform: rotate(-0.3deg); } + +/* The tray the chalk actually sits in. Used once per page, under the hero. */ +.chalk-tray { + position: relative; + height: 15px; + margin: -1px auto 0; + max-width: calc(100% - 26px); + border-radius: 0 0 9px 9px; + background: linear-gradient(180deg, var(--wood-1) 0%, #56361b 45%, var(--wood-2) 100%); + box-shadow: 0 7px 16px -8px rgba(0,0,0,0.9), inset 0 1px 0 rgba(255,220,180,0.18); +} +.chalk-tray::before { + content: ""; + position: absolute; + left: 34px; top: -5px; + width: 52px; height: 7px; + border-radius: 3px; + background: linear-gradient(180deg, #fdfaf0, #cfc9b4); + box-shadow: 0 1px 2px rgba(0,0,0,0.5); + transform: rotate(-0.7deg); +} +.chalk-tray::after { + content: ""; + position: absolute; + right: 40px; top: -7px; + width: 34px; height: 9px; + border-radius: 2px; + background: linear-gradient(180deg, #5d6a76, #38424c); + box-shadow: 0 1px 2px rgba(0,0,0,0.5); + transform: rotate(0.6deg); } -/* Active press */ -.chalk-card.interactive:active, -a.chalk-card:active, -.mode-card:not(.disabled):active { - transform: translateY(-1px) rotate(0); - transition-duration: .08s; +@media (max-width: 780px) { + .hero-card { grid-template-columns: 1fr; padding: 24px; } + .hero-card .hero-art { height: 150px; } + .chalk-tray::after { display: none; } +} + + +/* ========================================================= + MODE CARDS + ========================================================= */ + +.mode-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr)); + gap: 16px; + margin-top: 20px; } -/* Mode card title underline animation */ -.mode-card h3 { - display: inline-block; +.chalk-card.mode-card { position: relative; + padding: 22px; + display: flex; + flex-direction: column; + gap: 7px; + overflow: hidden; +} +.mode-card .mode-era { + font-family: var(--font-stamp); + font-size: 0.62rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent-soft); + order: -1; } +.mode-card h3 { margin: 0; position: relative; align-self: flex-start; } .mode-card h3::after { content: ""; position: absolute; - left: 0; right: 100%; bottom: -2px; - height: 2px; + left: 0; bottom: -3px; + height: 2px; width: 100%; background: var(--accent); - transition: right .35s cubic-bezier(.2,.7,.2,1); border-radius: 2px; + transform: scaleX(0); + transform-origin: left center; + transition: transform .38s var(--ease); } -.mode-card:not(.disabled):hover h3::after { right: 0; } +.mode-card:not(.disabled):hover h3::after { transform: scaleX(1); } +.mode-card p { margin: 0; font-size: 0.94rem; color: var(--chalk-soft); } -/* About-grid icons */ -.about-grid .chalk-card { - position: relative; - overflow: hidden; -} -.about-grid .chalk-card::after { - content: ""; +.mode-card.disabled { cursor: not-allowed; } +.mode-card.disabled h3, .mode-card.disabled p { color: var(--chalk-dim); } +.mode-card.disabled .mode-era { color: var(--chalk-dim); } +.mode-card .mode-flag { position: absolute; - right: -30px; top: -30px; - width: 120px; height: 120px; - background: radial-gradient(circle at center, rgba(217,122,61,0.18), transparent 65%); - opacity: 0; - transition: opacity .35s ease, transform .5s ease; - pointer-events: none; + top: 14px; right: 14px; + font-family: var(--font-stamp); + font-size: 0.58rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--chalk-dim); + border: 1px dashed rgba(244,241,230,0.35); + border-radius: 3px; + padding: 1px 7px; + transform: rotate(3deg); } -.about-grid .chalk-card:hover::after { - opacity: 1; - transform: scale(1.15) rotate(15deg); + + +/* ========================================================= + FEATURE GRID (about / play) + ========================================================= */ + +/* Capped at three so the feature sets stay balanced: Play's trio fills one + row, About's six read as 3 + 3 rather than a stranded 4 + 2. + + Every auto-fit grid in this file wraps its track minimum in min(_, 100%). + A bare minmax(300px, 1fr) refuses to go under 300px, so on a 320px phone + the cards pushed 28px past the screen and the whole page scrolled + sideways. Above 300px the two are the same number, so nothing else moves. */ +.about-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); + gap: 16px; + margin-top: 20px; } +.about-grid .chalk-card { padding: 22px; } .about-grid h3 { - display: flex; align-items: center; gap: 10px; + display: flex; + align-items: center; + gap: 11px; + font-size: 1.35rem; + margin: 0 0 8px; } .about-grid h3 .ico { - display: inline-flex; align-items: center; justify-content: center; + display: inline-flex; + align-items: center; justify-content: center; width: 34px; height: 34px; - border: 2px dashed var(--accent); - border-radius: 8px; + flex: 0 0 34px; + border: var(--rule-strong); + border-radius: 9px; color: var(--accent-soft); - font-size: 1.2rem; - transition: transform .3s ease, background .3s ease; + background: rgba(217,122,61,0.09); + transition: transform .3s var(--ease), background .3s, border-color .3s; } +.about-grid h3 .ico svg { width: 18px; height: 18px; } .about-grid .chalk-card:hover h3 .ico { - transform: rotate(-8deg) scale(1.1); - background: rgba(217,122,61,0.15); + transform: rotate(-7deg); + background: rgba(217,122,61,0.2); + border-color: var(--accent); } +.about-grid p { margin: 0; font-size: 0.94rem; } -/* Buttons — subtle hover wobble + glow */ -.btn-primary { - position: relative; -} -.btn-primary::after { - content: ""; - position: absolute; - inset: -6px; - border-radius: 14px; - background: radial-gradient(circle, rgba(217,122,61,0.35), transparent 70%); - opacity: 0; - transition: opacity .3s ease; - z-index: -1; - pointer-events: none; -} -.btn-primary:hover:not(:disabled)::after { opacity: 1; } +.about-hero { text-align: center; padding: 46px 24px 12px; } +.about-hero p { margin-left: auto; margin-right: auto; } -/* Footer link hover — chalk underline grow */ -.site-footer a, -.topbar nav a { - position: relative; - border-bottom: none; -} -.site-footer a::after, -.topbar nav a::after { - content: ""; - position: absolute; - left: 8px; right: 8px; - bottom: 0; - height: 2px; - background-image: linear-gradient(to right, var(--chalk) 60%, transparent 60%); - background-size: 8px 2px; - transform: scaleX(0); - transform-origin: left center; - transition: transform .3s cubic-bezier(.2,.7,.2,1); -} -.site-footer a:hover::after, -.topbar nav a:hover::after, -.topbar nav a.active::after { transform: scaleX(1); } -.topbar nav a.active::after { background-image: linear-gradient(to right, var(--accent) 60%, transparent 60%); } -/* Logo subtle float in topbar */ -.topbar .brand img { - transition: transform .35s ease; +/* ========================================================= + UPDATES + ========================================================= */ + +.updates-strip { + display: flex; + align-items: center; + gap: 16px; + padding: 15px 20px; + margin-top: 18px; + border-left: 3px solid var(--accent); + text-decoration: none; + flex-wrap: wrap; } -.topbar .brand:hover img { - transform: rotate(-6deg) scale(1.08); +.updates-strip .label { + font-family: var(--font-stamp); + font-size: 0.78rem; + letter-spacing: 0.06em; + color: var(--accent-soft); } +.updates-strip .muted { font-size: 0.93rem; } -/* Auth logo gentle breathing */ -.auth-logo { - animation: breathe 6s ease-in-out infinite; +.timeline { display: flex; flex-direction: column; gap: 18px; margin-top: 20px; } +.update-entry { padding: 24px; border-left: 3px solid rgba(217,122,61,0.55); } +.update-entry .meta { + display: flex; align-items: center; gap: 12px; flex-wrap: wrap; + margin-bottom: 6px; } -@keyframes breathe { - 0%, 100% { transform: translateY(0) rotate(0); } - 50% { transform: translateY(-4px) rotate(-0.4deg); } +.update-entry .version { + color: var(--accent-soft); + border: 1px solid rgba(217,122,61,0.5); + background: rgba(217,122,61,0.12); + border-radius: 4px; + padding: 1px 9px; + font-size: 0.72rem; } +.update-entry .date { color: var(--chalk-dim); font-size: 0.68rem; } +.update-entry .meta .muted { font-size: 0.86rem; text-transform: none; letter-spacing: 0; } +.update-entry h2 { font-size: 1.7rem; margin: 0 0 8px; } +.update-entry ul { padding-left: 20px; margin: 10px 0 0; } +.update-entry li { margin-bottom: 5px; font-size: 0.95rem; } +.update-entry li::marker { color: var(--accent); } -/* Input focus shimmer */ -.field input:focus, .field select:focus { - box-shadow: 0 0 0 3px rgba(217,122,61,0.25); -} -/* Reveal-on-scroll */ -.reveal { - opacity: 0; - transform: translateY(20px); - transition: opacity .6s ease, transform .6s cubic-bezier(.2,.7,.2,1); -} -.reveal.in { - opacity: 1; - transform: translateY(0); -} -.reveal.delay-1 { transition-delay: .08s; } -.reveal.delay-2 { transition-delay: .16s; } -.reveal.delay-3 { transition-delay: .24s; } -.reveal.delay-4 { transition-delay: .32s; } +/* ========================================================= + PREVIEW SHOWCASE + ========================================================= */ -/* ============================================================ - PREVIEW FOOTAGE SHOWCASE - ============================================================ */ .preview-showcase { - position: relative; display: grid; - grid-template-columns: 1fr 280px; - gap: 20px; - padding: 22px; - margin-top: 18px; + grid-template-columns: 1fr 250px; + gap: 18px; + padding: 18px; + margin-top: 20px; } .preview-stage { position: relative; aspect-ratio: 16 / 9; - border-radius: 12px; + border-radius: var(--r); overflow: hidden; background: #000; - border: 2px dashed rgba(244,241,230,0.55); - box-shadow: 0 14px 36px rgba(0,0,0,0.55), inset 0 0 0 1px rgba(0,0,0,0.5); + border: var(--rule-strong); + box-shadow: var(--lift-2); } .preview-stage .slide { - position: absolute; - inset: 0; - background-size: cover; - background-position: center; + position: absolute; inset: 0; + background-size: cover; background-position: center; opacity: 0; - transform: scale(1.04); - transition: opacity .6s ease, transform 6s ease-out; -} -.preview-stage .slide.active { - opacity: 1; - transform: scale(1.0); + transform: scale(1.05); + transition: opacity .6s ease, transform 6.5s ease-out; } +.preview-stage .slide.active { opacity: 1; transform: scale(1); } + .preview-stage .stage-overlay { - position: absolute; - left: 0; right: 0; bottom: 0; - padding: 18px 20px; - background: linear-gradient(180deg, transparent, rgba(0,0,0,0.85)); - color: var(--chalk); + position: absolute; left: 0; right: 0; bottom: 0; + padding: 30px 20px 16px; + background: linear-gradient(180deg, transparent, rgba(0,0,0,0.88)); pointer-events: none; } .preview-stage .stage-overlay .caption { - font-family: 'Caveat', cursive; - font-size: 1.6rem; + font-family: var(--font-display); + font-size: 1.55rem; + color: var(--chalk); + margin: 0 0 2px; line-height: 1.1; - margin: 0 0 4px; - text-shadow: 0 2px 6px rgba(0,0,0,0.8); + text-shadow: 0 2px 8px rgba(0,0,0,0.9); } .preview-stage .stage-overlay .sub { - font-size: 1rem; + margin: 0; + font-size: 0.85rem; color: var(--chalk-soft); - opacity: 0.9; } .preview-stage .tag-live { - position: absolute; - top: 12px; left: 12px; + position: absolute; top: 12px; left: 12px; + font-family: var(--font-stamp); + font-size: 0.62rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--board-ink); background: var(--accent); - color: var(--board-3); - padding: 4px 10px; - border-radius: 6px; - font-family: 'Caveat', cursive; - font-weight: 700; - font-size: 1rem; - border: 2px solid var(--chalk); + padding: 3px 9px; + border-radius: 4px; pointer-events: none; } .preview-stage .stage-nav { position: absolute; - top: 50%; transform: translateY(-50%); - width: 40px; height: 40px; - background: rgba(15,32,24,0.7); - border: 2px dashed var(--chalk); - color: var(--chalk); + top: 50%; + width: 38px; height: 38px; + transform: translateY(-50%); + border: var(--rule-strong); border-radius: 50%; + background: rgba(7,17,11,0.78); + backdrop-filter: blur(4px); + color: var(--chalk); + font-size: 1.3rem; + line-height: 1; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; - font-family: 'Caveat', cursive; - font-size: 1.6rem; - font-weight: 700; opacity: 0; - transition: opacity .25s ease, background .2s, transform .2s; + transition: opacity .25s, background .18s, transform .18s; z-index: 3; } -.preview-stage:hover .stage-nav { opacity: 1; } -.preview-stage .stage-nav:hover { background: var(--accent); color: var(--board-3); transform: translateY(-50%) scale(1.08); } +.preview-stage:hover .stage-nav, +.preview-stage:focus-within .stage-nav { opacity: 1; } +.preview-stage .stage-nav:hover { + background: var(--accent); + color: var(--board-ink); + transform: translateY(-50%) scale(1.08); +} .preview-stage .stage-nav.prev { left: 12px; } .preview-stage .stage-nav.next { right: 12px; } +.preview-progress { + position: absolute; left: 0; right: 0; bottom: 0; + height: 3px; + background: rgba(0,0,0,0.5); + z-index: 2; +} +.preview-progress .bar { + height: 100%; width: 0; + background: var(--accent); + transition: width 80ms linear; +} + .preview-thumbs { - display: flex; - flex-direction: column; - gap: 10px; + display: flex; flex-direction: column; gap: 9px; max-height: 100%; overflow-y: auto; padding-right: 4px; @@ -1081,1313 +1428,1146 @@ a.chalk-card:active, .preview-thumbs .thumb { position: relative; flex: 0 0 auto; - height: 70px; - border-radius: 8px; - border: 2px dashed rgba(244,241,230,0.35); - background-size: cover; - background-position: center; + height: 64px; + border-radius: var(--r-sm); + border: var(--rule); + background-size: cover; background-position: center; cursor: pointer; - transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease; overflow: hidden; + transition: transform .2s var(--ease), border-color .2s, box-shadow .2s; } .preview-thumbs .thumb::after { content: ""; position: absolute; inset: 0; - background: rgba(0,0,0,0.35); - transition: background .2s ease; + background: rgba(0,0,0,0.5); + transition: background .2s; } -.preview-thumbs .thumb:hover { transform: translateX(-4px); border-color: var(--chalk); } -.preview-thumbs .thumb:hover::after { background: rgba(0,0,0,0.1); } +.preview-thumbs .thumb:hover { border-color: rgba(244,241,230,0.4); transform: translateX(-3px); } +.preview-thumbs .thumb:hover::after { background: rgba(0,0,0,0.18); } .preview-thumbs .thumb.active { border-color: var(--accent); - border-style: solid; - box-shadow: 0 0 0 2px rgba(217,122,61,0.35), 0 8px 18px rgba(0,0,0,0.5); - transform: translateX(-6px); + box-shadow: 0 0 0 1px var(--accent), var(--lift-1); + transform: translateX(-5px); } -.preview-thumbs .thumb.active::after { background: transparent; } +.preview-thumbs .thumb.active::after { background: rgba(0,0,0,0.05); } .preview-thumbs .thumb .label { - position: absolute; left: 8px; bottom: 6px; right: 8px; - font-family: 'Caveat', cursive; - font-size: 1rem; - color: var(--chalk); - text-shadow: 0 1px 3px rgba(0,0,0,0.9); + position: absolute; left: 9px; right: 9px; bottom: 6px; z-index: 2; + font-family: var(--font-stamp); + font-size: 0.62rem; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--chalk); + text-shadow: 0 1px 4px rgba(0,0,0,1); } +.preview-thumbs::-webkit-scrollbar { width: 6px; height: 6px; } +.preview-thumbs::-webkit-scrollbar-thumb { background: rgba(244,241,230,0.2); border-radius: 6px; } -.preview-progress { - position: absolute; - left: 0; right: 0; bottom: 0; - height: 3px; - background: rgba(0,0,0,0.4); - z-index: 2; -} -.preview-progress .bar { - height: 100%; - width: 0%; - background: var(--accent); - transition: width 80ms linear; -} +/* Counter. Desktop has five thumbnails in view at all times, so it already + knows where it is; on a phone the strip is scrolled and it doesn't. */ +.preview-count { display: none; } -/* Lightbox */ -.lightbox { - position: fixed; inset: 0; - background: rgba(10,22,15,0.94); - z-index: var(--z-overlay); - display: flex; align-items: center; justify-content: center; - cursor: zoom-out; - animation: fade-in .25s ease; -} -.lightbox img { - max-width: 92vw; - max-height: 88vh; - border: 2px dashed var(--chalk); - border-radius: 10px; - box-shadow: 0 20px 60px rgba(0,0,0,0.7); -} -.lightbox .close { - position: absolute; - top: 18px; right: 22px; - width: 44px; height: 44px; - display: inline-flex; align-items: center; justify-content: center; - border: 2px dashed var(--chalk); - border-radius: 50%; - background: transparent; - color: var(--chalk); - cursor: pointer; - font-family: 'Caveat', cursive; - font-size: 1.6rem; -} -.lightbox .close:hover { background: var(--chalk); color: var(--board-3); } -@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } +@media (max-width: 880px) { + .preview-showcase { grid-template-columns: 1fr; } + .preview-thumbs { flex-direction: row; overflow-x: auto; padding-bottom: 6px; } + .preview-thumbs .thumb { flex: 0 0 132px; height: 74px; } + .preview-thumbs .thumb:hover, .preview-thumbs .thumb.active { transform: translateY(-3px); } +} + +/* --- Carousel on a phone ------------------------------------------------- + Landing here it was a 289px-wide 16:9 letterbox inside two layers of + padding, with the only controls hidden behind :hover. On a touch screen + that is a picture, not a carousel. So: bleed the stage to the card edge, + give it a shape a phone can actually read, and put the controls on screen. + ------------------------------------------------------------------------ */ +@media (max-width: 620px) { + .preview-showcase { padding: 0 0 14px; gap: 12px; overflow: hidden; } + + /* Edge to edge. The card's own border draws the frame, so the stage drops + its border and its top corners follow the card's. */ + .preview-stage { + aspect-ratio: 4 / 3; + border-radius: var(--r-lg) var(--r-lg) 0 0; + border: none; + border-bottom: var(--rule-strong); + box-shadow: none; + } -/* Make hero art look photo-framed and clickable */ -.hero-card .hero-art { - cursor: pointer; - transition: transform .35s ease, box-shadow .35s ease; -} -.hero-card .hero-art:hover { - transform: rotate(-1.2deg) scale(1.02); - box-shadow: 0 16px 38px rgba(0,0,0,0.6); + .preview-stage .stage-overlay { padding: 42px 16px 14px; } + .preview-stage .stage-overlay .caption { font-size: 1.3rem; } + .preview-stage .stage-overlay .sub { font-size: 0.78rem; } + /* Shares the top row with the slide counter now, so it gives up some width. */ + .preview-stage .tag-live { top: 10px; left: 10px; font-size: 0.54rem; padding: 3px 7px; } + + .preview-thumbs { padding: 0 14px 4px; gap: 8px; } + .preview-thumbs .thumb { flex: 0 0 118px; height: 66px; } + .preview-thumbs .thumb .label { font-size: 0.56rem; } } -/* Preview responsive */ -@media (max-width: 860px) { - .preview-showcase { - grid-template-columns: 1fr; +/* Below 880 the card is already stacked, so the arrows come out from behind + :hover and get sized for a thumb. Keyed to width rather than (hover: none) + on purpose — plenty of touch devices report hover: hover, and hiding a + carousel's only controls from them leaves the reel with no controls at all. */ +@media (max-width: 880px) { + .preview-stage .stage-nav { + opacity: 1; + width: 44px; height: 44px; + font-size: 1.5rem; + background: rgba(7,17,11,0.62); + border-color: rgba(244,241,230,0.34); + /* Lifted off centre so they clear the caption block underneath. */ + top: 42%; } - .preview-thumbs { - flex-direction: row; - overflow-x: auto; - overflow-y: hidden; - padding-bottom: 6px; + .preview-stage .stage-nav.prev { left: 8px; } + .preview-stage .stage-nav.next { right: 8px; } + .preview-stage .stage-nav:active { + background: var(--accent); + color: var(--board-ink); + transform: translateY(-50%) scale(0.94); + } + + /* Where you are in the reel, since only two thumbs fit on screen. */ + .preview-count { + display: block; + position: absolute; + top: 10px; right: 10px; + z-index: 3; + padding: 3px 9px; + border-radius: 999px; + background: rgba(7,17,11,0.7); + border: var(--rule); + font-family: var(--font-stamp); + font-size: 0.62rem; + letter-spacing: 0.1em; + color: var(--chalk-soft); + pointer-events: none; } - .preview-thumbs .thumb { - flex: 0 0 140px; - height: 80px; + + /* Snap, so a flick lands on a thumbnail instead of halfway across one. + The 6px scrollbar it replaces drew a grey stub under the strip on + Android and nothing at all on iOS. */ + .preview-thumbs { + scroll-snap-type: x mandatory; + scroll-padding-left: 14px; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; } - .preview-thumbs .thumb:hover, - .preview-thumbs .thumb.active { - transform: translateY(-4px); + .preview-thumbs::-webkit-scrollbar { display: none; } + .preview-thumbs .thumb { scroll-snap-align: start; } +} + +/* No hover on touch, so the thumbnail lift only ever means "this is the one". */ +@media (hover: none) and (max-width: 880px) { + .preview-stage .stage-nav:hover { + background: rgba(7,17,11,0.62); + color: var(--chalk); + transform: translateY(-50%); } + .preview-thumbs .thumb:hover { transform: none; border-color: rgba(244,241,230,0.13); } + .preview-thumbs .thumb:hover::after { background: rgba(0,0,0,0.5); } + .preview-thumbs .thumb.active { transform: none; } + .preview-thumbs .thumb.active::after { background: rgba(0,0,0,0.05); } } -/* ============================================================ - STORE / MARKETPLACE - ============================================================ */ -.store-grid { + +/* ========================================================= + PLAY + ========================================================= */ + +/* Launch panel — the copy holds one side, the actual board holds the other. + A single centred button in an empty card is a poster, not a game page. */ +.launch-panel { display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 18px; - margin-top: 18px; + grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr); + align-items: stretch; + padding: 0; + margin-top: 26px; + overflow: hidden; } -.store-item { - position: relative; - padding: 18px; - display: flex; flex-direction: column; gap: 10px; + +.launch-copy { + padding: clamp(28px, 3.4vw, 48px); + display: flex; + flex-direction: column; + justify-content: center; + min-width: 0; } -.store-item .thumb { - position: relative; - height: 130px; - border-radius: 10px; - border: 2px dashed rgba(244,241,230,0.45); - background: linear-gradient(160deg, rgba(217,122,61,0.18), rgba(0,0,0,0.35)); - display: flex; align-items: center; justify-content: center; - overflow: hidden; +.launch-copy h1 { + margin: 0 0 12px; + font-size: clamp(2.4rem, 4.2vw, 3.5rem); } -.store-item .thumb svg { width: 78%; height: 78%; } -.store-item h3 { - font-family: 'Caveat', cursive; - margin: 0; - font-size: 1.5rem; +.launch-copy .lede { + font-size: 1.04rem; + color: var(--chalk-soft); + max-width: 40ch; + margin: 0 0 28px; } -.store-item p { - font-size: 1rem; + +.launch-actions { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} +.btn-launch { + font-size: 1.12rem; + padding: 15px 34px; + border-radius: 12px; +} +.btn-launch svg { width: 19px; height: 19px; } +.launch-note { + font-family: var(--font-stamp); + font-size: 0.66rem; + letter-spacing: 0.06em; + color: var(--chalk-dim); +} + +/* Spec list, not a marketing stat bar */ +.launch-spec { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 15px 28px; + margin: 30px 0 0; + padding-top: 22px; + border-top: var(--rule); +} +.launch-spec div { min-width: 0; } +.launch-spec dt { + font-family: var(--font-stamp); + font-size: 0.6rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--chalk-dim); + margin-bottom: 3px; +} +.launch-spec dd { margin: 0; - color: var(--chalk-soft); - min-height: 2.6em; + font-size: 0.95rem; + font-weight: 600; + color: var(--chalk); } -.store-item .row-bottom { - display: flex; align-items: center; justify-content: space-between; gap: 8px; - margin-top: auto; + +/* The board itself, bled to the card edge */ +.launch-plate { + position: relative; + min-height: 380px; + overflow: hidden; } -.lock-badge { - display: inline-flex; align-items: center; gap: 6px; - background: rgba(0,0,0,0.45); - border: 1.5px dashed var(--chalk-faded); - color: var(--chalk-soft); - font-family: 'Caveat', cursive; - font-size: 1rem; - padding: 3px 10px; - border-radius: 999px; +.launch-plate img { + position: absolute; + inset: 0; + width: 100%; height: 100%; + object-fit: cover; + object-position: 60% center; } -.lock-badge::before { +/* Fade the inner edge into the board so the plate reads as part of the card + rather than a screenshot pasted on top of it. */ +.launch-plate::before { content: ""; - width: 12px; height: 14px; - background: currentColor; - -webkit-mask: url("data:image/svg+xml;utf8,") center/contain no-repeat; - mask: url("data:image/svg+xml;utf8,") center/contain no-repeat; - display: inline-block; + position: absolute; inset: 0; z-index: 1; + background: linear-gradient(90deg, + rgba(10,22,15,0.97) 0%, + rgba(10,22,15,0.55) 20%, + rgba(10,22,15,0) 52%); } -.price-pill { - display: inline-flex; align-items: center; gap: 6px; - font-family: 'Caveat', cursive; - font-size: 1.1rem; - color: var(--chalk-faded); - padding: 4px 10px; - border: 1.5px dashed var(--chalk-faded); - border-radius: 999px; +.launch-plate::after { + content: ""; + position: absolute; inset: 0; z-index: 1; + background: linear-gradient(0deg, rgba(7,17,11,0.65), transparent 42%); } -.price-pill .coin { - display: inline-block; width: 14px; height: 14px; border-radius: 50%; - background: radial-gradient(circle at 35% 35%, #fff2b3, var(--accent) 60%, var(--accent-deep)); - border: 1.5px solid var(--board-3); +.plate-caption { + position: absolute; + right: 16px; bottom: 13px; z-index: 2; + font-family: var(--font-stamp); + font-size: 0.62rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--chalk-soft); + text-shadow: 0 1px 5px rgba(0,0,0,0.95); +} + +@media (max-width: 900px) { + .launch-panel { grid-template-columns: 1fr; } + .launch-plate { + order: -1; + min-height: 0; + aspect-ratio: 16 / 8; + } + .launch-plate::before { + background: linear-gradient(0deg, rgba(10,22,15,0.95) 0%, rgba(10,22,15,0) 58%); + } } +@media (max-width: 520px) { + .launch-spec { grid-template-columns: 1fr; gap: 13px; } +} + + + +/* ========================================================= + STORE + ========================================================= */ .store-featured { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 18px; - margin-top: 18px; + grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); + gap: 16px; + margin-top: 20px; } .store-featured .featured-card { position: relative; padding: 24px; - min-height: 220px; + min-height: 218px; + display: flex; flex-direction: column; gap: 10px; + overflow: hidden; + border: var(--sketch); + background: + linear-gradient(135deg, rgba(217,122,61,0.2), rgba(0,0,0,0.4)), + rgba(10,22,15,0.4); +} +.store-featured .featured-card h3 { font-size: 1.6rem; margin: 0; } +.store-featured .featured-card p { font-size: 0.93rem; } +.store-featured .featured-card .glyph { + position: absolute; + right: -22px; bottom: -22px; + width: 172px; height: 172px; + opacity: 0.14; + pointer-events: none; +} + +.store-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(216px, 100%), 1fr)); + gap: 16px; + margin-top: 20px; +} +.store-item { + position: relative; + padding: 16px; display: flex; flex-direction: column; gap: 10px; + border: var(--sketch); + background: rgba(0,0,0,0.2); +} +.store-item .thumb { + height: 124px; + border-radius: var(--r-sm); + border: var(--rule); background: - linear-gradient(135deg, rgba(217,122,61,0.28), rgba(0,0,0,0.45)), - radial-gradient(ellipse at top right, rgba(244,241,230,0.08), transparent 60%); + repeating-linear-gradient(-45deg, rgba(244,241,230,0.03) 0 9px, transparent 9px 18px), + linear-gradient(160deg, rgba(217,122,61,0.14), rgba(0,0,0,0.3)); + display: flex; align-items: center; justify-content: center; overflow: hidden; } -.store-featured .featured-card .glyph { - position: absolute; - right: -20px; bottom: -20px; - width: 180px; height: 180px; - opacity: .18; - pointer-events: none; -} -.store-featured .featured-card h3 { - font-family: 'Caveat', cursive; font-size: 1.9rem; margin: 0; -} -.store-featured .featured-card .badge { - display: inline-block; - background: var(--accent); - color: var(--board-3); - font-family: 'Caveat', cursive; - font-weight: 700; - font-size: .9rem; - padding: 2px 10px; - border-radius: 6px; - border: 2px solid var(--chalk); - width: max-content; +.store-item .thumb svg { width: 74%; height: 74%; } +.store-item h3 { margin: 0; font-size: 1.35rem; } +.store-item p { margin: 0; font-size: 0.88rem; color: var(--chalk-dim); min-height: 2.4em; } +.store-item .row-bottom { + display: flex; align-items: center; justify-content: space-between; + gap: 8px; margin-top: auto; } -/* ============================================================ + +/* ========================================================= MEMBERSHIP - ============================================================ */ + ========================================================= */ + .tier-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 20px; - margin-top: 18px; + grid-template-columns: repeat(auto-fit, minmax(min(262px, 100%), 1fr)); + gap: 18px; + margin-top: 22px; align-items: stretch; } .tier-card { position: relative; - padding: 28px 26px; - display: flex; flex-direction: column; gap: 14px; + padding: 28px 24px; + display: flex; flex-direction: column; gap: 12px; overflow: hidden; } -.tier-card .tier-name { - font-family: 'Caveat', cursive; - font-size: 2.6rem; - margin: 0; - line-height: 1; -} -.tier-card .tier-tagline { - color: var(--chalk-faded); - font-size: 1.05rem; - margin: 0; -} +.tier-card .tier-name { font-size: 2.3rem; margin: 0; line-height: 1; } +.tier-card .tier-tagline { color: var(--chalk-dim); font-size: 0.94rem; margin: 0; } .tier-card .tier-price { - font-family: 'Caveat', cursive; - font-size: 2.2rem; + font-family: var(--font-display); + font-size: 2rem; color: var(--chalk); - margin: 6px 0 2px; + margin: 4px 0 0; line-height: 1; } .tier-card .tier-price small { display: block; - font-size: 1rem; - color: var(--chalk-faded); - margin-top: 4px; - font-family: 'Architects Daughter', cursive; + margin-top: 6px; + font-family: var(--font-stamp); + font-size: 0.64rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--chalk-dim); } .tier-card ul { list-style: none; - padding: 0; margin: 6px 0 0; - display: flex; flex-direction: column; gap: 8px; + padding: 0; + margin: 8px 0 0; + display: flex; flex-direction: column; gap: 9px; + border-top: var(--rule); + padding-top: 16px; } .tier-card ul li { position: relative; - padding-left: 28px; + padding-left: 26px; + font-size: 0.93rem; color: var(--chalk-soft); - font-size: 1rem; } .tier-card ul li::before { content: ""; - position: absolute; left: 0; top: 4px; - width: 18px; height: 18px; - background: var(--accent); - border-radius: 50%; - border: 2px dashed var(--chalk); - box-sizing: border-box; -} -.tier-card ul li::after { - content: "✓"; - position: absolute; left: 3px; top: 1px; - font-family: 'Caveat', cursive; - color: var(--board-3); - font-weight: 700; - font-size: 14px; - line-height: 1.2; + position: absolute; left: 0; top: 7px; + width: 11px; height: 6px; + border-left: 2px solid var(--accent); + border-bottom: 2px solid var(--accent); + transform: rotate(-45deg); +} +.tier-card ul li.muted-perk { color: var(--chalk-dim); opacity: 0.6; } +.tier-card ul li.muted-perk::before { + border: none; + width: 10px; height: 2px; + top: 11px; + background: var(--chalk-dim); + transform: none; } -.tier-card ul li.muted-perk { color: var(--chalk-faded); opacity: .7; } -.tier-card ul li.muted-perk::before { background: transparent; border-style: solid; } -.tier-card ul li.muted-perk::after { content: ""; } - -.tier-card .tier-cta { margin-top: auto; } +.tier-card .tier-cta { margin-top: auto; padding-top: 6px; } +.tier-card .tier-cta .btn { width: 100%; } .tier-card.featured { - border: none; + border-color: rgba(217,122,61,0.6); background: - linear-gradient(160deg, rgba(217,122,61,0.30), rgba(0,0,0,0.40)), - radial-gradient(ellipse at top, rgba(244,241,230,0.10), transparent 60%); - transform: translateY(-6px); + linear-gradient(165deg, rgba(217,122,61,0.2), rgba(0,0,0,0.32)), + rgba(10,22,15,0.4); + box-shadow: var(--lift-2), 0 0 40px -18px rgba(217,122,61,0.7); } -.tier-card.featured::before { border-color: var(--accent); } .tier-card .ribbon { position: absolute; - top: 14px; right: -38px; + top: 15px; right: -36px; + padding: 3px 42px; + transform: rotate(38deg); background: var(--accent); - color: var(--board-3); - font-family: 'Caveat', cursive; - font-weight: 700; - padding: 4px 44px; - transform: rotate(35deg); - border-top: 2px solid var(--chalk); - border-bottom: 2px solid var(--chalk); - font-size: 1rem; - box-shadow: 0 4px 10px rgba(0,0,0,0.5); - letter-spacing: 0.5px; + color: var(--board-ink); + font-family: var(--font-stamp); + font-size: 0.6rem; + letter-spacing: 0.12em; + text-transform: uppercase; + box-shadow: 0 4px 12px rgba(0,0,0,0.55); +} +.tier-card .tier-name .badge-inline { + display: inline-block; + vertical-align: middle; + margin-left: 8px; + font-family: var(--font-stamp); + font-size: 0.6rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--chalk-dim); + border: var(--rule-strong); + border-radius: 999px; + padding: 2px 9px; +} +.btn.current-tier { + background: rgba(217,122,61,0.14); + border: 1px solid var(--accent); + color: var(--accent-soft); + box-shadow: none; + cursor: default; + pointer-events: none; } .faq-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(min(278px, 100%), 1fr)); gap: 16px; - margin-top: 18px; -} -.faq-grid .chalk-card h3 { - font-family: 'Caveat', cursive; - font-size: 1.5rem; - margin: 0 0 6px; - color: var(--accent-soft); + margin-top: 20px; } +.faq-grid .chalk-card h3 { font-size: 1.3rem; margin: 0 0 6px; color: var(--accent-soft); } +.faq-grid p { margin: 0; font-size: 0.93rem; } -/* ============================================================ + +/* ========================================================= AVATAR CUSTOMIZER - ============================================================ */ + ========================================================= */ + .avatar-layout { display: grid; - grid-template-columns: 300px 1fr 300px; + grid-template-columns: 288px 1fr 288px; gap: 18px; - margin-top: 18px; + margin-top: 22px; align-items: start; } .avatar-col { padding: 18px; } -.avatar-col h3 { - font-family: 'Caveat', cursive; - font-size: 1.6rem; - margin: 0 0 10px; -} - -.country-search { - width: 100%; - font-family: 'Architects Daughter', cursive; - color: var(--chalk); - background: rgba(0,0,0,0.30); - border: 2px dashed rgba(244,241,230,0.45); - border-radius: 10px; - padding: 8px 12px; - margin-bottom: 10px; - font-size: 1rem; -} -.country-search:focus { - outline: none; border-color: var(--accent); border-style: solid; - box-shadow: 0 0 0 3px rgba(217,122,61,0.25); -} -.country-tabs { - display: flex; gap: 8px; margin-bottom: 10px; -} -.country-tabs button { - flex: 1; - background: transparent; - border: 2px dashed rgba(244,241,230,0.40); - color: var(--chalk-soft); - font-family: 'Caveat', cursive; - font-size: 1.15rem; - border-radius: 8px; - padding: 4px 8px; - cursor: pointer; -} -.country-tabs button.active { - border-color: var(--accent); - border-style: solid; - color: var(--chalk); - background: rgba(217,122,61,0.18); -} +.avatar-col h3 { font-size: 1.4rem; margin: 0 0 12px; } +.country-search { margin-bottom: 10px; } +.country-tabs { margin-bottom: 12px; } .country-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; - max-height: 560px; + max-height: 540px; overflow-y: auto; - padding: 4px 8px 4px 2px; + padding: 2px 6px 2px 2px; } .country-chip { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 10px 6px 8px; - border-radius: 12px; - border: 2px dashed transparent; - background: rgba(0,0,0,0.22); + min-height: 94px; + border: 1px solid transparent; + border-radius: var(--r-sm); + background: rgba(0,0,0,0.24); cursor: pointer; - transition: transform .15s ease, border-color .15s ease, background .15s ease; - min-height: 96px; + transition: transform .15s var(--ease), border-color .15s, background .15s; } .country-chip:hover { - background: rgba(244,241,230,0.08); + background: rgba(244,241,230,0.07); + border-color: rgba(244,241,230,0.25); transform: translateY(-2px); - border-color: rgba(244,241,230,0.35); } .country-chip.active { border-color: var(--accent); - border-style: solid; - background: rgba(217,122,61,0.20); - box-shadow: 0 0 0 2px rgba(217,122,61,0.30), 0 6px 14px rgba(0,0,0,0.4); + background: rgba(217,122,61,0.18); + box-shadow: 0 0 0 1px var(--accent); } .country-chip.locked { - opacity: 0.55; + border: var(--sketch); + opacity: 0.5; cursor: not-allowed; - filter: grayscale(0.5); -} -.country-chip.locked:hover { - transform: none; - background: rgba(0,0,0,0.22); - border-color: transparent; -} -.country-chip.locked svg { - filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)) grayscale(0.5); -} -.country-chip.locked .name { - color: var(--chalk-faded); -} -.country-chip svg { - flex: 0 0 auto; - filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); + filter: grayscale(0.6); } +.country-chip.locked:hover { transform: none; background: rgba(0,0,0,0.24); } +.country-chip svg { flex: 0 0 auto; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.55)); } .country-chip .name { - font-family: 'Architects Daughter', 'Kalam', cursive; - font-size: 0.92rem; - font-weight: 700; - color: #ffffff; + font-family: var(--font-body); + font-size: 0.8rem; + font-weight: 600; + color: #fff; text-align: center; - line-height: 1.15; - letter-spacing: 0.2px; - /* keep names crisp regardless of background — chalkboard-flavored stroke */ - text-shadow: - 0 0 1px rgba(0,0,0,0.95), - 0 1px 2px rgba(0,0,0,0.85), - 0 0 6px rgba(0,0,0,0.55); - word-break: break-word; - hyphens: auto; + line-height: 1.2; + text-shadow: 0 1px 3px rgba(0,0,0,0.95); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; max-width: 100%; + word-break: break-word; } -.country-chip.active .name { color: #fff8e0; } -.country-grid::-webkit-scrollbar { width: 8px; } -.country-grid::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 8px; } -.country-grid::-webkit-scrollbar-thumb { - background: rgba(244,241,230,0.25); border-radius: 8px; -} +.country-grid::-webkit-scrollbar { width: 6px; } +.country-grid::-webkit-scrollbar-thumb { background: rgba(244,241,230,0.2); border-radius: 6px; } -/* preview pane */ -.avatar-preview { - display: flex; flex-direction: column; align-items: center; gap: 14px; - text-align: center; -} -.avatar-preview .country-name { - font-family: 'Caveat', cursive; - font-size: 2.2rem; - margin: 0; -} -.avatar-stage { - width: min(360px, 75vw); - aspect-ratio: 1 / 1; - position: relative; -} +.avatar-preview { display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center; } +.avatar-preview .country-name { font-size: 2rem; margin: 0; } +.avatar-stage { width: min(340px, 74vw); aspect-ratio: 1; position: relative; } .avatar-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; } -.avatar-saved-pulse { animation: saved-pulse .8s ease; } -@keyframes saved-pulse { - 0% { transform: scale(1); } - 35% { transform: scale(1.05) rotate(-3deg); } - 100% { transform: scale(1); } -} -/* accessory tile grid */ .acc-section { margin-bottom: 16px; } .acc-section h4 { - font-family: 'Caveat', cursive; - font-size: 1.25rem; - margin: 0 0 6px; - color: var(--accent-soft); -} -.acc-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 8px; -} + font-family: var(--font-stamp); + font-size: 0.66rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--chalk-dim); + margin: 0 0 7px; +} +.acc-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .acc-tile { - aspect-ratio: 1 / 1; - border: 2px dashed rgba(244,241,230,0.40); - border-radius: 10px; - background: rgba(0,0,0,0.25); + position: relative; + aspect-ratio: 1; + border: var(--rule); + border-radius: var(--r-sm); + background: rgba(0,0,0,0.26); display: flex; align-items: center; justify-content: center; cursor: pointer; - transition: transform .15s ease, border-color .15s ease, background .15s ease; - position: relative; -} -.acc-tile:hover { transform: translateY(-2px); background: rgba(244,241,230,0.06); } -.acc-tile.active { - border-color: var(--accent); - background: rgba(217,122,61,0.18); + transition: transform .15s var(--ease), border-color .15s, background .15s; } +.acc-tile:hover { transform: translateY(-2px); background: rgba(244,241,230,0.07); } +.acc-tile.active { border-color: var(--accent); background: rgba(217,122,61,0.18); } .acc-tile.locked { - opacity: 0.5; + border: var(--sketch); + opacity: 0.45; cursor: not-allowed; - filter: grayscale(0.5); - position: relative; -} -.acc-tile.locked:hover { - transform: none; - background: rgba(0,0,0,0.25); - border-color: rgba(244,241,230,0.40); + filter: grayscale(0.6); } +.acc-tile.locked:hover { transform: none; background: rgba(0,0,0,0.26); } .acc-tile .acc-lock { - position: absolute; - right: 4px; top: 4px; - font-size: 0.75rem; - line-height: 1; - filter: none; -} -.acc-tile svg { width: 60%; height: 60%; } -.acc-tile .none-x { - font-family: 'Caveat', cursive; - font-size: 1.4rem; - color: var(--chalk-faded); -} -.acc-tile.active .none-x { color: var(--chalk); } - -/* mini countryball in user-chip */ -.user-chip .avatar.countryball { - background: none; - border: none; - padding: 0; - width: 30px; height: 30px; - overflow: visible; -} -.user-chip .avatar.countryball svg { - width: 100%; height: 100%; - display: block; -} - -@media (max-width: 980px) { - .avatar-layout { - grid-template-columns: 1fr; - } - .avatar-stage { width: min(280px, 70vw); } - .country-grid { - grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); - max-height: 360px; - } -} - -/* ============================================================ - TOPBAR — split into left (brand + nav) and right (account) - ============================================================ */ -.topbar .inner { - display: flex; - align-items: center; - /* hard split: left cluster pushes right, right cluster pinned to far right */ - justify-content: space-between; - gap: 24px; -} -.topbar .topbar-left, -.topbar .topbar-right { - display: flex; - align-items: center; - gap: 12px; - flex-wrap: nowrap; -} -.topbar .topbar-left { flex: 1 1 auto; min-width: 0; } -.topbar .topbar-right { flex: 0 0 auto; } - -/* visual divider chalk dash between the halves on wide screens */ -.topbar .topbar-right::before { - content: ""; - display: inline-block; - width: 2px; height: 28px; - border-left: 2px dashed rgba(244,241,230,0.25); - margin-right: 4px; -} - -/* TIER BADGE — sits inside user-chip, replaces direct membership link */ -.tier-badge { - display: inline-flex; - align-items: center; - gap: 4px; - font-family: 'Caveat', cursive; - font-weight: 700; - font-size: 0.95rem; - padding: 2px 10px; - border-radius: 999px; - border: 1.5px dashed var(--tier-guest-bd); - background: var(--tier-guest-bg); - color: var(--tier-guest-fg); - text-decoration: none; - letter-spacing: 0.3px; - transition: transform .15s ease, box-shadow .15s ease, background .15s ease; - line-height: 1.1; - cursor: pointer; -} -.tier-badge:hover { transform: translateY(-1px); } -.tier-badge[data-tier="regular"] { - border-color: var(--tier-regular-bd); - background: var(--tier-regular-bg); - color: var(--tier-regular-fg); -} -.tier-badge[data-tier="emperor"] { - border-color: var(--tier-emperor-bd); - border-style: solid; - background: var(--tier-emperor-bg); - color: var(--tier-emperor-fg); - text-shadow: 0 1px 2px rgba(0,0,0,0.6); - box-shadow: 0 0 0 1px rgba(244,201,93,0.45), 0 0 16px rgba(255,210,80,0.30); -} -.tier-badge[data-tier="emperor"]:hover { - box-shadow: 0 0 0 1px rgba(244,201,93,0.6), 0 0 22px rgba(255,210,80,0.5); -} -.tier-badge .crown { - display: inline-block; + position: absolute; right: 4px; top: 4px; width: 12px; height: 12px; - background: currentColor; - -webkit-mask: url("data:image/svg+xml;utf8,") center/contain no-repeat; - mask: url("data:image/svg+xml;utf8,") center/contain no-repeat; -} - -/* Username inside user-chip — make name + tier badge sit nicely */ -.user-chip { - gap: 8px; -} -.user-chip .name { line-height: 1.1; } -.user-chip a.tier-badge { border-bottom: none; } - -/* The username chip itself becomes a wrapper for name+tier on the right side */ -.user-block { - display: inline-flex; - flex-direction: column; - align-items: flex-start; - gap: 2px; - line-height: 1.1; -} -.user-block .name { - font-family: 'Caveat', cursive; - font-size: 1.15rem; - color: var(--chalk); -} - -/* Settings icon button */ -.btn-icon { - width: 38px; height: 38px; - display: inline-flex; align-items: center; justify-content: center; - border: 2px dashed rgba(244,241,230,0.55); - border-radius: 50%; - background: transparent; - color: var(--chalk); - cursor: pointer; - transition: transform .18s ease, background .18s ease, border-color .18s ease; - padding: 0; -} -.btn-icon:hover { - background: rgba(244,241,230,0.10); - transform: rotate(35deg); - border-color: var(--accent); -} -.btn-icon svg { width: 18px; height: 18px; } -.btn-icon:focus-visible { outline: 2px dashed var(--accent); outline-offset: 3px; } - -/* ============================================================ - SETTINGS PAGE - ============================================================ */ -.settings-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 18px; - margin-top: 18px; -} -.settings-card h3 { - font-family: 'Caveat', cursive; - font-size: 1.6rem; - margin: 0 0 4px; -} -.settings-row { - display: flex; align-items: center; gap: 12px; - padding: 10px 0; - border-top: 1px dashed rgba(244,241,230,0.20); -} -.settings-row:first-of-type { border-top: none; } -.settings-row .label { - flex: 1; min-width: 0; -} -.settings-row .label .key { - display: block; - font-family: 'Patrick Hand', cursive; - font-size: 1.1rem; - color: var(--chalk); -} -.settings-row .label .desc { - display: block; - font-size: 0.92rem; - color: var(--chalk-faded); -} -.settings-row select, -.settings-row input[type="text"] { - font-family: 'Architects Daughter', cursive; - color: var(--chalk); - background: rgba(0,0,0,0.30); - border: 2px dashed rgba(244,241,230,0.45); - border-radius: 8px; - padding: 6px 10px; - font-size: 1rem; - min-width: 140px; -} -.settings-row select:focus, -.settings-row input:focus { - outline: none; border-color: var(--accent); border-style: solid; -} - -/* tier switcher chips on settings */ -.tier-switch { - display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; -} -.tier-switch button { - font-family: 'Caveat', cursive; - font-weight: 700; - font-size: 1.1rem; - padding: 6px 14px; - border-radius: 999px; - border: 2px dashed rgba(244,241,230,0.40); - background: rgba(0,0,0,0.25); - color: var(--chalk-soft); - cursor: pointer; - transition: all .15s ease; -} -.tier-switch button[data-tier="guest"].active { background: var(--tier-guest-bg); color: var(--tier-guest-fg); border-color: var(--tier-guest-bd); border-style: solid; } -.tier-switch button[data-tier="regular"].active { background: var(--tier-regular-bg); color: var(--tier-regular-fg); border-color: var(--tier-regular-bd); border-style: solid; } -.tier-switch button[data-tier="emperor"].active { background: var(--tier-emperor-bg); color: var(--tier-emperor-fg); border-color: var(--tier-emperor-bd); border-style: solid; box-shadow: 0 0 12px rgba(255,210,80,0.3); } - -/* Toggle switch */ -.toggle { - position: relative; width: 50px; height: 26px; - display: inline-block; - flex: 0 0 auto; -} -.toggle input { opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; } -.toggle .track { - position: absolute; inset: 0; - background: rgba(0,0,0,0.45); - border: 2px dashed rgba(244,241,230,0.40); - border-radius: 999px; - transition: background .2s, border-color .2s; -} -.toggle .thumb { - position: absolute; - top: 2px; left: 2px; - width: 18px; height: 18px; - background: var(--chalk); - border-radius: 50%; - transition: transform .25s cubic-bezier(.2,.7,.2,1), background .2s; + color: var(--chalk-dim); + filter: none; } -.toggle input:checked + .track { background: rgba(217,122,61,0.35); border-color: var(--accent); border-style: solid; } -.toggle input:checked + .track + .thumb { transform: translateX(24px); background: var(--accent-soft); } -.toggle input:focus-visible + .track { outline: 2px dashed var(--accent); outline-offset: 3px; } +.acc-tile .acc-lock svg { width: 12px; height: 12px; display: block; } +.acc-tile svg { width: 58%; height: 58%; } +.acc-tile .none-x { font-family: var(--font-stamp); font-size: 0.7rem; color: var(--chalk-dim); } -/* ============================================================ - COMING-SOON AVATARS GRID - ============================================================ */ .coming-soon-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 12px; + grid-template-columns: repeat(auto-fill, minmax(min(132px, 100%), 1fr)); + gap: 10px; margin-top: 14px; } .coming-soon-tile { - aspect-ratio: 1 / 1.15; - border: 2px dashed rgba(244,241,230,0.30); - border-radius: 12px; - background: - repeating-linear-gradient(45deg, rgba(244,241,230,0.04) 0 8px, transparent 8px 16px), - rgba(0,0,0,0.25); + aspect-ratio: 1 / 1.12; + border-radius: var(--r-sm); display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 8px; + gap: 7px; text-align: center; padding: 10px 6px; - position: relative; - overflow: hidden; } -.coming-soon-tile .silhouette { - width: 56px; height: 56px; - border-radius: 50%; - background: rgba(244,241,230,0.15); - border: 2px dashed rgba(244,241,230,0.45); - display: flex; align-items: center; justify-content: center; - font-family: 'Caveat', cursive; - font-size: 1.8rem; - color: rgba(244,241,230,0.5); +.coming-soon-tile .silhouette { + width: 50px; height: 50px; + border-radius: 50%; + border: var(--sketch); + background: rgba(244,241,230,0.05); + display: flex; align-items: center; justify-content: center; + font-family: var(--font-display); + font-size: 1.5rem; + color: var(--chalk-ghost); +} +.coming-soon-tile .cs-label { font-size: 0.82rem; color: var(--chalk-soft); line-height: 1.2; } +.coming-soon-tile .cs-eta { font-size: 0.6rem; color: var(--accent-soft); } + +@media (max-width: 1000px) { + .avatar-layout { grid-template-columns: 1fr; } + .avatar-stage { width: min(270px, 68vw); } + .country-grid { grid-template-columns: repeat(auto-fill, minmax(min(108px, 100%), 1fr)); max-height: 340px; } +} + + +/* ========================================================= + SETTINGS + ========================================================= */ + +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(288px, 100%), 1fr)); + gap: 18px; + margin-top: 22px; +} +.settings-card h3 { font-size: 1.45rem; margin: 0 0 4px; } +.settings-row { + display: flex; align-items: center; gap: 14px; + padding: 13px 0; + border-top: var(--rule); } -.coming-soon-tile .cs-label { - font-family: 'Architects Daughter', cursive; +.settings-row:first-of-type { border-top: none; } +.settings-row .label { flex: 1; min-width: 0; } +.settings-row .label .key { display: block; font-size: 0.97rem; color: var(--chalk); font-weight: 600; } +.settings-row .label .desc { display: block; font-size: 0.85rem; color: var(--chalk-dim); } +.settings-row select, .settings-row input[type="text"] { + width: auto; + min-width: 150px; + padding: 8px 12px; font-size: 0.9rem; - color: var(--chalk-soft); - line-height: 1.15; } -.coming-soon-tile .cs-eta { - font-family: 'Caveat', cursive; - font-size: 0.95rem; + + +/* ========================================================= + TERMS + ========================================================= */ + +.tos-doc { max-width: 840px; margin: 26px auto 0; padding: 34px clamp(22px, 4vw, 44px); } +.tos-doc h1 { font-size: clamp(2.1rem, 4vw, 3rem); } +.tos-doc h2 { + margin-top: 34px; + font-size: 1.7rem; + padding-top: 14px; + border-top: var(--rule); +} +.tos-doc h3 { + margin-top: 20px; + font-family: var(--font-body); + font-weight: 700; + font-size: 1rem; + letter-spacing: 0.01em; color: var(--accent-soft); + text-shadow: none; } +.tos-doc p, .tos-doc li { max-width: 74ch; font-size: 0.96rem; } +.tos-doc ul { padding-left: 20px; } +.tos-doc ul li { margin-bottom: 6px; } +.tos-doc .placeholder { font-style: italic; color: var(--chalk-dim); } -/* Tier label INSIDE the tier card */ -.tier-card .tier-name .badge-inline { - display: inline-block; - vertical-align: middle; - font-family: 'Patrick Hand', cursive; - font-size: 0.85rem; - margin-left: 8px; - padding: 2px 10px; - border-radius: 999px; - border: 1.5px dashed currentColor; - color: var(--chalk-faded); - letter-spacing: 0.3px; -} +.tos-doc .tos-meta { + display: flex; flex-wrap: wrap; gap: 4px 22px; + margin: 0 0 8px; + font-family: var(--font-stamp); + font-size: 0.7rem; + letter-spacing: 0.04em; + color: var(--chalk-dim); +} + +.tos-toc { margin-top: 22px; padding: 18px 22px; } +.tos-toc h2 { margin: 0 0 10px; font-size: 1.35rem; border-top: none; padding-top: 0; } +.tos-toc ol { columns: 2; column-gap: 30px; padding-left: 20px; margin: 0; } +.tos-toc li { margin-bottom: 5px; break-inside: avoid; font-size: 0.92rem; } +.tos-toc a { border-bottom-color: transparent; } +.tos-toc a:hover { border-bottom-color: var(--accent); } +@media (max-width: 620px) { .tos-toc ol { columns: 1; } } -/* Responsive nav: hide secondary nav text in cramped widths */ -@media (max-width: 880px) { - .topbar .topbar-right::before { display: none; } - .user-block .name { font-size: 1.05rem; } - .tier-badge { font-size: 0.85rem; padding: 1px 8px; } +.tos-doc .legal-note { + margin-top: 28px; + padding: 18px 22px; + border: 1px solid rgba(217,122,61,0.5); + border-left: 3px solid var(--accent); + border-radius: var(--r-sm); + background: rgba(217,122,61,0.08); +} +.tos-doc .legal-note .legal-tag { + display: block; + font-family: var(--font-stamp); + font-size: 0.64rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent-soft); + margin-bottom: 6px; } -/* ============================================================ - CHAT PAGE - ============================================================ */ + +/* ========================================================= + CHAT / FRIENDS / SEARCH + ========================================================= */ + .chat-layout { display: grid; - grid-template-columns: 260px 1fr; + grid-template-columns: 250px 1fr; gap: 18px; - margin-top: 18px; + margin-top: 22px; align-items: start; } -.chat-sidebar { - padding: 16px 14px; - position: sticky; - top: 84px; -} -.chat-room-list { - list-style: none; - padding: 0; margin: 0 0 8px; - display: flex; flex-direction: column; gap: 4px; -} +.chat-sidebar { padding: 15px 13px; position: sticky; top: 78px; } +.chat-room-list { list-style: none; padding: 0; margin: 0 0 8px; display: flex; flex-direction: column; gap: 3px; } .chat-room-btn { width: 100%; - display: flex; align-items: center; gap: 8px; + display: flex; align-items: center; gap: 9px; text-align: left; + padding: 8px 11px; + border: 1px solid transparent; + border-radius: var(--r-sm); background: transparent; - border: 2px dashed transparent; - color: var(--chalk-soft); - font-family: 'Architects Daughter', cursive; - font-size: 1rem; - padding: 6px 10px; - border-radius: 8px; + color: var(--chalk-dim); + font-size: 0.92rem; cursor: pointer; - transition: background .15s, border-color .15s, color .15s; -} -.chat-room-btn:hover { - background: rgba(244,241,230,0.06); - color: var(--chalk); + transition: background .15s, color .15s, border-color .15s; } +.chat-room-btn:hover { background: rgba(244,241,230,0.07); color: var(--chalk); } .chat-room-btn.active { - background: rgba(217,122,61,0.18); + background: rgba(217,122,61,0.16); border-color: var(--accent); - border-style: solid; color: var(--chalk); } .chat-room-btn .dot { - width: 8px; height: 8px; border-radius: 50%; - background: var(--chalk-faded); + width: 7px; height: 7px; border-radius: 50%; + background: var(--chalk-dim); flex: 0 0 auto; } -.chat-room-btn.active .dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); } -.chat-new-dm { - display: flex; gap: 6px; margin-top: 8px; -} -.chat-new-dm input { - flex: 1; min-width: 0; - font-family: 'Architects Daughter', cursive; - background: rgba(0,0,0,0.30); - color: var(--chalk); - border: 2px dashed rgba(244,241,230,0.45); - border-radius: 8px; - padding: 6px 10px; - font-size: .95rem; -} -.chat-new-dm input:focus { outline: none; border-color: var(--accent); border-style: solid; } -.chat-new-dm button { padding: 4px 12px; font-size: 1.2rem; } +.chat-room-btn.active .dot { background: var(--accent); box-shadow: 0 0 7px var(--accent); } +.chat-new-dm { display: flex; gap: 6px; margin-top: 10px; } +.chat-new-dm input { flex: 1; min-width: 0; padding: 8px 11px; font-size: 0.88rem; } +.chat-new-dm button { padding: 6px 13px; } .chat-main { display: flex; flex-direction: column; - height: calc(100vh - 140px); - min-height: 520px; - padding: 14px; + height: calc(100vh - 150px); + min-height: 500px; + padding: 15px; } .chat-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; - border-bottom: 2px dashed rgba(244,241,230,0.25); - padding-bottom: 10px; margin-bottom: 10px; + padding-bottom: 11px; margin-bottom: 10px; + border-bottom: var(--rule); } -.chat-header .chat-me { font-size: .95rem; } +.chat-header .chat-me { font-size: 0.85rem; color: var(--chalk-dim); } .chat-messages { flex: 1; min-height: 0; overflow-y: auto; padding: 6px 4px; display: flex; flex-direction: column; gap: 10px; } -.chat-messages::-webkit-scrollbar { width: 8px; } -.chat-messages::-webkit-scrollbar-thumb { background: rgba(244,241,230,0.25); border-radius: 8px; } -.chat-msg { - display: flex; flex-direction: column; - max-width: 78%; - align-self: flex-start; -} +.chat-messages::-webkit-scrollbar { width: 6px; } +.chat-messages::-webkit-scrollbar-thumb { background: rgba(244,241,230,0.2); border-radius: 6px; } +.chat-msg { display: flex; flex-direction: column; max-width: 76%; align-self: flex-start; } .chat-msg.me { align-self: flex-end; align-items: flex-end; } .chat-from { - font-family: 'Caveat', cursive; - font-size: .95rem; + font-family: var(--font-stamp); + font-size: 0.64rem; + letter-spacing: 0.08em; + text-transform: uppercase; color: var(--accent-soft); - padding: 0 6px 2px; + padding: 0 4px 3px; } .chat-bubble { - background: rgba(244,241,230,0.10); - border: 2px dashed rgba(244,241,230,0.30); - border-radius: 12px; - padding: 8px 12px; - font-family: 'Architects Daughter', cursive; + padding: 9px 13px; + border: var(--rule); + border-radius: var(--r); + background: rgba(244,241,230,0.07); color: var(--chalk); + font-size: 0.95rem; line-height: 1.45; white-space: pre-wrap; word-wrap: break-word; } .chat-msg.me .chat-bubble { - background: rgba(217,122,61,0.22); - border-color: var(--accent); - border-style: solid; -} -.chat-time { - font-size: .75rem; - color: var(--chalk-faded); - padding: 2px 6px 0; + background: rgba(217,122,61,0.2); + border-color: rgba(217,122,61,0.55); } +.chat-time { font-size: 0.68rem; color: var(--chalk-dim); padding: 3px 5px 0; } .chat-composer { display: flex; gap: 8px; - padding-top: 10px; - border-top: 2px dashed rgba(244,241,230,0.25); - margin-top: 8px; -} -.chat-composer input { - flex: 1; min-width: 0; - font-family: 'Architects Daughter', cursive; - font-size: 1.05rem; - background: rgba(0,0,0,0.30); - color: var(--chalk); - border: 2px dashed rgba(244,241,230,0.45); - border-radius: 10px; - padding: 10px 14px; + padding-top: 11px; margin-top: 8px; + border-top: var(--rule); } -.chat-composer input:focus { outline: none; border-color: var(--accent); border-style: solid; } -.chat-composer input:disabled { opacity: 0.55; cursor: not-allowed; } +.chat-composer input { flex: 1; min-width: 0; } -@media (max-width: 760px) { +/* Friend / search result rows */ +.fr-panel { padding: 16px 18px; } +.fr-panel + .fr-panel { margin-top: 14px; } +.rname { font-weight: 600; color: var(--chalk); } + +@media (max-width: 800px) { .chat-layout { grid-template-columns: 1fr; } .chat-sidebar { position: static; } .chat-main { height: auto; min-height: 420px; } } -/* ============================================================ - SOCIAL POPOVER — body-attached chalkboard panel - Toggle lives in the topbar nav; the menu itself is appended to - by JS (position: fixed) so it escapes the topbar's flex - cascade and can never collide with surrounding link styles. - ============================================================ */ -/* Toggle button inside the nav — styled to match sibling links */ -.topbar nav .social-toggle { - background: transparent; - border: none; - color: var(--chalk-soft); - font-family: 'Architects Daughter', cursive; - font-size: 1.05rem; - padding: 6px 12px; - border-radius: 8px; - cursor: pointer; - letter-spacing: 0.2px; - line-height: 1.4; - display: inline-flex; - align-items: center; - gap: 6px; - position: relative; - transition: background .15s ease, color .15s ease; +/* ========================================================= + OVERLAYS + ========================================================= */ + +.loading-overlay { + position: fixed; inset: 0; + z-index: var(--z-overlay); + background: rgba(7,17,11,0.95); + backdrop-filter: blur(6px); + display: none; + align-items: center; justify-content: center; + flex-direction: column; gap: 22px; + text-align: center; } -.topbar nav .social-toggle:hover, -.topbar nav .social-toggle[aria-expanded="true"], -.topbar nav .social-toggle.has-active { - background: rgba(244,241,230,0.10); - color: var(--chalk); +.loading-overlay.show { display: flex; } +.loading-overlay .spinner { + width: 58px; height: 58px; + border: 3px solid rgba(244,241,230,0.16); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 1s linear infinite; } -.topbar nav .social-toggle .caret { - width: 10px; height: 10px; - flex: 0 0 10px; - transition: transform .25s cubic-bezier(.2,.7,.2,1); -} -.topbar nav .social-toggle[aria-expanded="true"] .caret { - transform: rotate(180deg); -} -/* Hide the chalk-underline ::after that .topbar nav a uses */ -.topbar nav .social-toggle::after, -.topbar nav .social-toggle::before { - content: none !important; - display: none !important; -} - -/* The popover panel — lives directly under */ -.social-popover { - position: fixed; - top: 0; left: 0; - min-width: 220px; - background: linear-gradient(180deg, rgba(15,32,24,0.985), rgba(10,22,15,0.985)); - border: 2.5px dashed rgba(244,241,230,0.55); - border-radius: 14px; - padding: 8px; - display: flex; - flex-direction: column; - gap: 2px; - z-index: 1000; - box-shadow: - 0 0 0 1px rgba(244,241,230,0.08), - 0 18px 38px rgba(0,0,0,0.65), - 0 0 24px rgba(217,122,61,0.10); - /* enter animation */ - opacity: 0; - transform: translateY(-6px) scale(0.96); - transform-origin: top center; - pointer-events: none; - transition: opacity .18s ease, transform .22s cubic-bezier(.2,.7,.2,1); +@keyframes spin { to { transform: rotate(360deg); } } +.loading-overlay h2 { font-size: 2rem; margin: 0; } +.loading-overlay p { font-family: var(--font-stamp); font-size: 0.76rem; letter-spacing: 0.06em; } + +.lightbox { + position: fixed; inset: 0; + z-index: var(--z-overlay); + background: rgba(7,17,11,0.95); + backdrop-filter: blur(5px); + display: flex; align-items: center; justify-content: center; + cursor: zoom-out; + animation: fade-in .22s ease; } -.social-popover.open { - opacity: 1; - transform: translateY(0) scale(1); - pointer-events: auto; +.lightbox img { + max-width: 92vw; max-height: 88vh; + border: var(--rule-strong); + border-radius: var(--r); + box-shadow: var(--lift-3); } - -/* Little chalk-arrow notch pointing back at the toggle */ -.social-popover::before { - content: ""; - position: absolute; - top: -8px; - left: var(--arrow-x, 50%); - transform: translateX(-50%) rotate(45deg); - width: 12px; height: 12px; - background: rgba(15,32,24,0.99); - border-left: 2.5px dashed rgba(244,241,230,0.55); - border-top: 2.5px dashed rgba(244,241,230,0.55); - border-top-left-radius: 3px; +.lightbox .close { + position: absolute; top: 18px; right: 22px; + width: 42px; height: 42px; + display: inline-flex; align-items: center; justify-content: center; + border: var(--rule-strong); + border-radius: 50%; + background: rgba(0,0,0,0.4); + color: var(--chalk); + font-size: 1.2rem; + cursor: pointer; } +.lightbox .close:hover { background: var(--chalk); color: var(--board-ink); } +@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } -/* Items */ -.social-popover a { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 12px; - font-family: 'Architects Daughter', cursive; - font-size: 1.05rem; - color: var(--chalk-soft); - text-decoration: none; - border: 1.5px dashed transparent; +.toast { + position: fixed; left: 50%; bottom: 30px; + z-index: var(--z-toast); + transform: translateX(-50%) translateY(8px); + padding: 12px 20px; + border: var(--rule-strong); border-radius: 10px; - line-height: 1.2; - white-space: nowrap; - transition: background .15s ease, color .15s ease, border-color .15s ease, transform .15s ease; -} -.social-popover a:hover, -.social-popover a:focus-visible { - background: rgba(244,241,230,0.08); + background: rgba(7,17,11,0.96); color: var(--chalk); - border-color: rgba(244,241,230,0.30); - outline: none; - transform: translateX(2px); -} -.social-popover a.active { - color: var(--accent-soft); - background: rgba(217,122,61,0.16); - border-color: var(--accent); - border-style: solid; -} -.social-popover .sp-icon { - width: 20px; height: 20px; - flex: 0 0 20px; - display: inline-flex; - align-items: center; - justify-content: center; - color: currentColor; - opacity: 0.9; -} -.social-popover .sp-icon svg { width: 20px; height: 20px; display: block; } -.social-popover .sp-label { flex: 1; min-width: 0; } -.social-popover .sp-sub { - display: block; - font-size: .8rem; - color: var(--chalk-faded); - font-family: 'Architects Daughter', cursive; - margin-top: 1px; - line-height: 1.1; + font-size: 0.92rem; + box-shadow: var(--lift-2); + opacity: 0; + pointer-events: none; + transition: opacity .24s ease, transform .24s var(--ease); } +.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } -/* Reduced motion */ -@media (prefers-reduced-motion: reduce) { - .social-popover { transition: opacity .1s ease; transform: none; } - .social-popover.open { transform: none; } -} -/* ============================================================ - LOCK PILL — small inline 🔒 badge for section titles - ============================================================ */ -.lock-pill { - display: inline-flex; - align-items: center; - gap: 4px; - vertical-align: middle; - font-family: 'Caveat', cursive; - font-weight: 700; - font-size: 0.95rem; - padding: 2px 12px; - margin-left: 8px; - border-radius: 999px; - background: rgba(217,122,61,0.18); - border: 1.5px dashed var(--accent); - color: var(--accent-soft); - letter-spacing: 0.3px; - line-height: 1.3; - text-shadow: none; - white-space: nowrap; -} -/* ============================================================ - LOCK OVERLAY — placeholder paywall / coming-soon gate - Wrap any block in
+ add a child -
as a sibling AFTER the content. - ============================================================ */ -.lock-wrap { - position: relative; - /* keep underlying content visible but unreachable */ -} -.lock-wrap > .lock-target { - filter: blur(3px) saturate(0.85); - opacity: 0.55; - pointer-events: none; - user-select: none; - /* prevent keyboard focus reaching locked content */ -} -.lock-overlay { - position: absolute; - inset: 0; - z-index: 5; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - gap: 14px; - padding: 32px 24px; - background: - radial-gradient(ellipse at center, rgba(15,32,24,0.55), rgba(15,32,24,0.85)); - border-radius: var(--radius); - pointer-events: auto; - backdrop-filter: blur(2px); -} -.lock-overlay::before { - content: ""; - position: absolute; - inset: 18px; - border: 3px dashed rgba(244,241,230,0.55); - border-radius: calc(var(--radius) - 4px); - pointer-events: none; - animation: lock-pulse 3s ease-in-out infinite; -} -@keyframes lock-pulse { - 0%, 100% { opacity: 0.55; transform: rotate(-0.3deg); } - 50% { opacity: 0.85; transform: rotate(0.3deg); } -} +/* ========================================================= + MOTION + ========================================================= */ -.lock-overlay .lock-icon { - width: clamp(96px, 14vw, 160px); - height: auto; - filter: drop-shadow(0 8px 18px rgba(0,0,0,0.5)); -} -.lock-overlay .lock-icon .body { - fill: rgba(244,241,230,0.92); - stroke: var(--board-3); - stroke-width: 4; -} -.lock-overlay .lock-icon .shackle { - fill: none; - stroke: rgba(244,241,230,0.92); - stroke-width: 14; - stroke-linecap: round; - /* slight chalk wobble using dasharray */ - stroke-dasharray: 220; - stroke-dashoffset: 0; -} -.lock-overlay .lock-icon .keyhole { - fill: var(--board-3); -} -.lock-overlay .lock-icon .accent { - fill: var(--accent); - opacity: 0.85; -} -.lock-overlay h2 { - font-family: 'Caveat', cursive; - font-size: clamp(2rem, 4vw, 3rem); - margin: 0; - color: var(--chalk); - text-shadow: 0 2px 8px rgba(0,0,0,0.7); -} -.lock-overlay p { - font-family: 'Architects Daughter', cursive; - color: var(--chalk-soft); - font-size: 1.05rem; - max-width: 46ch; - margin: 0; -} -.lock-overlay .lock-actions { - display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; - margin-top: 6px; -} -.lock-overlay .lock-stamp { - position: absolute; - top: 14px; right: 14px; - background: var(--accent); - color: var(--board-3); - font-family: 'Caveat', cursive; - font-weight: 700; - padding: 4px 14px; - border-radius: 999px; - border: 2px solid var(--chalk); - font-size: 0.95rem; - transform: rotate(8deg); - letter-spacing: 0.4px; - box-shadow: 0 4px 12px rgba(0,0,0,0.5); +.reveal { + opacity: 0; + transform: translateY(16px); + transition: opacity .55s ease, transform .55s var(--ease); } +.reveal.in { opacity: 1; transform: none; } +.reveal.delay-1 { transition-delay: .06s; } +.reveal.delay-2 { transition-delay: .12s; } +.reveal.delay-3 { transition-delay: .18s; } +.reveal.delay-4 { transition-delay: .24s; } -/* Locked tier chip / button — universal lock affordance */ -.tier-switch button.locked, -.tier-card .btn.locked, -.tier-card .btn.emperor-locked { - position: relative; - cursor: not-allowed; - opacity: 0.6; - filter: grayscale(0.4); -} -.tier-switch button.locked::after, -.tier-card .btn.locked::after, -.tier-card .btn.emperor-locked::after { - content: "🔒"; - margin-left: 6px; - font-size: 0.9em; - filter: none; +/* ========================================================= + RESPONSIVE + ========================================================= */ + +@media (max-width: 1040px) { + .nav-tab { padding: 9px 10px; font-size: 0.9rem; } + .nav-tab svg { display: none; } +} + +@media (max-width: 900px) { + .topbar-inner, .topbar .inner { padding: 8px 16px; gap: 10px; } + .nav-toggle { display: block; order: 3; } + .brand { order: 1; } + .topbar-right { order: 2; margin-left: auto; } + + /* The drawer. It hangs off the sticky bar, so it travels with it, and it + is capped to the space actually below the bar rather than a flat 78vh — + a landscape phone has about 250px there, not 600. */ + .primary-nav { + order: 4; + position: absolute; + top: 100%; left: 0; right: 0; + flex-direction: column; + align-items: stretch; + gap: 2px; + padding: 10px 12px 16px; + background: linear-gradient(180deg, #0d1c13, #0a160f); + border-bottom: var(--rule-strong); + border-radius: 0 0 var(--r-lg) var(--r-lg); + box-shadow: var(--lift-3); + /* 100% here is the topbar's own height — .primary-nav is absolutely + positioned inside it — so this is "whatever is left below the bar". + dvh second, for the browsers that have it: on mobile Safari the URL bar + makes vh larger than what you can actually see. */ + max-height: calc(100vh - 100% - 8px); + max-height: calc(100dvh - 100% - 8px); + overflow-y: auto; + overscroll-behavior: contain; + /* Slide, don't squash. Animating max-height concertinas every row on the + way down; this moves the finished panel into place instead. */ + opacity: 0; + visibility: hidden; + transform: translateY(-12px); + transition: opacity .2s ease, transform .3s var(--ease), visibility .3s; + } + .topbar.menu-open .primary-nav { + opacity: 1; + visibility: visible; + transform: none; + } + + /* Everything behind the drawer goes quiet. Sits under --z-nav so the bar + and the drawer stay above it, and taps land on — which is what + nav.js already treats as "outside", so it closes. */ + body.nav-open::after { + content: ""; + position: fixed; + inset: 0; + z-index: 30; + background: rgba(4,10,7,0.6); + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); + animation: scrim-in .22s ease both; + } + + .nav-tab { + justify-content: flex-start; + padding: 13px 14px; + font-size: 1rem; + border-left: 3px solid transparent; + border-radius: 0 var(--r-sm) var(--r-sm) 0; + } + .nav-tab svg { display: inline-flex; width: 18px; height: 18px; } + /* The sliding dash is desktop-only, so the current page needs its own mark + down here — the same accent rule the rest of the site uses for "you are + on this one". */ + .nav-tab.active { + border-left-color: var(--accent); + background: rgba(217,122,61,0.1); + } + .nav-marker { display: none; } + + /* Dropdowns become inline accordions — but only inside the drawer. The + account menu lives in .topbar-right and must stay a real dropdown; + unscoped, this rule dropped it into the header row as a static block + and pushed the whole bar 300px tall. */ + .primary-nav .nav-group { display: block; } + .primary-nav .nav-group > .nav-tab { width: 100%; } + .primary-nav .nav-menu { + position: static; + transform: none; + min-width: 0; + margin: 2px 0 6px 17px; + box-shadow: none; + /* Transparent while collapsed. With max-height:0 the box is still 2px of + border, and a tinted background there drew a stray rule under Social + that looked like a divider nobody asked for. */ + background: transparent; + max-height: 0; + padding: 0 7px; + overflow: hidden; + visibility: visible; + opacity: 1; + border-color: transparent; + transition: max-height .3s var(--ease), padding .3s var(--ease), border-color .2s, background .2s; + } + .primary-nav .nav-group.open > .nav-menu { + transform: none; + max-height: 340px; + padding: 7px; + background: rgba(0,0,0,0.28); + border-color: rgba(244,241,230,0.16); + } + .primary-nav .nav-menu::before { display: none; } + + /* Account menu: still a dropdown, just sized for a thumb and kept inside + the screen on a narrow one. */ + .topbar-right .nav-menu { + min-width: 0; + width: max-content; + max-width: calc(100vw - 24px); + top: calc(100% + 12px); + } + .topbar-right .nav-menu a, + .topbar-right .nav-menu button { padding: 13px 12px; } + + .user-block .name { max-width: 88px; } + .coin-chip .soon { display: none; } + + /* Thumb-sized. 38x34 is a mouse target. */ + .nav-toggle { width: 44px; height: 44px; } + .nav-toggle span, .nav-toggle::before, .nav-toggle::after { left: 12px; right: 12px; } + .nav-toggle::before { top: 15px; } + .nav-toggle span { top: 21px; } + .nav-toggle::after { top: 27px; } + .topbar.menu-open .nav-toggle::before { transform: translateY(6px) rotate(45deg); } + .topbar.menu-open .nav-toggle::after { transform: translateY(-6px) rotate(-45deg); } + .user-chip { min-height: 40px; } +} + +@keyframes scrim-in { from { opacity: 0; } to { opacity: 1; } } + +@media (max-width: 620px) { + body { font-size: 16px; } + .brand-name { font-size: 1.3rem; } + .brand-mark, .topbar .brand img { width: 30px; height: 30px; } + .coin-chip { padding: 4px 9px; font-size: 0.72rem; } + .user-chip { padding: 3px 8px 3px 4px; } + .user-block .name { max-width: 70px; font-size: 0.8rem; } + .site-footer .footer-inner { flex-direction: column; align-items: flex-start; } + .site-footer .footer-meta { margin-left: 0; } + + /* Label and control side by side leaves the label about 100px, so "See what + each membership unlocks" ran to three lines against a 150px select. + Stack them and both get the full width. */ + .settings-row { flex-direction: column; align-items: stretch; gap: 10px; } + .settings-row select, + .settings-row input[type="text"] { width: 100%; min-width: 0; } + .settings-row > .btn, .settings-row > a.btn { align-self: flex-start; } } -.tier-switch button.locked:hover, -.tier-card .btn.locked:hover, -.tier-card .btn.emperor-locked:hover { - transform: none; - background: transparent; - color: var(--chalk-soft); + +/* Four things competed for a 375px header: mark, coin balance, player chip, + hamburger. The coin balance is a 0 for a store that hasn't opened, so it + is the one that goes — which buys back enough room to keep the wordmark. */ +@media (max-width: 560px) { + .coin-chip { display: none; } + .topbar-inner, .topbar .inner { padding: 8px 12px; gap: 8px; } } -/* Current tier badge (on Membership CTA) */ -.tier-card .btn.current-tier { - background: rgba(217,122,61,0.15); - border-color: var(--accent); - border-style: solid; - color: var(--accent-soft); - cursor: default; - pointer-events: none; +@media (max-width: 360px) { + .brand-text { display: none; } } -.tier-card .btn.current-tier:hover { - transform: none; - background: rgba(217,122,61,0.15); + + +/* ========================================================= + TOUCH + Everything in here is gated on (hover: none), so a desktop + with a mouse never sees any of it. + ========================================================= */ + +@media (hover: none) { + /* The grey flash Safari and Chrome paint over any tapped control. It fires + on cards and nav rows too, which on a dark board looks like a rendering + fault rather than feedback — the :active states below do the job. */ + a, button, .thumb, .chalk-card, .country-chip, .acc-tile, + .nav-tab, .user-chip, .coin-chip, .checkbox { + -webkit-tap-highlight-color: transparent; + } + + /* :hover latches on after a tap and stays until you tap something else, so + every card you touched keeps hovering. Hand the feedback to :active, + which releases with your finger. */ + a.chalk-card:hover, + button.chalk-card:hover, + .mode-card:not(.disabled):hover, + .about-grid .chalk-card:hover, + .faq-grid .chalk-card:hover, + .update-entry:hover, + .country-chip:hover, + .acc-tile:hover { + transform: none; + box-shadow: none; + border-color: var(--line, rgba(244,241,230,0.13)); + } + a.chalk-card:active, + button.chalk-card:active, + .mode-card:not(.disabled):active, + .country-chip:active, + .acc-tile:active { + transform: scale(0.985); + background: rgba(244,241,230,0.07); + transition-duration: .08s; + } + .country-chip.active:hover, .acc-tile.active:hover { border-color: var(--accent); } + .nav-menu a:hover, .nav-menu button:hover { transform: none; } + .btn:hover:not(:disabled):not([aria-disabled="true"]) { transform: none; } + .btn:active:not(:disabled) { transform: translateY(1px) scale(0.99); } + + /* iOS zooms the whole page when you focus an input under 16px, then leaves + you scrolled sideways. 0.98rem is 15.7px, which is just under the line. */ + .field input, .field select, + .country-search, .settings-row select, .settings-row input[type="text"], + .chat-new-dm input, .chat-composer input, .text-input { + font-size: 16px; + } + + /* Controls a finger has to hit. */ + .auth-tabs button, .country-tabs button, .tier-switch button { min-height: 46px; } + .field input, .field select, .settings-row select, + .settings-row input[type="text"], .country-search, .text-input { min-height: 46px; } + .checkbox { padding: 6px 0; } + .site-footer .footer-links { gap: 8px 22px; } + .site-footer .footer-links a { padding: 6px 0; } + + /* Standalone controls that happen to be 20px-tall text links. Vertical + padding on an inline element grows the hit box without moving the line, + so nothing shifts — which is why this is safe here and not on links + sitting inside a paragraph, where the boxes would overlap. */ + .section-title .see-all, + .avatar-actions a { padding: 11px 4px; margin: -11px -4px; } } -/* Reduced motion */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; } - .chalk-dust { display: none; } .reveal { opacity: 1; transform: none; } + .section-title.reveal::after { --rule-scale: 1; } } diff --git a/site/friends.html b/site/friends.html index c976b54..d5d5bd3 100644 --- a/site/friends.html +++ b/site/friends.html @@ -5,46 +5,63 @@ Heredita — Friends - +
-
-
- - + -
-
- - 0 - coming soon + Pre-alpha + + + + + + + +
+ + 0 + + + - - - - - -
@@ -53,13 +70,12 @@

Friends

- your roster
-

- All footage from the pre-alpha build. Art & maps by the Heredita team. +

+ Everything above is from the pre-alpha build, so expect it to look different next month. + Maps and art drawn in-house.

@@ -178,18 +203,43 @@

Preview Footage

- - - - - + + + + + + diff --git a/site/index.html b/site/index.html index a206905..330c64b 100644 --- a/site/index.html +++ b/site/index.html @@ -6,16 +6,37 @@ Heredita — Roleplay the World - + - +
- -

Roll the dice. Paint the map. Rewrite history.

+ +
+ +

Roll the dice. Paint the map. Rewrite history.

+ +
    +
  • + + Runs in the browser. No launcher, no download, no patcher. +
  • +
  • + + Start in 1936, 1945 or 1991. Play the century straight, or break it on turn one. +
  • +
  • + + Every roll is public. Annexations are 2d6 in the open — no hidden combat math. +
  • +
+ + +
-

Join the timeline

+ Free while it's in pre-alpha +

Pick up the chalk

@@ -78,21 +99,20 @@

Join the time onclick="document.querySelector('[data-tab=signup]').click()">Create a new account

+

+ Guest sessions play the same game. An account just keeps your profile between devices. +

- -

- By continuing you confirm you've read our Terms & Conditions. -

@@ -100,6 +120,17 @@

Join the time - - - - + + + + + diff --git a/site/js/app.js b/site/js/app.js index 8a4ed86..065f842 100644 --- a/site/js/app.js +++ b/site/js/app.js @@ -113,7 +113,7 @@ openPaywall({ title: 'Emperor — Locked', body: 'Emperor opens with the paid release. We\'ll notify you the moment it ships.', - primaryLabel: '🔔 Notify me' + primaryLabel: 'Notify me' }); } else if (verdict.reason === 'needs-signup') { openTierGate({ @@ -158,7 +158,7 @@ -

${title || 'Locked'}

+

${title || 'Locked'}

${body || ''}

@@ -195,7 +195,7 @@ -

${title || 'Locked'}

+

${title || 'Locked'}

${body || 'This feature is locked until the paid release.'}

@@ -241,7 +241,7 @@ localStorage.setItem('heredita.avatar', JSON.stringify(next)); s.freeHatClaimed = true; setSession(s); - setTimeout(() => toast('🎩 Free top hat unlocked — enjoy until Aug 28!'), 400); + setTimeout(() => toast('Free top hat unlocked — yours until 28 August.'), 400); } catch (_) {} } @@ -273,28 +273,6 @@ document.body.appendChild(lb); } - // ---------- floating chalk dust ---------- - function spawnChalkDust(count = 22) { - if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; - const layer = document.createElement('div'); - layer.className = 'chalk-dust'; - for (let i = 0; i < count; i++) { - const s = document.createElement('span'); - const size = 2 + Math.random() * 4; - const dur = 12 + Math.random() * 22; - const delay = -Math.random() * dur; - s.style.left = Math.random() * 100 + 'vw'; - s.style.width = size + 'px'; - s.style.height = size + 'px'; - s.style.setProperty('--dx', (Math.random() * 120 - 60) + 'px'); - s.style.animationDuration = dur + 's'; - s.style.animationDelay = delay + 's'; - s.style.opacity = (0.10 + Math.random() * 0.35).toFixed(2); - layer.appendChild(s); - } - document.body.appendChild(layer); - } - // ---------- reveal on scroll ---------- let _revealIO = null; function wireReveal() { @@ -319,15 +297,9 @@ // Public hook so dynamically-injected cards can be picked up after fetch. window.HereditaApp = Object.assign(window.HereditaApp || {}, { rewireReveal: wireReveal }); - // ---------- top nav (mobile) ---------- - function wireNavToggle() { - const bar = $('.topbar'); - const btn = $('.nav-toggle'); - if (bar && btn) { - btn.addEventListener('click', () => bar.classList.toggle('mobile-open')); - } - renderUserChip(); - } + // ---------- top nav ---------- + // Drawer, dropdowns and the sliding marker all live in nav.js. This only + // fills in who's signed in. function renderUserChip() { const s = getSession(); @@ -338,12 +310,15 @@ const av = $('.user-chip .avatar'); if (av && !av.classList.contains('countryball')) av.textContent = name.charAt(0).toUpperCase(); - // new structure: .user-block with name + tier-badge + // .user-block holds the name + the tier badge; the whole chip is the + // button that opens the account menu, so the badge is just a label. $$('.user-chip .name').forEach(el => { el.textContent = name; }); $$('.user-chip .tier-badge').forEach(el => { + // For guests the badge just repeats the name ("Guest" over "GUEST"), so + // it only earns its space once there's a real tier to show. + el.hidden = (tier === 'guest'); el.setAttribute('data-tier', tier); - el.setAttribute('href', 'membership.html'); - el.setAttribute('aria-label', 'Membership tier: ' + TIER_LABEL[tier]); + el.setAttribute('title', 'Membership tier: ' + TIER_LABEL[tier]); el.innerHTML = (tier === 'emperor' ? '' : '') + TIER_LABEL[tier]; }); } @@ -622,6 +597,29 @@ let progress = 0; const PERIOD = 5500; + const stage = $('.preview-stage', root); + // Touch has no hover, so the mouse-only pause/resume never fires and the + // reel keeps advancing while a thumb is on it. + const touch = window.matchMedia('(hover: none)').matches; + + // "3 / 5". CSS shows it on touch only, so desktop is unchanged — there, + // all five thumbnails are on screen and the strip is its own counter. + let counter = null; + if (stage && slides.length > 1) { + counter = document.createElement('span'); + counter.className = 'preview-count'; + stage.appendChild(counter); + } + + // "click to enlarge" on a phone. Done here rather than in the markup so + // the desktop copy stays exactly as written. + if (touch) { + const heading = root.previousElementSibling; + const hint = heading && heading.classList.contains('section-title') + ? heading.querySelector('.muted') : null; + if (hint) hint.textContent = hint.textContent.replace(/\bclick\b/gi, 'tap'); + } + function applyMeta(i) { const t = thumbs[i]; if (!t) return; @@ -629,11 +627,29 @@ if (subEl) subEl.textContent = t.dataset.sub || ''; } + // Only two thumbnails fit on a phone, so the active one has to be brought + // to you — otherwise arrowing past the third leaves the strip behind. + function revealThumb(i) { + const t = thumbs[i]; + const strip = t && t.parentElement; + // Desktop stacks the strip vertically and shows all five, so this is a + // no-op there. + if (!t || !strip || strip.scrollWidth <= strip.clientWidth + 4) return; + // Measured, not offsetLeft — .thumb is position:relative and its + // offsetParent is the card, not the strip. + const tb = t.getBoundingClientRect(); + const sb = strip.getBoundingClientRect(); + const left = strip.scrollLeft + (tb.left - sb.left) - (sb.width - tb.width) / 2; + strip.scrollTo({ left: Math.max(0, left), behavior: 'smooth' }); + } + function goto(i, { resetTimer = true } = {}) { idx = (i + slides.length) % slides.length; slides.forEach((s, n) => s.classList.toggle('active', n === idx)); thumbs.forEach((t, n) => t.classList.toggle('active', n === idx)); applyMeta(idx); + if (counter) counter.textContent = (idx + 1) + ' / ' + slides.length; + revealThumb(idx); progress = 0; if (bar) bar.style.width = '0%'; if (resetTimer) startTimer(); @@ -647,33 +663,90 @@ function startTimer() { stopTimer(); + if (halted) return; timer = setInterval(tick, 80); } function stopTimer() { if (timer) { clearInterval(timer); timer = null; } } + // On touch, once you've driven the reel yourself it stops driving itself. + // Having it yank to the next capture mid-read is the single most annoying + // thing a carousel does on a phone. Desktop keeps its autoplay. + let halted = false; + let swiped = false; + function manual() { + if (!touch) return; + halted = true; + stopTimer(); + if (bar) bar.style.width = '0%'; + } + thumbs.forEach((t, i) => { - t.addEventListener('click', () => goto(i)); + t.addEventListener('click', () => { manual(); goto(i); }); t.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); goto(i); } }); t.setAttribute('tabindex', '0'); }); - if (prev) prev.addEventListener('click', () => goto(idx - 1)); - if (next) next.addEventListener('click', () => goto(idx + 1)); + if (prev) prev.addEventListener('click', () => { manual(); goto(idx - 1); }); + if (next) next.addEventListener('click', () => { manual(); goto(idx + 1); }); - // click stage = lightbox - const stage = $('.preview-stage', root); if (stage) { + // click stage = lightbox stage.addEventListener('click', (e) => { if (e.target.closest('.stage-nav')) return; + if (swiped) { swiped = false; return; } // that was a swipe, not a tap const active = $('.slide.active', stage); const url = active && active.dataset.full; if (url) openLightbox(url, captionEl ? captionEl.textContent : ''); }); stage.addEventListener('mouseenter', stopTimer); stage.addEventListener('mouseleave', startTimer); + wireSwipe(stage); + } + + // ---- swipe ---- + // A carousel you can only advance by aiming at a 44px arrow isn't one on a + // phone. Passive listeners throughout: we never preventDefault, so a + // vertical drag scrolls the page as normal and only a clearly horizontal + // one changes slide. + function wireSwipe(el) { + let x0 = 0, y0 = 0, tracking = false; + + el.addEventListener('touchstart', (e) => { + if (e.touches.length !== 1) return; + x0 = e.touches[0].clientX; + y0 = e.touches[0].clientY; + tracking = true; + swiped = false; + stopTimer(); // don't advance under someone's finger + }, { passive: true }); + + el.addEventListener('touchmove', (e) => { + if (!tracking) return; + const dx = e.touches[0].clientX - x0; + const dy = e.touches[0].clientY - y0; + // Once it's plainly a vertical scroll, stop watching this gesture. + if (Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > 12) tracking = false; + }, { passive: true }); + + el.addEventListener('touchend', (e) => { + const started = tracking; + tracking = false; + if (!started || !e.changedTouches.length) { startTimer(); return; } + const dx = e.changedTouches[0].clientX - x0; + const dy = e.changedTouches[0].clientY - y0; + if (Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy)) { + swiped = true; // swallow the click that follows + manual(); + goto(idx + (dx < 0 ? 1 : -1)); + } else { + startTimer(); + } + }, { passive: true }); + + el.addEventListener('touchcancel', () => { tracking = false; startTimer(); }, { passive: true }); } // keyboard nav @@ -687,6 +760,14 @@ if (document.hidden) stopTimer(); else startTimer(); }); + // On a phone the reel is one screen of a long page. Ticking every 80ms + // while it's nowhere near the viewport is pure battery. + if (touch && 'IntersectionObserver' in window) { + new IntersectionObserver((entries) => { + entries.forEach((en) => { if (en.isIntersecting) startTimer(); else stopTimer(); }); + }, { threshold: 0.25 }).observe(root); + } + goto(0); } @@ -715,7 +796,7 @@ // Token's gone — clean up. clearSession(); renderUserChip(); - toast('Your session expired — please sign in again.'); + toast('Session expired — please sign in again.'); } catch (_) {} } @@ -726,7 +807,7 @@ // Mirror local session into the shared .heredita.net cookie so the // game app on app.heredita.net auto-recognizes the signed-in user. syncCookieOnLoad(); - wireNavToggle(); + renderUserChip(); // Verify any stored game-API token in the background (non-blocking). verifyStoredToken(); wirePlayCtas(); @@ -734,6 +815,5 @@ wireHomePage(); wirePreviewShowcase(); wireReveal(); - if (document.body.classList.contains('with-dust')) spawnChalkDust(); }); })(); diff --git a/site/js/avatar.js b/site/js/avatar.js index 7d0aba0..9477f23 100644 --- a/site/js/avatar.js +++ b/site/js/avatar.js @@ -459,7 +459,7 @@ return ` `; }).join('') || `

No countries match "${search}".

`; @@ -490,7 +490,7 @@ return ` `; }).join(''); @@ -509,7 +509,7 @@ function flashLockMsg(msg) { let t = document.querySelector('.toast'); if (!t) { t = document.createElement('div'); t.className = 'toast'; document.body.appendChild(t); } - t.textContent = '🔒 ' + msg; + t.textContent = msg; t.classList.add('show'); clearTimeout(t._lockT); t._lockT = setTimeout(() => t.classList.remove('show'), 1800); @@ -555,7 +555,7 @@ // toast const t = document.createElement('div'); t.className = 'toast show'; - t.textContent = '✓ Avatar saved!'; + t.textContent = '✓ Avatar saved.'; document.body.appendChild(t); setTimeout(() => t.remove(), 1800); renderUserChips(); diff --git a/site/js/chat.js b/site/js/chat.js index cac3427..c23a6ef 100644 --- a/site/js/chat.js +++ b/site/js/chat.js @@ -79,30 +79,42 @@ mountUI({ db, dbm, username, session }); } + // chat.js is a module, so it runs after app.js has already swept the page + // for .reveal elements. Anything we inject here needs the sweep re-run or it + // stays at opacity 0 forever. + function setRoot(html) { + const root = document.getElementById('chatRoot'); + root.innerHTML = html; + if (window.HereditaApp && typeof window.HereditaApp.rewireReveal === 'function') { + window.HereditaApp.rewireReveal(); + } else { + root.querySelectorAll('.reveal').forEach(el => el.classList.add('in')); + } + return root; + } + // ---- Sign-in gate shown to guests and signed-out users function renderSignInGate(isGuest) { - const root = document.getElementById('chatRoot'); - root.innerHTML = ` + setRoot(`
-

Sign in to chat

+

Sign in to chat

${isGuest - ? "Guest sessions can't post in chat. Create a free account to join the conversation." - : "Chat is for signed-in Heredita members. Make a free account or sign in to join."} + ? "Guests can play, but the rooms need an account. Making one is free." + : "The rooms are for account holders. Sign in, or make one — it takes a minute."}

-
`; + `); } // ---- Setup screen shown when config is still the placeholder function renderSetupNotice() { - const root = document.getElementById('chatRoot'); - root.innerHTML = ` + setRoot(`
-

Chat not yet configured

+

Chat isn't wired up yet

The chat backend uses Firebase Realtime Database (free tier). Set it up in ~5 minutes — instructions are inside @@ -111,16 +123,15 @@

Once you paste your Firebase config and push, this page becomes a live chat.

-
`; + `); } function renderError(msg) { - const root = document.getElementById('chatRoot'); - root.innerHTML = ` + setRoot(`
-

Chat unavailable

+

Chat is down

${escapeHtml(msg)}

-
`; + `); } // ---- DMs: list of usernames we've started a conversation with, persisted @@ -136,18 +147,17 @@ } function mountUI({ db, dbm, username, session }) { - const root = document.getElementById('chatRoot'); - root.innerHTML = ` + setRoot(`
-

Community Chat

- real-time · signed in as ${escapeHtml(username)} +

Chat

+ signed in as ${escapeHtml(username)}
- `; + `); // ---- Render the room + DM lists const roomListEl = document.getElementById('roomList'); diff --git a/site/js/friends.js b/site/js/friends.js index 8f9465a..77c23ad 100644 --- a/site/js/friends.js +++ b/site/js/friends.js @@ -154,9 +154,9 @@ return `
- ${escapeHtml(initial)} + ${escapeHtml(initial)}
-
${escapeHtml(u.username)}
+
${escapeHtml(u.username)}
${escapeHtml(rankLabel(rank))}
diff --git a/site/js/nav.js b/site/js/nav.js new file mode 100644 index 0000000..575aa10 --- /dev/null +++ b/site/js/nav.js @@ -0,0 +1,313 @@ +/* Heredita — top navigation behaviour. + * + * Three things live here: + * 1. the chalk dash that slides between tabs + * 2. the dropdown panels (Social, account) + * 3. the mobile drawer + * + * The markup itself is plain HTML in every page — this file only + * enhances it, so the nav still works with JS off. + */ +(function () { + 'use strict'; + + var MOBILE = '(max-width: 900px)'; + var nav, marker, topbar; + var groups = []; + var scrollLock = null; + + document.addEventListener('DOMContentLoaded', init); + + function init() { + topbar = document.querySelector('.topbar'); + if (!topbar) return; + nav = topbar.querySelector('.primary-nav'); + + markActiveTab(); + initMarker(); + initDropdowns(); + initDrawer(); + initAccountMenu(); + } + + function isMobile() { return window.matchMedia(MOBILE).matches; } + + // --------------------------------------------------------- + // Active tab — derived from the URL so no page can get it wrong + // --------------------------------------------------------- + function markActiveTab() { + if (!nav) return; + var here = (location.pathname.split('/').pop() || 'index.html').toLowerCase(); + + nav.querySelectorAll('a.nav-tab').forEach(function (a) { + var target = (a.getAttribute('href') || '').split('/').pop().toLowerCase(); + var on = target === here; + a.classList.toggle('active', on); + if (on) a.setAttribute('aria-current', 'page'); + else a.removeAttribute('aria-current'); + }); + + // A dropdown's toggle lights up when you're on one of its pages. + nav.querySelectorAll('.nav-group').forEach(function (group) { + var toggle = group.querySelector(':scope > .nav-tab'); + var hit = null; + group.querySelectorAll('.nav-menu a').forEach(function (a) { + var target = (a.getAttribute('href') || '').split('/').pop().toLowerCase(); + if (target === here) { + a.classList.add('active'); + a.setAttribute('aria-current', 'page'); + hit = a; + } + }); + if (toggle) toggle.classList.toggle('active', !!hit); + }); + } + + // --------------------------------------------------------- + // Sliding chalk dash + // --------------------------------------------------------- + function initMarker() { + if (!nav) return; + marker = nav.querySelector('.nav-marker'); + if (!marker) return; + + // `instant` places the dash without sliding it in from x=0, which is what + // we want for the initial placement and after a resize. + function settle(instant) { + if (instant) marker.style.transition = 'none'; + moveMarker(nav.querySelector('.nav-tab.active'), false); + if (instant) { + void marker.offsetWidth; // flush, so the next move animates again + marker.style.transition = ''; + } + } + + nav.addEventListener('pointerenter', function (e) { + var tab = e.target.closest ? e.target.closest('.nav-tab') : null; + if (tab) moveMarker(tab, true); + }, true); + + nav.addEventListener('focusin', function (e) { + var tab = e.target.closest('.nav-tab'); + if (tab) moveMarker(tab, true); + }); + + nav.addEventListener('pointerleave', settle); + nav.addEventListener('focusout', function () { + // Let focus land somewhere before deciding where the dash goes. + setTimeout(function () { + if (!nav.contains(document.activeElement)) settle(); + }, 0); + }); + + window.addEventListener('resize', function () { settle(true); }); + + // Place it synchronously. requestAnimationFrame never fires while the tab + // is in the background, and a nav that only appears once you've looked at + // it is worse than one that appears a frame early. + settle(true); + nav.classList.add('marker-live'); + + // Web fonts land after first paint and change every tab's width. + if (document.fonts && document.fonts.ready) { + document.fonts.ready.then(function () { settle(true); }); + } + } + + function moveMarker(tab, hovering) { + if (!marker || !nav) return; + if (isMobile()) return; + if (!tab) { marker.style.width = '0px'; return; } + + var navBox = nav.getBoundingClientRect(); + var tabBox = tab.getBoundingClientRect(); + // Inset a little so the dash reads as an underline, not a bar. + var inset = 9; + marker.style.width = Math.max(0, tabBox.width - inset * 2) + 'px'; + marker.style.transform = 'translateX(' + (tabBox.left - navBox.left + inset) + 'px)'; + nav.classList.toggle('marker-hover', !!hovering); + } + + // --------------------------------------------------------- + // Dropdowns + // --------------------------------------------------------- + function initDropdowns() { + groups = Array.prototype.slice.call(topbar.querySelectorAll('.nav-group')); + + groups.forEach(function (group) { + var toggle = group.querySelector(':scope > .nav-tab, :scope > .user-chip'); + var menu = group.querySelector(':scope > .nav-menu'); + if (!toggle || !menu) return; + + toggle.setAttribute('aria-expanded', 'false'); + if (menu.id) toggle.setAttribute('aria-controls', menu.id); + + toggle.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + // The account chip stays reachable while the drawer is open, and two + // panels down the same edge is one too many. + if (isMobile() && nav && !nav.contains(toggle)) closeDrawer(); + toggleGroup(group, !group.classList.contains('open')); + }); + + toggle.addEventListener('keydown', function (e) { + if (e.key === 'ArrowDown') { + e.preventDefault(); + toggleGroup(group, true); + focusItem(menu, 0); + } + }); + + menu.addEventListener('keydown', function (e) { + var items = itemsOf(menu); + var i = items.indexOf(document.activeElement); + if (e.key === 'ArrowDown') { e.preventDefault(); focusItem(menu, i + 1); } + else if (e.key === 'ArrowUp') { e.preventDefault(); focusItem(menu, i - 1); } + else if (e.key === 'Escape') { closeAll(); toggle.focus(); } + else if (e.key === 'Tab' && !e.shiftKey && i === items.length - 1) closeAll(); + }); + }); + + document.addEventListener('click', function (e) { + if (!topbar.contains(e.target)) closeAll(); + }); + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') closeAll(); + }); + } + + function toggleGroup(group, open) { + groups.forEach(function (g) { + if (g !== group) setOpen(g, false); + }); + setOpen(group, open); + } + + function setOpen(group, open) { + group.classList.toggle('open', open); + var toggle = group.querySelector(':scope > .nav-tab, :scope > .user-chip'); + if (toggle) toggle.setAttribute('aria-expanded', String(open)); + if (open) pointNotch(group); + } + + function closeAll() { groups.forEach(function (g) { setOpen(g, false); }); } + + // Aim the little chalk notch at the middle of whatever opened the menu. + function pointNotch(group) { + var toggle = group.querySelector(':scope > .nav-tab, :scope > .user-chip'); + var menu = group.querySelector(':scope > .nav-menu'); + if (!toggle || !menu) return; + // Menus inside the drawer flatten into accordions on mobile and have no + // notch to aim; the account menu stays a real dropdown, so it still does. + if (getComputedStyle(menu).position === 'static') return; + var t = toggle.getBoundingClientRect(); + var m = menu.getBoundingClientRect(); + var x = Math.max(16, Math.min(m.width - 16, t.left + t.width / 2 - m.left)); + menu.style.setProperty('--notch', x + 'px'); + } + + function itemsOf(menu) { + return Array.prototype.slice.call(menu.querySelectorAll('a, button')) + .filter(function (el) { return el.offsetParent !== null; }); + } + + function focusItem(menu, i) { + var items = itemsOf(menu); + if (!items.length) return; + items[(i + items.length) % items.length].focus(); + } + + // --------------------------------------------------------- + // Mobile drawer + // --------------------------------------------------------- + function initDrawer() { + var btn = topbar.querySelector('.nav-toggle'); + if (!btn) return; + + btn.setAttribute('aria-expanded', 'false'); + btn.addEventListener('click', function (e) { + e.stopPropagation(); + setDrawer(btn, !topbar.classList.contains('menu-open')); + }); + + // Tapping a destination closes the drawer. + if (nav) { + nav.addEventListener('click', function (e) { + if (e.target.closest('a')) closeDrawer(btn); + }); + } + + document.addEventListener('click', function (e) { + if (!topbar.contains(e.target)) closeDrawer(btn); + }); + + // Esc closes it too, same as the dropdowns. + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') closeDrawer(btn); + }); + + window.addEventListener('resize', function () { + if (!isMobile()) closeDrawer(btn); + }); + + // Belt and braces on the one that matters: the drawer holds a scroll lock + // on , so if a rotation or a window drag crosses 900px while it's + // open and the resize event gets coalesced away, the desktop layout is + // left unable to scroll. This fires on the breakpoint itself. + var mq = window.matchMedia(MOBILE); + var onChange = function (e) { if (!e.matches) closeDrawer(btn); }; + if (mq.addEventListener) mq.addEventListener('change', onChange); + else if (mq.addListener) mq.addListener(onChange); + } + + function setDrawer(btn, open) { + topbar.classList.toggle('menu-open', open); + if (btn) btn.setAttribute('aria-expanded', String(open)); + if (!open) closeAll(); + + // The scrim is a body::after, so the class has to live on . + document.body.classList.toggle('nav-open', open); + + // Without this the page keeps scrolling under the open drawer, which on a + // phone reads as the menu having come loose from the page. Restored to + // whatever the page had rather than a hardcoded '', so pages that set + // their own overflow (chat) aren't clobbered. + if (open) { + scrollLock = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + } else if (scrollLock !== null) { + document.body.style.overflow = scrollLock; + scrollLock = null; + } + } + + function closeDrawer(btn) { + if (!topbar.classList.contains('menu-open')) return; + setDrawer(btn || topbar.querySelector('.nav-toggle'), false); + } + + // --------------------------------------------------------- + // Account menu — swaps sign-in / sign-out to match the session + // --------------------------------------------------------- + function initAccountMenu() { + var menu = topbar.querySelector('#accountMenu'); + if (!menu) return; + var S = window.HereditaSession; + var signedIn = !!(S && S.isSignedIn && S.isSignedIn()); + + var out = menu.querySelector('[data-account-signout]'); + var into = menu.querySelector('[data-account-signin]'); + if (out) out.hidden = !signedIn; + if (into) into.hidden = signedIn; + + if (out) { + out.addEventListener('click', function () { + if (!S) return; + S.clear(); + try { localStorage.removeItem('heredita.avatar'); } catch (_) {} + location.href = 'index.html'; + }); + } + } +})(); diff --git a/site/js/social-nav.js b/site/js/social-nav.js deleted file mode 100644 index a788df8..0000000 --- a/site/js/social-nav.js +++ /dev/null @@ -1,177 +0,0 @@ -/* Social popover — injects a "Social ▾" button into the primary nav and - * mounts a body-attached popover panel beneath it. - * - * Why body-attached: the topbar's nav uses display:flex with its own per- - * link padding, animated chalk underlines, etc. Putting the menu inside - * the nav fights that cascade. Attaching to with position:fixed - * sidesteps every collision and lets the panel be styled as a first-class - * chalkboard card. - * - * Pages opt in by declaring
-

- Want to preview a tier? Use the Settings page to switch your local profile. -

-
-

FAQ

- questions we keep getting +

Questions we keep getting

What's the difference between Guest and Regular?

-

Gameplay is exactly the same. Regular members get a profile, monthly Heredita Coins, and the free seasonal hat — but you can do everything Regular can do as a Guest, minus the goodies.

+

The game is identical. Regular gets you a profile that survives a browser wipe, monthly coins, and the seasonal hat. That is the whole difference.

The free hat — really free?

-

Yes. Every Regular account auto-claims a top hat the first time they open the site. It stays on your avatar forever, but the claim window closes Aug 28, 2026.

+

Yes. It claims itself the first time a Regular account opens the site, and it stays on your countryball afterwards. The window shuts 28 August 2026.

What can Emperor do that Regular can't?

@@ -154,19 +167,19 @@

Will my saves stay forever?

- - - - - + + + + + - - - - + + + + + diff --git a/site/search.html b/site/search.html index 5abca75..b8a355e 100644 --- a/site/search.html +++ b/site/search.html @@ -5,46 +5,63 @@ Heredita — Find Player - +
-
-
- - + -
-
- - 0 - coming soon + Pre-alpha + + + + + + + +
+ + 0 + + + - - - - - -
@@ -52,20 +69,19 @@
-

Find a Player

- search by exact username +

Find a player

+ Exact usernames only
+ class="text-input" style="flex: 1; min-width: 220px;" />

- Exact usernames only — the Heredita API does not yet expose fuzzy / partial search. - If the player exists, a friend request is sent immediately. + Partial search is not wired up yet, so spelling counts. If the name exists, a friend request goes out straight away.

@@ -75,7 +91,7 @@

Find a Player

@@ -92,7 +108,7 @@

Player search arrives with the real la
-

Tips

+

Tips

  • Usernames are case-sensitive.
  • You can't friend yourself.
  • @@ -105,19 +121,19 @@

    Tips

    - - - - - + + + + + - - - - + + + + + - - - - + + + + + diff --git a/site/terms.html b/site/terms.html index f4e8fca..4e415aa 100644 --- a/site/terms.html +++ b/site/terms.html @@ -5,7 +5,7 @@ Heredita — Terms & Conditions - +