Spaces:
Running
Phase 3c (D): frontend polish pass — Metropolis design system
Browse filesAesthetic direction: a discreet old-world chess café crossed with a precise
digital instrument — brass, ink, felt, parchment, under warm light. Dark theme
with warm undertones, not cool neutrals.
Design system (base.html):
- Color tokens as CSS custom properties (--mp-bg, --mp-surface-1..3,
--mp-hairline/-2, --mp-ink/muted/faint/ghost, --mp-brass/bright/dim,
--mp-felt/bright, --mp-oxblood, --mp-ink-blue(-alt), --mp-rating-*)
- Typography: Fraunces (display serif w/ optical sizing + SOFT/WONK axes),
IBM Plex Sans (body), IBM Plex Mono (numerics / eyebrows)
- Utilities: .mp-display, .mp-italic, .mp-mono, .mp-eyebrow, .mp-hr, .mp-brass-rule
- Components: .mp-panel/.mp-panel-raised/.mp-framed (brass corner marks),
.mp-btn (brass/ghost/danger variants), .mp-chip, .mp-input, .mp-board-frame,
.conn-pill (live/retry/lost), .mp-livedot (breathing pulse)
- Motion: single choreographed .mp-enter + stagger slots (1/2/3),
custom --mp-ease cubic-bezier. No ambient animation.
Shared Jinja partials refreshed:
- _macros.html — rating/visibility/preset/state/result chips on new palette
- character_card.html — member-dossier card (portrait in brass frame +
metadata grid), not SaaS tile
- match_row.html — live/recent row with result chip + live dot
Pages polished:
- landing, login, settings — typographic hero, framed forms
- / (index), /discovery — eyebrow headers, brass rule, reused partials
- /matches/{id} (play) — board in double-ruled brass frame, thinking state
with live dot, inputs + chat bubbles on tokens, disconnect overlay as
oxblood-bordered modal
- /matches/{id}/watch — spectator variant with blue-accented crowd noise panel
- /matches/{id}/summary — payoff screen: "You won." as display italic,
memories italicised with brass left border, opponent note as margin quote
- /leaderboard/characters + /players — library-catalog tables, top-3 rank in
brass, current-user highlight on player leaderboard
- /characters/{id} (detail) — member-dossier header; hall of fame inherits
leaderboard treatment
- /players/{username} — dossier header + styled match rows
- /characters/new + /edit — brass submit, display header; form bodies left
on prior Phase 3a styling (see design_system.md "known gaps")
docs/design_system.md documents tokens, components, patterns, anti-patterns,
and per-page polish levels.
All 197 tests pass, 3 skipped (opt-in live). No functional regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app/web/templates/_partials/_macros.html +18 -16
- app/web/templates/_partials/character_card.html +57 -26
- app/web/templates/_partials/match_row.html +16 -11
- app/web/templates/base.html +270 -21
- app/web/templates/detail.html +33 -47
- app/web/templates/discovery.html +30 -28
- app/web/templates/edit.html +3 -2
- app/web/templates/index.html +16 -9
- app/web/templates/landing.html +21 -9
- app/web/templates/leaderboard_characters.html +47 -35
- app/web/templates/leaderboard_players.html +51 -34
- app/web/templates/login.html +19 -18
- app/web/templates/new.html +3 -2
- app/web/templates/play.html +77 -71
- app/web/templates/player_profile.html +25 -25
- app/web/templates/settings.html +26 -39
- app/web/templates/summary.html +79 -84
- app/web/templates/watch.html +52 -55
- docs/design_system.md +130 -0
|
@@ -1,48 +1,50 @@
|
|
| 1 |
-
{# Phase 3c: shared Jinja macros
|
| 2 |
-
discovery page, leaderboards, and summary views. #}
|
| 3 |
|
| 4 |
{% macro rating_badge(r) -%}
|
| 5 |
{% set val = r.value if r.value is defined else r %}
|
| 6 |
{% if val == 'family' %}
|
| 7 |
-
<span class="
|
| 8 |
{% elif val == 'mature' %}
|
| 9 |
-
<span class="
|
| 10 |
{% elif val == 'unrestricted' %}
|
| 11 |
-
<span class="
|
| 12 |
{% endif %}
|
| 13 |
{%- endmacro %}
|
| 14 |
|
| 15 |
{% macro character_state_badge(s) -%}
|
| 16 |
{% set val = s.value if s.value is defined else s %}
|
| 17 |
{% if val == 'generating_memories' %}
|
| 18 |
-
<span class="
|
| 19 |
{% elif val == 'generation_failed' %}
|
| 20 |
-
<span class="
|
| 21 |
{% else %}
|
| 22 |
-
<span class="
|
| 23 |
{% endif %}
|
| 24 |
{%- endmacro %}
|
| 25 |
|
| 26 |
{% macro visibility_badge(v) -%}
|
| 27 |
{% set val = v.value if v.value is defined else v %}
|
| 28 |
{% if val == 'private' %}
|
| 29 |
-
<span class="
|
| 30 |
{% endif %}
|
| 31 |
{%- endmacro %}
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
{% macro result_chip(m) -%}
|
| 34 |
-
{# m is a MatchSummaryOut-ish thing: has .status and .result #}
|
| 35 |
{% if m.status == 'in_progress' %}
|
| 36 |
-
<span class="
|
| 37 |
{% elif m.status == 'abandoned' %}
|
| 38 |
-
<span class="
|
| 39 |
{% elif m.result == 'draw' %}
|
| 40 |
-
<span class="
|
| 41 |
{% elif m.result == 'white_win' %}
|
| 42 |
-
<span class="
|
| 43 |
{% elif m.result == 'black_win' %}
|
| 44 |
-
<span class="
|
| 45 |
{% else %}
|
| 46 |
-
<span class="
|
| 47 |
{% endif %}
|
| 48 |
{%- endmacro %}
|
|
|
|
| 1 |
+
{# Phase 3c polish pass: shared Jinja macros. Tokens defined in base.html. #}
|
|
|
|
| 2 |
|
| 3 |
{% macro rating_badge(r) -%}
|
| 4 |
{% set val = r.value if r.value is defined else r %}
|
| 5 |
{% if val == 'family' %}
|
| 6 |
+
<span class="mp-chip" style="background: var(--mp-rating-family-bg); color: var(--mp-rating-family);">Family</span>
|
| 7 |
{% elif val == 'mature' %}
|
| 8 |
+
<span class="mp-chip" style="background: var(--mp-rating-mature-bg); color: var(--mp-rating-mature);">Mature</span>
|
| 9 |
{% elif val == 'unrestricted' %}
|
| 10 |
+
<span class="mp-chip" style="background: var(--mp-rating-unrestricted-bg); color: var(--mp-rating-unrestricted);">Unrestricted</span>
|
| 11 |
{% endif %}
|
| 12 |
{%- endmacro %}
|
| 13 |
|
| 14 |
{% macro character_state_badge(s) -%}
|
| 15 |
{% set val = s.value if s.value is defined else s %}
|
| 16 |
{% if val == 'generating_memories' %}
|
| 17 |
+
<span class="mp-chip" style="background: rgba(201,166,107,0.12); color: var(--mp-brass-bright);">Generating</span>
|
| 18 |
{% elif val == 'generation_failed' %}
|
| 19 |
+
<span class="mp-chip" style="background: rgba(168,66,63,0.18); color: #E79E9B;">Failed</span>
|
| 20 |
{% else %}
|
| 21 |
+
<span class="mp-chip" style="background: rgba(47,107,92,0.14); color: var(--mp-felt-bright);">Ready</span>
|
| 22 |
{% endif %}
|
| 23 |
{%- endmacro %}
|
| 24 |
|
| 25 |
{% macro visibility_badge(v) -%}
|
| 26 |
{% set val = v.value if v.value is defined else v %}
|
| 27 |
{% if val == 'private' %}
|
| 28 |
+
<span class="mp-chip" style="background: var(--mp-surface-3); color: var(--mp-ink-muted); border-color: var(--mp-hairline-2);">Private</span>
|
| 29 |
{% endif %}
|
| 30 |
{%- endmacro %}
|
| 31 |
|
| 32 |
+
{% macro preset_badge() -%}
|
| 33 |
+
<span class="mp-chip" style="background: transparent; color: var(--mp-brass); border-color: var(--mp-brass-dim);">House</span>
|
| 34 |
+
{%- endmacro %}
|
| 35 |
+
|
| 36 |
{% macro result_chip(m) -%}
|
|
|
|
| 37 |
{% if m.status == 'in_progress' %}
|
| 38 |
+
<span class="mp-chip" style="background: rgba(79,164,141,0.12); color: var(--mp-felt-bright);"><span class="mp-livedot"></span>Live</span>
|
| 39 |
{% elif m.status == 'abandoned' %}
|
| 40 |
+
<span class="mp-chip" style="background: var(--mp-surface-3); color: var(--mp-ink-faint); border-color: var(--mp-hairline-2);">Resigned</span>
|
| 41 |
{% elif m.result == 'draw' %}
|
| 42 |
+
<span class="mp-chip" style="background: rgba(88,121,163,0.14); color: var(--mp-ink-blue-alt);">Draw</span>
|
| 43 |
{% elif m.result == 'white_win' %}
|
| 44 |
+
<span class="mp-chip" style="background: rgba(237,228,206,0.12); color: var(--mp-ink);">White</span>
|
| 45 |
{% elif m.result == 'black_win' %}
|
| 46 |
+
<span class="mp-chip" style="background: rgba(47,74,66,0.60); color: var(--mp-ink);">Black</span>
|
| 47 |
{% else %}
|
| 48 |
+
<span class="mp-chip" style="background: var(--mp-surface-3); color: var(--mp-ink-faint);">—</span>
|
| 49 |
{% endif %}
|
| 50 |
{%- endmacro %}
|
|
@@ -1,30 +1,61 @@
|
|
| 1 |
-
{# Character
|
| 2 |
-
|
|
|
|
| 3 |
<a href="/characters/{{ c.id }}"
|
| 4 |
-
class="
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
{{
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
</div>
|
| 15 |
-
</div>
|
| 16 |
-
<div class="mt-3">
|
| 17 |
-
<div class="text-lg font-semibold group-hover:text-white">{{ c.name }}</div>
|
| 18 |
-
<div class="text-sm text-neutral-400 mt-1 line-clamp-3">{{ c.short_description }}</div>
|
| 19 |
-
</div>
|
| 20 |
-
<div class="mt-3 text-xs text-neutral-500">
|
| 21 |
-
{% if c.is_preset %}System character
|
| 22 |
-
{% elif player and c.owner_id == player.id %}Your character
|
| 23 |
-
{% elif c.owner_id and owner_map and owner_map.get(c.owner_id) %}@{{ owner_map[c.owner_id] }}'s character
|
| 24 |
-
{% endif %}
|
| 25 |
-
</div>
|
| 26 |
-
<div class="mt-4 flex items-center justify-between text-xs text-neutral-500">
|
| 27 |
-
<span>Target {{ c.target_elo }} Elo{% if c.adaptive %} · adaptive{% endif %}</span>
|
| 28 |
-
<span>agg {{ c.aggression }} · risk {{ c.risk_tolerance }} · pat {{ c.patience }} · talk {{ c.trash_talk }}</span>
|
| 29 |
</div>
|
| 30 |
</a>
|
|
|
|
| 1 |
+
{# Character trading-card. Requires `c`, `player`, `owner_map` in scope.
|
| 2 |
+
Asymmetric composition: portrait panel on the left, dossier on the right. #}
|
| 3 |
+
{% from "_partials/_macros.html" import rating_badge, character_state_badge, visibility_badge, preset_badge %}
|
| 4 |
<a href="/characters/{{ c.id }}"
|
| 5 |
+
class="mp-panel-raised mp-framed group relative block rounded-sm transition"
|
| 6 |
+
style="padding: 1.1rem 1.2rem;">
|
| 7 |
+
<span class="mp-frame-tl"></span><span class="mp-frame-br"></span>
|
| 8 |
+
|
| 9 |
+
<div class="flex items-start gap-4">
|
| 10 |
+
{# Portrait panel — emoji inside a square brass frame. #}
|
| 11 |
+
<div class="shrink-0 w-14 h-14 flex items-center justify-center border border-[var(--mp-hairline-2)] bg-[var(--mp-surface-1)] rounded-sm text-3xl leading-none"
|
| 12 |
+
style="background-image: radial-gradient(60% 60% at 50% 40%, rgba(201,166,107,0.10), transparent 70%);">
|
| 13 |
+
<span class="block translate-y-[-1px]">{{ c.avatar_emoji or "♟" }}</span>
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
{# Dossier. #}
|
| 17 |
+
<div class="flex-1 min-w-0">
|
| 18 |
+
<div class="flex items-start justify-between gap-2 mb-1">
|
| 19 |
+
<h3 class="mp-display text-[20px] leading-tight text-[var(--mp-ink)] group-hover:text-[var(--mp-brass-bright)] transition">
|
| 20 |
+
{{ c.name }}
|
| 21 |
+
</h3>
|
| 22 |
+
<div class="flex items-center gap-1 flex-wrap justify-end shrink-0">
|
| 23 |
+
{% if c.is_preset %}{{ preset_badge() }}{% endif %}
|
| 24 |
+
{{ rating_badge(c.content_rating) }}
|
| 25 |
+
{{ visibility_badge(c.visibility) }}
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
<p class="text-[13px] text-[var(--mp-ink-muted)] mp-clamp-2 leading-snug">
|
| 30 |
+
{{ c.short_description }}
|
| 31 |
+
</p>
|
| 32 |
+
|
| 33 |
+
<div class="mt-3 flex items-center justify-between text-[11px] text-[var(--mp-ink-faint)]">
|
| 34 |
+
<span class="mp-mono">
|
| 35 |
+
{{ c.current_elo }} <span class="text-[var(--mp-ink-ghost)]">Elo</span>
|
| 36 |
+
{% if c.adaptive %} · <span class="text-[var(--mp-felt-bright)]">adaptive</span>{% endif %}
|
| 37 |
+
</span>
|
| 38 |
+
<span>
|
| 39 |
+
{% if c.is_preset %}
|
| 40 |
+
House character
|
| 41 |
+
{% elif player and c.owner_id == player.id %}
|
| 42 |
+
Your character
|
| 43 |
+
{% elif c.owner_id and owner_map and owner_map.get(c.owner_id) %}
|
| 44 |
+
<span class="mp-mono">@{{ owner_map[c.owner_id] }}</span>
|
| 45 |
+
{% endif %}
|
| 46 |
+
</span>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<div class="mt-2 flex items-center gap-3 text-[10px] text-[var(--mp-ink-faint)] mp-mono uppercase tracking-[0.1em]">
|
| 50 |
+
<span>agg {{ c.aggression }}</span>
|
| 51 |
+
<span class="text-[var(--mp-ink-ghost)]">·</span>
|
| 52 |
+
<span>risk {{ c.risk_tolerance }}</span>
|
| 53 |
+
<span class="text-[var(--mp-ink-ghost)]">·</span>
|
| 54 |
+
<span>pat {{ c.patience }}</span>
|
| 55 |
+
<span class="text-[var(--mp-ink-ghost)]">·</span>
|
| 56 |
+
<span>talk {{ c.trash_talk }}</span>
|
| 57 |
+
<span class="ml-auto">{{ character_state_badge(c.state) }}</span>
|
| 58 |
+
</div>
|
| 59 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
</div>
|
| 61 |
</a>
|
|
@@ -2,27 +2,32 @@
|
|
| 2 |
{% from "_partials/_macros.html" import result_chip %}
|
| 3 |
{% set href = '/matches/' + m.match_id + '/watch' if m.status == 'in_progress' else '/matches/' + m.match_id + '/summary' %}
|
| 4 |
<a href="{{ href }}"
|
| 5 |
-
class="flex items-center gap-4 rounded-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
<div class="flex-1 min-w-0">
|
| 8 |
-
<div class="flex items-center gap-2">
|
| 9 |
-
<
|
| 10 |
{{ result_chip(m) }}
|
| 11 |
</div>
|
| 12 |
-
<div class="text-
|
| 13 |
-
vs
|
|
|
|
|
|
|
| 14 |
{% if m.status == 'in_progress' %}
|
| 15 |
-
· started {{ m.started_at.strftime('%H:%M') }}
|
| 16 |
{% elif m.ended_at %}
|
| 17 |
-
|
| 18 |
{% endif %}
|
| 19 |
</div>
|
| 20 |
</div>
|
| 21 |
-
<div class="text-
|
| 22 |
{% if m.status == 'in_progress' %}
|
| 23 |
-
<span class="text-
|
| 24 |
{% else %}
|
| 25 |
-
<span class="text-
|
| 26 |
{% endif %}
|
| 27 |
</div>
|
| 28 |
</a>
|
|
|
|
| 2 |
{% from "_partials/_macros.html" import result_chip %}
|
| 3 |
{% set href = '/matches/' + m.match_id + '/watch' if m.status == 'in_progress' else '/matches/' + m.match_id + '/summary' %}
|
| 4 |
<a href="{{ href }}"
|
| 5 |
+
class="mp-panel-raised group flex items-center gap-4 rounded-sm transition relative"
|
| 6 |
+
style="padding: 0.9rem 1rem;">
|
| 7 |
+
<div class="shrink-0 w-11 h-11 flex items-center justify-center border border-[var(--mp-hairline-2)] bg-[var(--mp-surface-1)] rounded-sm text-xl leading-none">
|
| 8 |
+
{{ m.character_avatar }}
|
| 9 |
+
</div>
|
| 10 |
<div class="flex-1 min-w-0">
|
| 11 |
+
<div class="flex items-center gap-2 flex-wrap">
|
| 12 |
+
<span class="mp-display text-[17px] text-[var(--mp-ink)] truncate group-hover:text-[var(--mp-brass-bright)] transition">{{ m.character_name }}</span>
|
| 13 |
{{ result_chip(m) }}
|
| 14 |
</div>
|
| 15 |
+
<div class="text-[11px] text-[var(--mp-ink-faint)] truncate mt-1 mp-mono uppercase tracking-[0.1em]">
|
| 16 |
+
vs <span class="text-[var(--mp-ink-muted)] normal-case tracking-normal">@{{ m.player_username }}</span>
|
| 17 |
+
<span class="text-[var(--mp-ink-ghost)]"> · </span>
|
| 18 |
+
move {{ m.move_count }}
|
| 19 |
{% if m.status == 'in_progress' %}
|
| 20 |
+
<span class="text-[var(--mp-ink-ghost)]"> · </span>started {{ m.started_at.strftime('%H:%M') }}
|
| 21 |
{% elif m.ended_at %}
|
| 22 |
+
<span class="text-[var(--mp-ink-ghost)]"> · </span>{{ m.ended_at.strftime('%Y-%m-%d %H:%M') }}
|
| 23 |
{% endif %}
|
| 24 |
</div>
|
| 25 |
</div>
|
| 26 |
+
<div class="text-[11px] text-[var(--mp-ink-faint)] mp-mono uppercase tracking-[0.15em] shrink-0">
|
| 27 |
{% if m.status == 'in_progress' %}
|
| 28 |
+
<span class="text-[var(--mp-brass)] group-hover:text-[var(--mp-brass-bright)]">Watch ›</span>
|
| 29 |
{% else %}
|
| 30 |
+
<span class="group-hover:text-[var(--mp-ink)]">Summary ›</span>
|
| 31 |
{% endif %}
|
| 32 |
</div>
|
| 33 |
</a>
|
|
@@ -5,38 +5,284 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
<title>{% block title %}Metropolis Chess Club{% endblock %}</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
{% block head_extra %}{% endblock %}
|
| 9 |
</head>
|
| 10 |
-
<body class="min-h-screen
|
| 11 |
-
<header class="border-b border-
|
| 12 |
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
| 13 |
-
<a href="/" class="
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
</a>
|
| 16 |
-
<nav class="text-
|
| 17 |
{% if player %}
|
| 18 |
-
<a href="/" class="hover:text-
|
| 19 |
-
<a href="/discovery" class="hover:text-
|
| 20 |
-
<a href="/leaderboard/characters" class="hover:text-
|
| 21 |
-
<a href="/characters/new" class="
|
| 22 |
-
|
|
|
|
| 23 |
</a>
|
| 24 |
-
<a href="/
|
| 25 |
-
@{{ player.username }}
|
| 26 |
-
</a>
|
| 27 |
-
<a href="/logout" class="text-neutral-400 hover:text-white">Logout</a>
|
| 28 |
{% else %}
|
| 29 |
-
<a href="/login" class="
|
| 30 |
-
Log in
|
| 31 |
-
</a>
|
| 32 |
{% endif %}
|
| 33 |
</nav>
|
| 34 |
</div>
|
| 35 |
</header>
|
|
|
|
| 36 |
{% block flash %}
|
| 37 |
{% if request.query_params.get('flash') %}
|
| 38 |
<div class="max-w-6xl mx-auto px-6 pt-4">
|
| 39 |
-
<div class="rounded border border-
|
| 40 |
{% set fkey = request.query_params.get('flash') %}
|
| 41 |
{% set fuser = request.query_params.get('u') %}
|
| 42 |
{% if fkey == 'welcome' %}Welcome, @{{ fuser }}. Your account was created.
|
|
@@ -49,11 +295,14 @@
|
|
| 49 |
</div>
|
| 50 |
{% endif %}
|
| 51 |
{% endblock %}
|
| 52 |
-
|
|
|
|
| 53 |
{% block content %}{% endblock %}
|
| 54 |
</main>
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
| 57 |
</footer>
|
| 58 |
</body>
|
| 59 |
</html>
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
<title>{% block title %}Metropolis Chess Club{% endblock %}</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
|
| 9 |
+
{# Typography: Fraunces (display serif) + IBM Plex Sans (body) + IBM Plex Mono (numerics). #}
|
| 10 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 11 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 12 |
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,300..800,0..100,0..1;1,9..144,300..800,0..100,0..1&family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
| 13 |
+
|
| 14 |
+
<style>
|
| 15 |
+
/* =========================================================
|
| 16 |
+
Metropolis design system — Phase 3c polish pass.
|
| 17 |
+
Established in base.html so every page inherits automatically.
|
| 18 |
+
Token names are documented in docs/design_system.md.
|
| 19 |
+
========================================================= */
|
| 20 |
+
:root {
|
| 21 |
+
/* Surface palette — felt + parchment + brass under low light. */
|
| 22 |
+
--mp-bg: #0F1412; /* deep green-black (felt in shadow) */
|
| 23 |
+
--mp-surface-1: #151B19; /* primary panel */
|
| 24 |
+
--mp-surface-2: #1C2320; /* raised card */
|
| 25 |
+
--mp-surface-3: #232A27; /* hover lift */
|
| 26 |
+
--mp-hairline: #2E3430; /* subtle border */
|
| 27 |
+
--mp-hairline-2: #3A4038; /* stronger border */
|
| 28 |
+
|
| 29 |
+
/* Ink — warm off-whites, not cool greys. */
|
| 30 |
+
--mp-ink: #EDE4CE; /* primary text (parchment) */
|
| 31 |
+
--mp-ink-muted: #B5AD96; /* secondary text */
|
| 32 |
+
--mp-ink-faint: #7F7966; /* captions, metadata */
|
| 33 |
+
--mp-ink-ghost: #4C483E; /* placeholders */
|
| 34 |
+
|
| 35 |
+
/* Metals & felt — the accent system. */
|
| 36 |
+
--mp-brass: #C9A66B; /* primary accent */
|
| 37 |
+
--mp-brass-bright: #E0C38F; /* emphasis / live-state */
|
| 38 |
+
--mp-brass-dim: #8E7649; /* borders under brass elements */
|
| 39 |
+
--mp-felt: #2F6B5C; /* success / in-progress */
|
| 40 |
+
--mp-felt-bright: #4FA48D;
|
| 41 |
+
--mp-oxblood: #A8423F; /* resign / danger */
|
| 42 |
+
--mp-ink-blue: #5879A3; /* cool secondary — chat, spectators */
|
| 43 |
+
--mp-ink-blue-alt: #8DA4C3;
|
| 44 |
+
|
| 45 |
+
/* Rating chips — keep the semantic mapping from Phase 3a, but warmer. */
|
| 46 |
+
--mp-rating-family: #4FA48D;
|
| 47 |
+
--mp-rating-family-bg: rgba(47, 107, 92, 0.20);
|
| 48 |
+
--mp-rating-mature: #C9A66B;
|
| 49 |
+
--mp-rating-mature-bg: rgba(201, 166, 107, 0.16);
|
| 50 |
+
--mp-rating-unrestricted: #B45A56;
|
| 51 |
+
--mp-rating-unrestricted-bg: rgba(168, 66, 63, 0.22);
|
| 52 |
+
|
| 53 |
+
/* Motion. */
|
| 54 |
+
--mp-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
html, body { background: var(--mp-bg); color: var(--mp-ink); }
|
| 58 |
+
body {
|
| 59 |
+
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif;
|
| 60 |
+
font-weight: 400;
|
| 61 |
+
-webkit-font-smoothing: antialiased;
|
| 62 |
+
text-rendering: optimizeLegibility;
|
| 63 |
+
/* Subtle paper-grain on the whole canvas. Keeps large dark areas from
|
| 64 |
+
looking flat under warm lighting. */
|
| 65 |
+
background-image:
|
| 66 |
+
radial-gradient(1200px 600px at 80% -10%, rgba(201, 166, 107, 0.06), transparent 60%),
|
| 67 |
+
radial-gradient(900px 500px at 10% 120%, rgba(47, 107, 92, 0.05), transparent 60%);
|
| 68 |
+
background-attachment: fixed;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Display type — serif with optical sizing and subtle WONK axis for character. */
|
| 72 |
+
.mp-display {
|
| 73 |
+
font-family: 'Fraunces', 'Times New Roman', serif;
|
| 74 |
+
font-optical-sizing: auto;
|
| 75 |
+
font-variation-settings: 'opsz' 48, 'SOFT' 30, 'WONK' 1;
|
| 76 |
+
letter-spacing: -0.01em;
|
| 77 |
+
}
|
| 78 |
+
.mp-display-tight { letter-spacing: -0.02em; }
|
| 79 |
+
.mp-italic { font-style: italic; font-variation-settings: 'opsz' 48, 'SOFT' 60, 'WONK' 1; }
|
| 80 |
+
.mp-mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; }
|
| 81 |
+
|
| 82 |
+
/* Eyebrow / uppercase label used across pages. */
|
| 83 |
+
.mp-eyebrow {
|
| 84 |
+
font-family: 'IBM Plex Mono', ui-monospace, monospace;
|
| 85 |
+
font-size: 10px;
|
| 86 |
+
letter-spacing: 0.18em;
|
| 87 |
+
text-transform: uppercase;
|
| 88 |
+
color: var(--mp-ink-faint);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/* Hairline rules — used sparingly, never Tailwind's default. */
|
| 92 |
+
.mp-hr { height: 1px; background: var(--mp-hairline); border: 0; }
|
| 93 |
+
.mp-hr-strong { height: 1px; background: var(--mp-hairline-2); border: 0; }
|
| 94 |
+
|
| 95 |
+
/* Brass corner marks — decorative, applied via .mp-framed on any container. */
|
| 96 |
+
.mp-framed { position: relative; }
|
| 97 |
+
.mp-framed::before, .mp-framed::after,
|
| 98 |
+
.mp-framed > .mp-frame-tl, .mp-framed > .mp-frame-br {
|
| 99 |
+
content: ''; position: absolute; width: 10px; height: 10px; pointer-events: none;
|
| 100 |
+
}
|
| 101 |
+
.mp-framed::before { /* top-right */
|
| 102 |
+
top: -1px; right: -1px;
|
| 103 |
+
border-top: 1px solid var(--mp-brass); border-right: 1px solid var(--mp-brass);
|
| 104 |
+
}
|
| 105 |
+
.mp-framed::after { /* bottom-left */
|
| 106 |
+
bottom: -1px; left: -1px;
|
| 107 |
+
border-bottom: 1px solid var(--mp-brass); border-left: 1px solid var(--mp-brass);
|
| 108 |
+
}
|
| 109 |
+
.mp-frame-tl {
|
| 110 |
+
top: -1px; left: -1px;
|
| 111 |
+
border-top: 1px solid var(--mp-brass); border-left: 1px solid var(--mp-brass);
|
| 112 |
+
}
|
| 113 |
+
.mp-frame-br {
|
| 114 |
+
bottom: -1px; right: -1px;
|
| 115 |
+
border-bottom: 1px solid var(--mp-brass); border-right: 1px solid var(--mp-brass);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* Panel — the workhorse container (cards, tables, list rows). */
|
| 119 |
+
.mp-panel {
|
| 120 |
+
background: var(--mp-surface-1);
|
| 121 |
+
border: 1px solid var(--mp-hairline);
|
| 122 |
+
}
|
| 123 |
+
.mp-panel-raised {
|
| 124 |
+
background: var(--mp-surface-2);
|
| 125 |
+
border: 1px solid var(--mp-hairline);
|
| 126 |
+
}
|
| 127 |
+
.mp-panel-raised:hover {
|
| 128 |
+
background: var(--mp-surface-3);
|
| 129 |
+
border-color: var(--mp-hairline-2);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* Button primitives. */
|
| 133 |
+
.mp-btn {
|
| 134 |
+
display: inline-flex; align-items: center; gap: 0.5rem;
|
| 135 |
+
font-family: 'IBM Plex Sans', sans-serif;
|
| 136 |
+
font-weight: 500; font-size: 13px;
|
| 137 |
+
padding: 0.45rem 0.85rem;
|
| 138 |
+
border-radius: 2px;
|
| 139 |
+
border: 1px solid transparent;
|
| 140 |
+
transition: background 180ms var(--mp-ease), border-color 180ms var(--mp-ease), color 180ms var(--mp-ease);
|
| 141 |
+
cursor: pointer;
|
| 142 |
+
}
|
| 143 |
+
.mp-btn-brass {
|
| 144 |
+
background: linear-gradient(180deg, #D5B579 0%, #A78248 100%);
|
| 145 |
+
color: #1A140A;
|
| 146 |
+
border-color: #7D5E32;
|
| 147 |
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), 0 1px 0 rgba(0,0,0,0.2);
|
| 148 |
+
}
|
| 149 |
+
.mp-btn-brass:hover { background: linear-gradient(180deg, #E4C48B 0%, #B48F56 100%); }
|
| 150 |
+
.mp-btn-ghost {
|
| 151 |
+
background: transparent; color: var(--mp-ink-muted); border-color: var(--mp-hairline-2);
|
| 152 |
+
}
|
| 153 |
+
.mp-btn-ghost:hover { color: var(--mp-ink); border-color: var(--mp-brass-dim); }
|
| 154 |
+
.mp-btn-danger {
|
| 155 |
+
background: rgba(168, 66, 63, 0.12); color: #E79E9B; border-color: rgba(168, 66, 63, 0.5);
|
| 156 |
+
}
|
| 157 |
+
.mp-btn-danger:hover { background: rgba(168, 66, 63, 0.2); color: #F3BEBB; }
|
| 158 |
+
|
| 159 |
+
/* Chips (rating / status / result). */
|
| 160 |
+
.mp-chip {
|
| 161 |
+
display: inline-flex; align-items: center;
|
| 162 |
+
font-family: 'IBM Plex Mono', monospace;
|
| 163 |
+
font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
|
| 164 |
+
padding: 2px 8px; border-radius: 2px;
|
| 165 |
+
border: 1px solid transparent;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* Live-state dot — breathes once a second. */
|
| 169 |
+
.mp-livedot {
|
| 170 |
+
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
| 171 |
+
background: var(--mp-felt-bright);
|
| 172 |
+
box-shadow: 0 0 0 0 rgba(79, 164, 141, 0.65);
|
| 173 |
+
animation: mp-pulse 2s infinite;
|
| 174 |
+
margin-right: 6px;
|
| 175 |
+
}
|
| 176 |
+
@keyframes mp-pulse {
|
| 177 |
+
0% { box-shadow: 0 0 0 0 rgba(79, 164, 141, 0.6); }
|
| 178 |
+
70% { box-shadow: 0 0 0 6px rgba(79, 164, 141, 0); }
|
| 179 |
+
100% { box-shadow: 0 0 0 0 rgba(79, 164, 141, 0); }
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* Page entry choreography — staggered fade-up on first render only. */
|
| 183 |
+
@keyframes mp-enter {
|
| 184 |
+
from { opacity: 0; transform: translateY(8px); }
|
| 185 |
+
to { opacity: 1; transform: translateY(0); }
|
| 186 |
+
}
|
| 187 |
+
.mp-enter { animation: mp-enter 520ms var(--mp-ease) both; }
|
| 188 |
+
.mp-enter-1 { animation-delay: 40ms; }
|
| 189 |
+
.mp-enter-2 { animation-delay: 120ms; }
|
| 190 |
+
.mp-enter-3 { animation-delay: 200ms; }
|
| 191 |
+
|
| 192 |
+
/* Forms — inputs feel like engraved fields. */
|
| 193 |
+
.mp-input {
|
| 194 |
+
font-family: 'IBM Plex Sans', sans-serif;
|
| 195 |
+
background: var(--mp-bg);
|
| 196 |
+
color: var(--mp-ink);
|
| 197 |
+
border: 1px solid var(--mp-hairline-2);
|
| 198 |
+
border-radius: 2px;
|
| 199 |
+
padding: 0.5rem 0.75rem;
|
| 200 |
+
font-size: 14px;
|
| 201 |
+
outline: none;
|
| 202 |
+
transition: border-color 140ms var(--mp-ease);
|
| 203 |
+
}
|
| 204 |
+
.mp-input:focus { border-color: var(--mp-brass); }
|
| 205 |
+
.mp-input::placeholder { color: var(--mp-ink-ghost); }
|
| 206 |
+
|
| 207 |
+
/* Chat bubble variants (used by play.html + watch.html). */
|
| 208 |
+
.chat-bubble-agent {
|
| 209 |
+
background: linear-gradient(180deg, rgba(47,107,92,0.18), rgba(47,107,92,0.06));
|
| 210 |
+
border-left: 2px solid var(--mp-felt-bright);
|
| 211 |
+
padding: 10px 14px; border-radius: 2px;
|
| 212 |
+
}
|
| 213 |
+
.chat-bubble-player {
|
| 214 |
+
background: linear-gradient(180deg, rgba(201,166,107,0.14), rgba(201,166,107,0.05));
|
| 215 |
+
border-left: 2px solid var(--mp-brass);
|
| 216 |
+
margin-left: 1.5rem;
|
| 217 |
+
padding: 10px 14px; border-radius: 2px;
|
| 218 |
+
}
|
| 219 |
+
.chat-bubble-player.in-transit { opacity: 0.55; }
|
| 220 |
+
.chat-bubble-spectator {
|
| 221 |
+
background: rgba(88, 121, 163, 0.10);
|
| 222 |
+
border-left: 2px solid var(--mp-ink-blue);
|
| 223 |
+
padding: 10px 14px; border-radius: 2px;
|
| 224 |
+
}
|
| 225 |
+
.chat-bubble-spectator.in-transit { opacity: 0.55; }
|
| 226 |
+
|
| 227 |
+
/* Chessboard.js colour overrides — felt tint. */
|
| 228 |
+
.white-1e1d7 { background-color: #EDE4CE !important; }
|
| 229 |
+
.black-3c85d { background-color: #2F4A42 !important; }
|
| 230 |
+
|
| 231 |
+
/* Utility: thin brass rule for section separators. */
|
| 232 |
+
.mp-brass-rule {
|
| 233 |
+
height: 1px;
|
| 234 |
+
background: linear-gradient(90deg, transparent, var(--mp-brass-dim) 20%, var(--mp-brass-dim) 80%, transparent);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
/* Connection pill states — reused across play/watch. */
|
| 238 |
+
.conn-pill {
|
| 239 |
+
display: inline-flex; align-items: center; gap: 6px;
|
| 240 |
+
font-family: 'IBM Plex Mono', monospace;
|
| 241 |
+
font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;
|
| 242 |
+
padding: 3px 8px; border-radius: 2px; border: 1px solid transparent;
|
| 243 |
+
}
|
| 244 |
+
.conn-live { background: rgba(79,164,141,0.12); color: var(--mp-felt-bright); border-color: rgba(79,164,141,0.35); }
|
| 245 |
+
.conn-retry { background: rgba(201,166,107,0.10); color: var(--mp-brass-bright); border-color: rgba(201,166,107,0.35); }
|
| 246 |
+
.conn-lost { background: rgba(168,66,63,0.15); color: #E79E9B; border-color: rgba(168,66,63,0.45); }
|
| 247 |
+
|
| 248 |
+
/* Line-clamp utility — Tailwind's ships via plugin we don't load here. */
|
| 249 |
+
.mp-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
| 250 |
+
.mp-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
| 251 |
+
</style>
|
| 252 |
+
|
| 253 |
{% block head_extra %}{% endblock %}
|
| 254 |
</head>
|
| 255 |
+
<body class="min-h-screen">
|
| 256 |
+
<header class="border-b border-[var(--mp-hairline)] bg-[var(--mp-surface-1)]/70 backdrop-blur-sm">
|
| 257 |
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
| 258 |
+
<a href="/" class="flex items-center gap-3 group">
|
| 259 |
+
{# Monogram M framed in brass — small but intentional. #}
|
| 260 |
+
<span class="inline-flex items-center justify-center w-8 h-8 rounded-sm border border-[var(--mp-brass-dim)] text-[var(--mp-brass)] mp-display text-lg group-hover:border-[var(--mp-brass)] group-hover:text-[var(--mp-brass-bright)] transition">
|
| 261 |
+
M
|
| 262 |
+
</span>
|
| 263 |
+
<span class="mp-display text-[19px] tracking-tight">Metropolis <span class="mp-italic text-[var(--mp-ink-muted)]">Chess Club</span></span>
|
| 264 |
</a>
|
| 265 |
+
<nav class="text-[13px] text-[var(--mp-ink-muted)] space-x-5 flex items-center">
|
| 266 |
{% if player %}
|
| 267 |
+
<a href="/" class="hover:text-[var(--mp-ink)] transition">Characters</a>
|
| 268 |
+
<a href="/discovery" class="hover:text-[var(--mp-ink)] transition">Discovery</a>
|
| 269 |
+
<a href="/leaderboard/characters" class="hover:text-[var(--mp-ink)] transition">Leaderboard</a>
|
| 270 |
+
<a href="/characters/new" class="mp-btn mp-btn-brass">+ New character</a>
|
| 271 |
+
<a href="/settings" class="hover:text-[var(--mp-ink)] transition" title="Settings (@{{ player.username }})">
|
| 272 |
+
<span class="mp-mono text-[12px]">@{{ player.username }}</span>
|
| 273 |
</a>
|
| 274 |
+
<a href="/logout" class="text-[var(--mp-ink-faint)] hover:text-[var(--mp-ink)] text-[12px]">Logout</a>
|
|
|
|
|
|
|
|
|
|
| 275 |
{% else %}
|
| 276 |
+
<a href="/login" class="mp-btn mp-btn-brass">Log in</a>
|
|
|
|
|
|
|
| 277 |
{% endif %}
|
| 278 |
</nav>
|
| 279 |
</div>
|
| 280 |
</header>
|
| 281 |
+
|
| 282 |
{% block flash %}
|
| 283 |
{% if request.query_params.get('flash') %}
|
| 284 |
<div class="max-w-6xl mx-auto px-6 pt-4">
|
| 285 |
+
<div class="rounded-sm border border-[rgba(47,107,92,0.4)] bg-[rgba(47,107,92,0.10)] p-3 text-sm text-[var(--mp-felt-bright)]">
|
| 286 |
{% set fkey = request.query_params.get('flash') %}
|
| 287 |
{% set fuser = request.query_params.get('u') %}
|
| 288 |
{% if fkey == 'welcome' %}Welcome, @{{ fuser }}. Your account was created.
|
|
|
|
| 295 |
</div>
|
| 296 |
{% endif %}
|
| 297 |
{% endblock %}
|
| 298 |
+
|
| 299 |
+
<main class="max-w-6xl mx-auto px-6 py-10">
|
| 300 |
{% block content %}{% endblock %}
|
| 301 |
</main>
|
| 302 |
+
|
| 303 |
+
<footer class="max-w-6xl mx-auto px-6 py-10 text-center">
|
| 304 |
+
<div class="mp-brass-rule mb-6"></div>
|
| 305 |
+
<div class="mp-eyebrow">Metropolis Chess Club · Phase 3c · est. 2026</div>
|
| 306 |
</footer>
|
| 307 |
</body>
|
| 308 |
</html>
|
|
@@ -9,75 +9,61 @@
|
|
| 9 |
{% endblock %}
|
| 10 |
|
| 11 |
{% block content %}
|
| 12 |
-
{%
|
| 13 |
-
{% set val = r.value if r.value is defined else r %}
|
| 14 |
-
{% if val == 'family' %}
|
| 15 |
-
<span class="text-[10px] uppercase tracking-wider bg-emerald-900/60 text-emerald-200 rounded px-1.5 py-0.5">Family</span>
|
| 16 |
-
{% elif val == 'mature' %}
|
| 17 |
-
<span class="text-[10px] uppercase tracking-wider bg-amber-900/60 text-amber-200 rounded px-1.5 py-0.5">Mature</span>
|
| 18 |
-
{% elif val == 'unrestricted' %}
|
| 19 |
-
<span class="text-[10px] uppercase tracking-wider bg-rose-900/60 text-rose-200 rounded px-1.5 py-0.5">Unrestricted</span>
|
| 20 |
-
{% endif %}
|
| 21 |
-
{% endmacro %}
|
| 22 |
|
| 23 |
-
<div class="flex items-start gap-6">
|
| 24 |
-
<div class="
|
|
|
|
|
|
|
| 25 |
<div class="flex-1">
|
| 26 |
-
<div class="
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
{% endif %}
|
| 31 |
{{ rating_badge(character.content_rating) }}
|
| 32 |
-
{
|
| 33 |
-
<span class="text-[10px] uppercase tracking-wider bg-neutral-700 text-neutral-200 rounded px-1.5 py-0.5">Private</span>
|
| 34 |
-
{% endif %}
|
| 35 |
{% if is_generating %}
|
| 36 |
-
|
| 37 |
{% elif is_failed %}
|
| 38 |
-
|
| 39 |
-
{% else %}
|
| 40 |
-
<span class="text-xs bg-emerald-900/60 text-emerald-200 rounded px-2 py-0.5">Ready</span>
|
| 41 |
{% endif %}
|
| 42 |
</div>
|
| 43 |
-
<p class="text-
|
| 44 |
-
<div class="mt-
|
| 45 |
-
{% if character.is_preset %}
|
| 46 |
-
|
| 47 |
-
{% elif
|
| 48 |
-
Your character
|
| 49 |
-
{% elif owner_username %}
|
| 50 |
-
@{{ owner_username }}'s character
|
| 51 |
{% endif %}
|
| 52 |
</div>
|
| 53 |
-
<div class="mt-
|
| 54 |
-
<span>Current {{ character.current_elo }}
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
| 57 |
</div>
|
|
|
|
|
|
|
|
|
|
| 58 |
</div>
|
| 59 |
-
<div class="flex flex-col gap-2 items-end">
|
| 60 |
{% if not is_generating %}
|
| 61 |
<form method="post" action="/play/{{ character.id }}">
|
| 62 |
-
<button class="
|
| 63 |
-
▶ Play
|
| 64 |
-
</button>
|
| 65 |
</form>
|
| 66 |
{% endif %}
|
| 67 |
{% if is_owner and not character.is_preset %}
|
| 68 |
-
<div class="flex gap-2
|
| 69 |
-
<a href="/characters/{{ character.id }}/edit"
|
| 70 |
-
class="rounded border border-neutral-700 text-neutral-200 px-3 py-1 hover:bg-neutral-800">Edit</a>
|
| 71 |
<form method="post" action="/characters/{{ character.id }}/delete"
|
| 72 |
onsubmit="return confirm('Delete {{ character.name }}? This is a soft delete.');">
|
| 73 |
-
<button class="
|
| 74 |
</form>
|
| 75 |
</div>
|
| 76 |
{% elif not is_owner %}
|
| 77 |
<form method="post" action="/characters/{{ character.id }}/clone">
|
| 78 |
-
<button class="
|
| 79 |
-
Clone
|
| 80 |
-
</button>
|
| 81 |
</form>
|
| 82 |
{% endif %}
|
| 83 |
</div>
|
|
|
|
| 9 |
{% endblock %}
|
| 10 |
|
| 11 |
{% block content %}
|
| 12 |
+
{% from "_partials/_macros.html" import rating_badge, visibility_badge, preset_badge, character_state_badge %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
<div class="flex items-start gap-6 mp-enter mp-enter-1">
|
| 15 |
+
<div class="shrink-0 w-20 h-20 flex items-center justify-center border border-[var(--mp-brass-dim)] bg-[var(--mp-surface-2)] rounded-sm text-5xl leading-none">
|
| 16 |
+
{{ character.avatar_emoji or "♟" }}
|
| 17 |
+
</div>
|
| 18 |
<div class="flex-1">
|
| 19 |
+
<div class="mp-eyebrow mb-2">Character dossier</div>
|
| 20 |
+
<div class="flex items-center gap-2 flex-wrap">
|
| 21 |
+
<h1 class="mp-display text-[36px] leading-[1] tracking-tight">{{ character.name }}</h1>
|
| 22 |
+
{% if character.is_preset %}{{ preset_badge() }}{% endif %}
|
|
|
|
| 23 |
{{ rating_badge(character.content_rating) }}
|
| 24 |
+
{{ visibility_badge(character.visibility) }}
|
|
|
|
|
|
|
| 25 |
{% if is_generating %}
|
| 26 |
+
<span class="mp-chip" style="background: rgba(201,166,107,0.14); color: var(--mp-brass-bright);"><span class="mp-livedot" style="background: var(--mp-brass-bright); box-shadow: 0 0 0 0 rgba(201,166,107,0.55);"></span>Generating</span>
|
| 27 |
{% elif is_failed %}
|
| 28 |
+
<span class="mp-chip" style="background: rgba(168,66,63,0.2); color: #E79E9B;">Generation failed</span>
|
|
|
|
|
|
|
| 29 |
{% endif %}
|
| 30 |
</div>
|
| 31 |
+
<p class="text-[14px] text-[var(--mp-ink-muted)] mt-2">{{ character.short_description }}</p>
|
| 32 |
+
<div class="mt-2 text-[11px] text-[var(--mp-ink-faint)] mp-mono uppercase tracking-[0.12em]">
|
| 33 |
+
{% if character.is_preset %}House character
|
| 34 |
+
{% elif is_owner %}Your character
|
| 35 |
+
{% elif owner_username %}@{{ owner_username }}'s character
|
|
|
|
|
|
|
|
|
|
| 36 |
{% endif %}
|
| 37 |
</div>
|
| 38 |
+
<div class="mt-4 text-[12px] text-[var(--mp-ink-muted)] mp-mono">
|
| 39 |
+
<span class="text-[var(--mp-ink-faint)]">Current</span> {{ character.current_elo }}
|
| 40 |
+
<span class="text-[var(--mp-ink-ghost)] mx-1">·</span>
|
| 41 |
+
<span class="text-[var(--mp-ink-faint)]">floor</span> {{ character.floor_elo }}
|
| 42 |
+
<span class="text-[var(--mp-ink-ghost)] mx-1">·</span>
|
| 43 |
+
<span class="text-[var(--mp-ink-faint)]">max</span> {{ character.max_elo }}
|
| 44 |
+
{% if character.adaptive %}<span class="text-[var(--mp-ink-ghost)] mx-1">·</span><span style="color: var(--mp-felt-bright);">adaptive</span>{% endif %}
|
| 45 |
</div>
|
| 46 |
+
{% if character.voice_descriptor %}
|
| 47 |
+
<div class="mt-2 text-[13px] mp-display italic text-[var(--mp-ink-muted)]">"{{ character.voice_descriptor }}"</div>
|
| 48 |
+
{% endif %}
|
| 49 |
</div>
|
| 50 |
+
<div class="flex flex-col gap-2 items-end shrink-0">
|
| 51 |
{% if not is_generating %}
|
| 52 |
<form method="post" action="/play/{{ character.id }}">
|
| 53 |
+
<button class="mp-btn mp-btn-brass" style="padding: 0.65rem 1.4rem; font-size: 14px;">▶ Play</button>
|
|
|
|
|
|
|
| 54 |
</form>
|
| 55 |
{% endif %}
|
| 56 |
{% if is_owner and not character.is_preset %}
|
| 57 |
+
<div class="flex gap-2">
|
| 58 |
+
<a href="/characters/{{ character.id }}/edit" class="mp-btn mp-btn-ghost">Edit</a>
|
|
|
|
| 59 |
<form method="post" action="/characters/{{ character.id }}/delete"
|
| 60 |
onsubmit="return confirm('Delete {{ character.name }}? This is a soft delete.');">
|
| 61 |
+
<button class="mp-btn mp-btn-danger">Delete</button>
|
| 62 |
</form>
|
| 63 |
</div>
|
| 64 |
{% elif not is_owner %}
|
| 65 |
<form method="post" action="/characters/{{ character.id }}/clone">
|
| 66 |
+
<button class="mp-btn mp-btn-ghost">Clone</button>
|
|
|
|
|
|
|
| 67 |
</form>
|
| 68 |
{% endif %}
|
| 69 |
</div>
|
|
@@ -3,61 +3,63 @@
|
|
| 3 |
{% block title %}Discovery — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
-
<div class="flex items-end justify-between mb-
|
| 7 |
<div>
|
| 8 |
-
<
|
| 9 |
-
<
|
|
|
|
|
|
|
|
|
|
| 10 |
</div>
|
| 11 |
-
<div class="
|
| 12 |
-
<a href="/leaderboard/characters" class="
|
|
|
|
|
|
|
|
|
|
| 13 |
</div>
|
| 14 |
</div>
|
| 15 |
|
| 16 |
-
<section class="mb-
|
| 17 |
-
<div class="flex items-
|
| 18 |
-
<h2 class="
|
| 19 |
-
<span class="text-
|
| 20 |
</div>
|
| 21 |
{% if live_matches %}
|
| 22 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
| 23 |
-
{% for m in live_matches %}
|
| 24 |
-
{% include "_partials/match_row.html" %}
|
| 25 |
-
{% endfor %}
|
| 26 |
</div>
|
| 27 |
{% else %}
|
| 28 |
-
<div class="
|
| 29 |
-
|
|
|
|
| 30 |
</div>
|
| 31 |
{% endif %}
|
| 32 |
</section>
|
| 33 |
|
| 34 |
-
<section class="mb-
|
| 35 |
-
<div class="flex items-
|
| 36 |
-
<h2 class="
|
| 37 |
</div>
|
| 38 |
{% if recent_matches %}
|
| 39 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
| 40 |
-
{% for m in recent_matches %}
|
| 41 |
-
{% include "_partials/match_row.html" %}
|
| 42 |
-
{% endfor %}
|
| 43 |
</div>
|
| 44 |
{% else %}
|
| 45 |
-
<div class="
|
| 46 |
Nothing finished yet.
|
| 47 |
</div>
|
| 48 |
{% endif %}
|
| 49 |
</section>
|
| 50 |
|
| 51 |
-
<section>
|
| 52 |
-
<div class="flex items-
|
| 53 |
-
<h2 class="
|
| 54 |
-
<a href="/characters/new" class="text-
|
| 55 |
</div>
|
| 56 |
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 57 |
-
{% for c in characters %}
|
| 58 |
-
{% include "_partials/character_card.html" %}
|
| 59 |
{% else %}
|
| 60 |
-
<div class="col-span-full text-center py-
|
| 61 |
No characters available at your content rating.
|
| 62 |
</div>
|
| 63 |
{% endfor %}
|
|
|
|
| 3 |
{% block title %}Discovery — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
+
<div class="flex items-end justify-between mb-10 mp-enter mp-enter-1">
|
| 7 |
<div>
|
| 8 |
+
<div class="mp-eyebrow mb-2">The floor tonight</div>
|
| 9 |
+
<h1 class="mp-display text-[44px] leading-[0.95] tracking-tight">Discovery</h1>
|
| 10 |
+
<p class="text-[14px] text-[var(--mp-ink-muted)] mt-3 max-w-lg">
|
| 11 |
+
Live matches right now, recent finishes, and every character you can play.
|
| 12 |
+
</p>
|
| 13 |
</div>
|
| 14 |
+
<div class="text-right">
|
| 15 |
+
<a href="/leaderboard/characters" class="mp-btn mp-btn-ghost">
|
| 16 |
+
Leaderboards
|
| 17 |
+
<span class="text-[var(--mp-brass)]">›</span>
|
| 18 |
+
</a>
|
| 19 |
</div>
|
| 20 |
</div>
|
| 21 |
|
| 22 |
+
<section class="mb-12 mp-enter mp-enter-2">
|
| 23 |
+
<div class="flex items-baseline justify-between mb-4">
|
| 24 |
+
<h2 class="mp-eyebrow">Live right now</h2>
|
| 25 |
+
<span class="mp-mono text-[11px] text-[var(--mp-ink-faint)]">{{ live_matches|length }} match{{ '' if live_matches|length == 1 else 'es' }}</span>
|
| 26 |
</div>
|
| 27 |
{% if live_matches %}
|
| 28 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
| 29 |
+
{% for m in live_matches %}{% include "_partials/match_row.html" %}{% endfor %}
|
|
|
|
|
|
|
| 30 |
</div>
|
| 31 |
{% else %}
|
| 32 |
+
<div class="mp-panel rounded-sm px-4 py-10 text-center">
|
| 33 |
+
<div class="mp-display italic text-[var(--mp-ink-faint)] text-lg">Quiet floor.</div>
|
| 34 |
+
<div class="text-[13px] text-[var(--mp-ink-faint)] mt-1">Start your own match below.</div>
|
| 35 |
</div>
|
| 36 |
{% endif %}
|
| 37 |
</section>
|
| 38 |
|
| 39 |
+
<section class="mb-12 mp-enter mp-enter-3">
|
| 40 |
+
<div class="flex items-baseline justify-between mb-4">
|
| 41 |
+
<h2 class="mp-eyebrow">Recently finished</h2>
|
| 42 |
</div>
|
| 43 |
{% if recent_matches %}
|
| 44 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
| 45 |
+
{% for m in recent_matches %}{% include "_partials/match_row.html" %}{% endfor %}
|
|
|
|
|
|
|
| 46 |
</div>
|
| 47 |
{% else %}
|
| 48 |
+
<div class="mp-panel rounded-sm px-4 py-10 text-center text-sm text-[var(--mp-ink-faint)]">
|
| 49 |
Nothing finished yet.
|
| 50 |
</div>
|
| 51 |
{% endif %}
|
| 52 |
</section>
|
| 53 |
|
| 54 |
+
<section class="mp-enter mp-enter-3">
|
| 55 |
+
<div class="flex items-baseline justify-between mb-4">
|
| 56 |
+
<h2 class="mp-eyebrow">Characters to play</h2>
|
| 57 |
+
<a href="/characters/new" class="text-[11px] mp-mono uppercase tracking-[0.15em] text-[var(--mp-brass)] hover:text-[var(--mp-brass-bright)]">+ New character</a>
|
| 58 |
</div>
|
| 59 |
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 60 |
+
{% for c in characters %}{% include "_partials/character_card.html" %}
|
|
|
|
| 61 |
{% else %}
|
| 62 |
+
<div class="col-span-full text-center py-10 text-[var(--mp-ink-faint)]">
|
| 63 |
No characters available at your content rating.
|
| 64 |
</div>
|
| 65 |
{% endfor %}
|
|
@@ -3,7 +3,8 @@
|
|
| 3 |
{% block title %}Edit {{ character.name }} — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
-
<
|
|
|
|
| 7 |
<p class="text-sm text-neutral-400 mb-6">
|
| 8 |
Editing the backstory does <strong>not</strong> regenerate memories — that's a destructive
|
| 9 |
operation. If you change the backstory meaningfully and want memories to match, run
|
|
@@ -103,7 +104,7 @@
|
|
| 103 |
</div>
|
| 104 |
|
| 105 |
<div class="pt-2">
|
| 106 |
-
<button class="
|
| 107 |
Save
|
| 108 |
</button>
|
| 109 |
<a href="/characters/{{ character.id }}" class="ml-3 text-sm text-neutral-400 hover:text-white">Cancel</a>
|
|
|
|
| 3 |
{% block title %}Edit {{ character.name }} — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
+
<div class="mp-eyebrow mb-2">Revising</div>
|
| 7 |
+
<h1 class="mp-display text-[36px] leading-[1] tracking-tight mb-2">Edit {{ character.name }}</h1>
|
| 8 |
<p class="text-sm text-neutral-400 mb-6">
|
| 9 |
Editing the backstory does <strong>not</strong> regenerate memories — that's a destructive
|
| 10 |
operation. If you change the backstory meaningfully and want memories to match, run
|
|
|
|
| 104 |
</div>
|
| 105 |
|
| 106 |
<div class="pt-2">
|
| 107 |
+
<button class="mp-btn mp-btn-brass" style="padding: 0.6rem 1.2rem;">
|
| 108 |
Save
|
| 109 |
</button>
|
| 110 |
<a href="/characters/{{ character.id }}" class="ml-3 text-sm text-neutral-400 hover:text-white">Cancel</a>
|
|
@@ -1,23 +1,30 @@
|
|
| 1 |
{% extends "base.html" %}
|
| 2 |
|
| 3 |
{% block content %}
|
| 4 |
-
<div class="flex items-end justify-between mb-
|
| 5 |
<div>
|
| 6 |
-
<
|
| 7 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
</div>
|
| 9 |
-
<a href="/discovery"
|
| 10 |
-
|
| 11 |
-
|
| 12 |
</a>
|
| 13 |
</div>
|
| 14 |
|
| 15 |
-
<div class="
|
|
|
|
|
|
|
| 16 |
{% for c in characters %}
|
| 17 |
{% include "_partials/character_card.html" %}
|
| 18 |
{% else %}
|
| 19 |
-
<div class="col-span-full text-center py-
|
| 20 |
-
|
|
|
|
| 21 |
</div>
|
| 22 |
{% endfor %}
|
| 23 |
</div>
|
|
|
|
| 1 |
{% extends "base.html" %}
|
| 2 |
|
| 3 |
{% block content %}
|
| 4 |
+
<div class="flex items-end justify-between mb-10 mp-enter mp-enter-1">
|
| 5 |
<div>
|
| 6 |
+
<div class="mp-eyebrow mb-2">Members' roster</div>
|
| 7 |
+
<h1 class="mp-display text-[44px] leading-[0.95] tracking-tight">Characters</h1>
|
| 8 |
+
<p class="text-[14px] text-[var(--mp-ink-muted)] mt-3 max-w-md">
|
| 9 |
+
Pick one to inspect, or create your own opponent. Each has a backstory, a voice,
|
| 10 |
+
and a running Elo — they remember what happened last time.
|
| 11 |
+
</p>
|
| 12 |
</div>
|
| 13 |
+
<a href="/discovery" class="mp-btn mp-btn-ghost shrink-0">
|
| 14 |
+
Browse live matches
|
| 15 |
+
<span class="text-[var(--mp-brass)]">›</span>
|
| 16 |
</a>
|
| 17 |
</div>
|
| 18 |
|
| 19 |
+
<div class="mp-brass-rule mb-6 mp-enter mp-enter-2"></div>
|
| 20 |
+
|
| 21 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mp-enter mp-enter-3">
|
| 22 |
{% for c in characters %}
|
| 23 |
{% include "_partials/character_card.html" %}
|
| 24 |
{% else %}
|
| 25 |
+
<div class="col-span-full text-center py-16 text-[var(--mp-ink-faint)]">
|
| 26 |
+
<div class="mp-display text-2xl italic mb-3">An empty roster.</div>
|
| 27 |
+
<a href="/characters/new" class="text-[var(--mp-brass)] hover:text-[var(--mp-brass-bright)]">Draft the first character →</a>
|
| 28 |
</div>
|
| 29 |
{% endfor %}
|
| 30 |
</div>
|
|
@@ -3,17 +3,29 @@
|
|
| 3 |
{% block title %}Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
-
<div class="max-w-
|
| 7 |
-
|
| 8 |
-
<
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
</p>
|
| 12 |
-
<p class="text-
|
| 13 |
-
Pick one of our
|
| 14 |
</p>
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
</a>
|
| 18 |
</div>
|
| 19 |
{% endblock %}
|
|
|
|
| 3 |
{% block title %}Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
+
<div class="max-w-3xl mx-auto text-center py-20 mp-enter mp-enter-1 relative">
|
| 7 |
+
{# Decorative monogram ring. #}
|
| 8 |
+
<div class="inline-flex items-center justify-center w-20 h-20 mb-8 relative">
|
| 9 |
+
<div class="absolute inset-0 border border-[var(--mp-brass-dim)] rounded-sm"></div>
|
| 10 |
+
<div class="absolute inset-1.5 border border-[var(--mp-hairline-2)] rounded-sm"></div>
|
| 11 |
+
<span class="mp-display text-[var(--mp-brass-bright)] text-4xl">M</span>
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
<div class="mp-eyebrow mb-3">Established 2026</div>
|
| 15 |
+
<h1 class="mp-display text-[56px] leading-[0.95] tracking-tight mb-3">Metropolis <span class="mp-italic text-[var(--mp-brass)]">Chess Club</span></h1>
|
| 16 |
+
|
| 17 |
+
<div class="mp-brass-rule max-w-md mx-auto mb-7"></div>
|
| 18 |
+
|
| 19 |
+
<p class="text-[17px] text-[var(--mp-ink-muted)] mb-3 max-w-xl mx-auto leading-relaxed">
|
| 20 |
+
Play chess against AI characters with <span class="mp-italic text-[var(--mp-ink)]">backstories</span>, personalities, and memories.
|
| 21 |
</p>
|
| 22 |
+
<p class="text-[14px] text-[var(--mp-ink-faint)] mb-10 max-w-lg mx-auto">
|
| 23 |
+
Pick one of our house characters or craft your own. They remember your games.
|
| 24 |
</p>
|
| 25 |
+
|
| 26 |
+
<a href="/login" class="mp-btn mp-btn-brass text-[14px]" style="padding: 0.75rem 1.5rem;">
|
| 27 |
+
Log in to get started
|
| 28 |
+
<span>›</span>
|
| 29 |
</a>
|
| 30 |
</div>
|
| 31 |
{% endblock %}
|
|
@@ -2,54 +2,66 @@
|
|
| 2 |
{% block title %}Character Leaderboard — Metropolis Chess Club{% endblock %}
|
| 3 |
|
| 4 |
{% block content %}
|
| 5 |
-
<div class="flex items-end justify-between mb-
|
| 6 |
<div>
|
| 7 |
-
<
|
| 8 |
-
<
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
<
|
| 12 |
-
<a href="/leaderboard/characters?window=30d" class="font-medium {% if window == '30d' %}text-white{% else %}text-neutral-500 hover:text-white{% endif %}">30 days</a>
|
| 13 |
-
<a href="/leaderboard/characters?window=7d" class="font-medium {% if window == '7d' %}text-white{% else %}text-neutral-500 hover:text-white{% endif %}">7 days</a>
|
| 14 |
-
<span class="text-neutral-700">|</span>
|
| 15 |
-
<a href="/leaderboard/players" class="text-neutral-400 hover:text-white">Players →</a>
|
| 16 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</div>
|
| 18 |
|
| 19 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
| 20 |
<table class="w-full text-sm">
|
| 21 |
-
<thead
|
| 22 |
-
<tr>
|
| 23 |
-
<th class="text-left
|
| 24 |
-
<th class="text-left
|
| 25 |
-
<th class="text-right
|
| 26 |
-
<th class="text-right
|
| 27 |
-
<th class="text-right
|
| 28 |
-
<th class="text-right
|
| 29 |
-
<th class="text-right
|
| 30 |
-
<th class="text-right
|
| 31 |
</tr>
|
| 32 |
</thead>
|
| 33 |
-
<tbody
|
| 34 |
{% for r in rows %}
|
| 35 |
-
<tr class="hover:bg-
|
| 36 |
-
<td class="px-4 py-3 text-
|
|
|
|
|
|
|
| 37 |
<td class="px-4 py-3">
|
| 38 |
-
<a href="/characters/{{ r.character_id }}" class="flex items-center gap-3 hover:text-
|
| 39 |
-
<span class="text-
|
| 40 |
-
<span class="
|
| 41 |
</a>
|
| 42 |
</td>
|
| 43 |
-
<td class="px-4 py-3 text-right
|
| 44 |
-
<td class="px-4 py-3 text-right
|
| 45 |
-
<td class="px-4 py-3 text-right
|
| 46 |
-
<td class="px-4 py-3 text-right
|
| 47 |
-
<td class="px-4 py-3 text-right
|
| 48 |
-
<td class="px-4 py-3 text-right
|
| 49 |
</tr>
|
| 50 |
{% else %}
|
| 51 |
-
<tr><td colspan="8" class="px-4 py-
|
| 52 |
-
|
|
|
|
| 53 |
</td></tr>
|
| 54 |
{% endfor %}
|
| 55 |
</tbody>
|
|
|
|
| 2 |
{% block title %}Character Leaderboard — Metropolis Chess Club{% endblock %}
|
| 3 |
|
| 4 |
{% block content %}
|
| 5 |
+
<div class="flex items-end justify-between mb-8 mp-enter mp-enter-1">
|
| 6 |
<div>
|
| 7 |
+
<div class="mp-eyebrow mb-2">Standings · House</div>
|
| 8 |
+
<h1 class="mp-display text-[40px] leading-[1] tracking-tight">Character leaderboard</h1>
|
| 9 |
+
<p class="text-[13px] text-[var(--mp-ink-muted)] mt-3 max-w-lg">
|
| 10 |
+
Ranked by win rate against all players. Minimum 5 matches within the selected window.
|
| 11 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
</div>
|
| 13 |
+
<nav class="flex items-center gap-2 text-[11px] mp-mono uppercase tracking-[0.15em] shrink-0">
|
| 14 |
+
<a href="/leaderboard/characters" class="px-3 py-1 border-b border-transparent
|
| 15 |
+
{% if window == 'all' %}text-[var(--mp-brass)] border-[var(--mp-brass)]{% else %}text-[var(--mp-ink-faint)] hover:text-[var(--mp-ink)]{% endif %}">All time</a>
|
| 16 |
+
<a href="/leaderboard/characters?window=30d" class="px-3 py-1 border-b border-transparent
|
| 17 |
+
{% if window == '30d' %}text-[var(--mp-brass)] border-[var(--mp-brass)]{% else %}text-[var(--mp-ink-faint)] hover:text-[var(--mp-ink)]{% endif %}">30 days</a>
|
| 18 |
+
<a href="/leaderboard/characters?window=7d" class="px-3 py-1 border-b border-transparent
|
| 19 |
+
{% if window == '7d' %}text-[var(--mp-brass)] border-[var(--mp-brass)]{% else %}text-[var(--mp-ink-faint)] hover:text-[var(--mp-ink)]{% endif %}">7 days</a>
|
| 20 |
+
<span class="mx-2 text-[var(--mp-ink-ghost)]">·</span>
|
| 21 |
+
<a href="/leaderboard/players" class="text-[var(--mp-ink-muted)] hover:text-[var(--mp-ink)]">Players ›</a>
|
| 22 |
+
</nav>
|
| 23 |
</div>
|
| 24 |
|
| 25 |
+
<div class="mp-brass-rule mb-6 mp-enter mp-enter-2"></div>
|
| 26 |
+
|
| 27 |
+
<div class="mp-panel rounded-sm overflow-hidden mp-framed mp-enter mp-enter-2">
|
| 28 |
+
<span class="mp-frame-tl"></span><span class="mp-frame-br"></span>
|
| 29 |
<table class="w-full text-sm">
|
| 30 |
+
<thead>
|
| 31 |
+
<tr class="text-left" style="background: var(--mp-surface-2); border-bottom: 1px solid var(--mp-hairline-2);">
|
| 32 |
+
<th class="mp-eyebrow text-left px-4 py-3 w-12">#</th>
|
| 33 |
+
<th class="mp-eyebrow text-left px-4 py-3">Character</th>
|
| 34 |
+
<th class="mp-eyebrow text-right px-4 py-3">Elo</th>
|
| 35 |
+
<th class="mp-eyebrow text-right px-4 py-3">Win %</th>
|
| 36 |
+
<th class="mp-eyebrow text-right px-4 py-3">W</th>
|
| 37 |
+
<th class="mp-eyebrow text-right px-4 py-3">L</th>
|
| 38 |
+
<th class="mp-eyebrow text-right px-4 py-3">D</th>
|
| 39 |
+
<th class="mp-eyebrow text-right px-4 py-3">Total</th>
|
| 40 |
</tr>
|
| 41 |
</thead>
|
| 42 |
+
<tbody>
|
| 43 |
{% for r in rows %}
|
| 44 |
+
<tr class="transition hover:bg-[var(--mp-surface-3)]" style="border-top: 1px solid var(--mp-hairline);">
|
| 45 |
+
<td class="px-4 py-3 mp-mono text-[var(--mp-ink-faint)]">
|
| 46 |
+
<span class="mp-display {% if r.rank <= 3 %}text-[var(--mp-brass-bright)]{% endif %}">{{ r.rank }}</span>
|
| 47 |
+
</td>
|
| 48 |
<td class="px-4 py-3">
|
| 49 |
+
<a href="/characters/{{ r.character_id }}" class="flex items-center gap-3 hover:text-[var(--mp-brass-bright)] transition">
|
| 50 |
+
<span class="w-9 h-9 inline-flex items-center justify-center border border-[var(--mp-hairline-2)] bg-[var(--mp-surface-2)] rounded-sm text-lg">{{ r.character_avatar }}</span>
|
| 51 |
+
<span class="mp-display text-[16px]">{{ r.character_name }}</span>
|
| 52 |
</a>
|
| 53 |
</td>
|
| 54 |
+
<td class="px-4 py-3 text-right mp-mono text-[var(--mp-ink-muted)]">{{ r.current_elo }}</td>
|
| 55 |
+
<td class="px-4 py-3 text-right mp-mono font-semibold" style="color: var(--mp-felt-bright);">{{ "%.1f"|format(r.win_rate * 100) }}%</td>
|
| 56 |
+
<td class="px-4 py-3 text-right mp-mono" style="color: var(--mp-felt-bright);">{{ r.wins }}</td>
|
| 57 |
+
<td class="px-4 py-3 text-right mp-mono" style="color: #E79E9B;">{{ r.losses }}</td>
|
| 58 |
+
<td class="px-4 py-3 text-right mp-mono text-[var(--mp-ink-faint)]">{{ r.draws }}</td>
|
| 59 |
+
<td class="px-4 py-3 text-right mp-mono text-[var(--mp-ink-muted)]">{{ r.total_matches }}</td>
|
| 60 |
</tr>
|
| 61 |
{% else %}
|
| 62 |
+
<tr><td colspan="8" class="px-4 py-14 text-center">
|
| 63 |
+
<div class="mp-display italic text-[var(--mp-ink-faint)] text-lg">The standings are quiet.</div>
|
| 64 |
+
<div class="text-[12px] text-[var(--mp-ink-faint)] mt-1">No characters have reached 5 matches in this window yet.</div>
|
| 65 |
</td></tr>
|
| 66 |
{% endfor %}
|
| 67 |
</tbody>
|
|
@@ -2,53 +2,70 @@
|
|
| 2 |
{% block title %}Player Leaderboard — Metropolis Chess Club{% endblock %}
|
| 3 |
|
| 4 |
{% block content %}
|
| 5 |
-
<div class="flex items-end justify-between mb-
|
| 6 |
<div>
|
| 7 |
-
<
|
| 8 |
-
<
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
<
|
| 12 |
-
<a href="/leaderboard/players?window=30d" class="font-medium {% if window == '30d' %}text-white{% else %}text-neutral-500 hover:text-white{% endif %}">30 days</a>
|
| 13 |
-
<a href="/leaderboard/players?window=7d" class="font-medium {% if window == '7d' %}text-white{% else %}text-neutral-500 hover:text-white{% endif %}">7 days</a>
|
| 14 |
-
<span class="text-neutral-700">|</span>
|
| 15 |
-
<a href="/leaderboard/characters" class="text-neutral-400 hover:text-white">Characters →</a>
|
| 16 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</div>
|
| 18 |
|
| 19 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
| 20 |
<table class="w-full text-sm">
|
| 21 |
-
<thead
|
| 22 |
-
<tr>
|
| 23 |
-
<th class="text-left
|
| 24 |
-
<th class="text-left
|
| 25 |
-
<th class="text-right
|
| 26 |
-
<th class="text-right
|
| 27 |
-
<th class="text-right
|
| 28 |
-
<th class="text-right
|
| 29 |
-
<th class="text-right
|
| 30 |
</tr>
|
| 31 |
</thead>
|
| 32 |
-
<tbody
|
| 33 |
{% for r in rows %}
|
| 34 |
-
<tr class="hover:bg-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 36 |
<td class="px-4 py-3">
|
| 37 |
-
<a href="/players/{{ r.username }}" class="
|
| 38 |
-
<span class="
|
| 39 |
-
{% if r.display_name and r.display_name != r.username %}
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
</a>
|
| 42 |
</td>
|
| 43 |
-
<td class="px-4 py-3 text-right
|
| 44 |
-
<td class="px-4 py-3 text-right
|
| 45 |
-
<td class="px-4 py-3 text-right
|
| 46 |
-
<td class="px-4 py-3 text-right
|
| 47 |
-
<td class="px-4 py-3 text-right
|
| 48 |
</tr>
|
| 49 |
{% else %}
|
| 50 |
-
<tr><td colspan="7" class="px-4 py-
|
| 51 |
-
|
|
|
|
| 52 |
</td></tr>
|
| 53 |
{% endfor %}
|
| 54 |
</tbody>
|
|
|
|
| 2 |
{% block title %}Player Leaderboard — Metropolis Chess Club{% endblock %}
|
| 3 |
|
| 4 |
{% block content %}
|
| 5 |
+
<div class="flex items-end justify-between mb-8 mp-enter mp-enter-1">
|
| 6 |
<div>
|
| 7 |
+
<div class="mp-eyebrow mb-2">Standings · Members</div>
|
| 8 |
+
<h1 class="mp-display text-[40px] leading-[1] tracking-tight">Player leaderboard</h1>
|
| 9 |
+
<p class="text-[13px] text-[var(--mp-ink-muted)] mt-3 max-w-lg">
|
| 10 |
+
Ranked by win rate against all characters. Minimum 5 matches within the selected window.
|
| 11 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
</div>
|
| 13 |
+
<nav class="flex items-center gap-2 text-[11px] mp-mono uppercase tracking-[0.15em] shrink-0">
|
| 14 |
+
<a href="/leaderboard/players" class="px-3 py-1 border-b border-transparent
|
| 15 |
+
{% if window == 'all' %}text-[var(--mp-brass)] border-[var(--mp-brass)]{% else %}text-[var(--mp-ink-faint)] hover:text-[var(--mp-ink)]{% endif %}">All time</a>
|
| 16 |
+
<a href="/leaderboard/players?window=30d" class="px-3 py-1 border-b border-transparent
|
| 17 |
+
{% if window == '30d' %}text-[var(--mp-brass)] border-[var(--mp-brass)]{% else %}text-[var(--mp-ink-faint)] hover:text-[var(--mp-ink)]{% endif %}">30 days</a>
|
| 18 |
+
<a href="/leaderboard/players?window=7d" class="px-3 py-1 border-b border-transparent
|
| 19 |
+
{% if window == '7d' %}text-[var(--mp-brass)] border-[var(--mp-brass)]{% else %}text-[var(--mp-ink-faint)] hover:text-[var(--mp-ink)]{% endif %}">7 days</a>
|
| 20 |
+
<span class="mx-2 text-[var(--mp-ink-ghost)]">·</span>
|
| 21 |
+
<a href="/leaderboard/characters" class="text-[var(--mp-ink-muted)] hover:text-[var(--mp-ink)]">Characters ›</a>
|
| 22 |
+
</nav>
|
| 23 |
</div>
|
| 24 |
|
| 25 |
+
<div class="mp-brass-rule mb-6 mp-enter mp-enter-2"></div>
|
| 26 |
+
|
| 27 |
+
<div class="mp-panel rounded-sm overflow-hidden mp-framed mp-enter mp-enter-2">
|
| 28 |
+
<span class="mp-frame-tl"></span><span class="mp-frame-br"></span>
|
| 29 |
<table class="w-full text-sm">
|
| 30 |
+
<thead>
|
| 31 |
+
<tr class="text-left" style="background: var(--mp-surface-2); border-bottom: 1px solid var(--mp-hairline-2);">
|
| 32 |
+
<th class="mp-eyebrow text-left px-4 py-3 w-12">#</th>
|
| 33 |
+
<th class="mp-eyebrow text-left px-4 py-3">Player</th>
|
| 34 |
+
<th class="mp-eyebrow text-right px-4 py-3">Win %</th>
|
| 35 |
+
<th class="mp-eyebrow text-right px-4 py-3">W</th>
|
| 36 |
+
<th class="mp-eyebrow text-right px-4 py-3">L</th>
|
| 37 |
+
<th class="mp-eyebrow text-right px-4 py-3">D</th>
|
| 38 |
+
<th class="mp-eyebrow text-right px-4 py-3">Total</th>
|
| 39 |
</tr>
|
| 40 |
</thead>
|
| 41 |
+
<tbody>
|
| 42 |
{% for r in rows %}
|
| 43 |
+
<tr class="transition hover:bg-[var(--mp-surface-3)]"
|
| 44 |
+
style="border-top: 1px solid var(--mp-hairline); {% if r.player_id == player.id %}background: rgba(201,166,107,0.08);{% endif %}">
|
| 45 |
+
<td class="px-4 py-3 mp-mono text-[var(--mp-ink-faint)]">
|
| 46 |
+
<span class="mp-display {% if r.rank <= 3 %}text-[var(--mp-brass-bright)]{% endif %}">{{ r.rank }}</span>
|
| 47 |
+
</td>
|
| 48 |
<td class="px-4 py-3">
|
| 49 |
+
<a href="/players/{{ r.username }}" class="hover:text-[var(--mp-brass-bright)] transition">
|
| 50 |
+
<span class="mp-mono text-[13px]">@{{ r.username }}</span>
|
| 51 |
+
{% if r.display_name and r.display_name != r.username %}
|
| 52 |
+
<span class="text-[var(--mp-ink-faint)] text-[12px] ml-2">{{ r.display_name }}</span>
|
| 53 |
+
{% endif %}
|
| 54 |
+
{% if r.player_id == player.id %}
|
| 55 |
+
<span class="mp-chip ml-2" style="background: rgba(201,166,107,0.15); color: var(--mp-brass-bright);">you</span>
|
| 56 |
+
{% endif %}
|
| 57 |
</a>
|
| 58 |
</td>
|
| 59 |
+
<td class="px-4 py-3 text-right mp-mono font-semibold" style="color: var(--mp-felt-bright);">{{ "%.1f"|format(r.win_rate * 100) }}%</td>
|
| 60 |
+
<td class="px-4 py-3 text-right mp-mono" style="color: var(--mp-felt-bright);">{{ r.wins }}</td>
|
| 61 |
+
<td class="px-4 py-3 text-right mp-mono" style="color: #E79E9B;">{{ r.losses }}</td>
|
| 62 |
+
<td class="px-4 py-3 text-right mp-mono text-[var(--mp-ink-faint)]">{{ r.draws }}</td>
|
| 63 |
+
<td class="px-4 py-3 text-right mp-mono text-[var(--mp-ink-muted)]">{{ r.total_matches }}</td>
|
| 64 |
</tr>
|
| 65 |
{% else %}
|
| 66 |
+
<tr><td colspan="7" class="px-4 py-14 text-center">
|
| 67 |
+
<div class="mp-display italic text-[var(--mp-ink-faint)] text-lg">No members have posted a full card yet.</div>
|
| 68 |
+
<div class="text-[12px] text-[var(--mp-ink-faint)] mt-1">Five matches within the window qualifies you.</div>
|
| 69 |
</td></tr>
|
| 70 |
{% endfor %}
|
| 71 |
</tbody>
|
|
@@ -3,14 +3,15 @@
|
|
| 3 |
{% block title %}Log in — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
-
<div class="max-w-md mx-auto mt-
|
| 7 |
-
<
|
| 8 |
-
<
|
| 9 |
-
|
|
|
|
| 10 |
</p>
|
| 11 |
|
| 12 |
{% if error %}
|
| 13 |
-
<div class="mb-
|
| 14 |
{% if error == 'empty' %}Username can't be blank.
|
| 15 |
{% elif error == 'too_short' %}Username must be at least 3 characters.
|
| 16 |
{% elif error == 'too_long' %}Username must be 24 characters or fewer.
|
|
@@ -20,30 +21,30 @@
|
|
| 20 |
</div>
|
| 21 |
{% endif %}
|
| 22 |
|
| 23 |
-
<form method="post" action="/login" class="space-y-
|
|
|
|
| 24 |
<input type="hidden" name="next" value="{{ next or '/' }}" />
|
| 25 |
<div>
|
| 26 |
-
<label class="
|
| 27 |
<input required autofocus name="username" value="{{ prefill }}"
|
| 28 |
minlength="3" maxlength="24" pattern="[a-z0-9_]+"
|
| 29 |
-
class="w-full
|
| 30 |
placeholder="your_username" />
|
| 31 |
-
<p class="text-
|
| 32 |
Lowercase letters, digits, underscore. 3–24 chars.
|
| 33 |
</p>
|
| 34 |
</div>
|
| 35 |
-
<button class="
|
| 36 |
-
Continue
|
| 37 |
</button>
|
| 38 |
</form>
|
| 39 |
|
| 40 |
-
<div class="mt-8
|
| 41 |
-
|
| 42 |
-
<
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
</p>
|
| 47 |
</div>
|
| 48 |
</div>
|
| 49 |
{% endblock %}
|
|
|
|
| 3 |
{% block title %}Log in — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
+
<div class="max-w-md mx-auto mt-10 mp-enter mp-enter-1">
|
| 7 |
+
<div class="mp-eyebrow mb-2">Members' entrance</div>
|
| 8 |
+
<h1 class="mp-display text-[36px] leading-[1] tracking-tight mb-2">Log in</h1>
|
| 9 |
+
<p class="text-[13px] text-[var(--mp-ink-muted)] mb-6">
|
| 10 |
+
Pick a username to get started. If it's new, we'll open an account.
|
| 11 |
</p>
|
| 12 |
|
| 13 |
{% if error %}
|
| 14 |
+
<div class="mb-5 rounded-sm border p-3 text-sm" style="border-color: rgba(168,66,63,0.45); background: rgba(168,66,63,0.08); color: #E79E9B;">
|
| 15 |
{% if error == 'empty' %}Username can't be blank.
|
| 16 |
{% elif error == 'too_short' %}Username must be at least 3 characters.
|
| 17 |
{% elif error == 'too_long' %}Username must be 24 characters or fewer.
|
|
|
|
| 21 |
</div>
|
| 22 |
{% endif %}
|
| 23 |
|
| 24 |
+
<form method="post" action="/login" class="space-y-5 mp-panel rounded-sm p-6 mp-framed relative">
|
| 25 |
+
<span class="mp-frame-tl"></span><span class="mp-frame-br"></span>
|
| 26 |
<input type="hidden" name="next" value="{{ next or '/' }}" />
|
| 27 |
<div>
|
| 28 |
+
<label class="mp-eyebrow block mb-2">Username</label>
|
| 29 |
<input required autofocus name="username" value="{{ prefill }}"
|
| 30 |
minlength="3" maxlength="24" pattern="[a-z0-9_]+"
|
| 31 |
+
class="mp-input w-full mp-mono"
|
| 32 |
placeholder="your_username" />
|
| 33 |
+
<p class="text-[11px] text-[var(--mp-ink-faint)] mt-2">
|
| 34 |
Lowercase letters, digits, underscore. 3–24 chars.
|
| 35 |
</p>
|
| 36 |
</div>
|
| 37 |
+
<button class="mp-btn mp-btn-brass w-full justify-center" style="padding: 0.65rem 1rem;">
|
| 38 |
+
Continue ›
|
| 39 |
</button>
|
| 40 |
</form>
|
| 41 |
|
| 42 |
+
<div class="mt-8 mp-panel rounded-sm p-4 text-[11px] text-[var(--mp-ink-muted)] leading-relaxed"
|
| 43 |
+
style="border-color: rgba(201,166,107,0.28);">
|
| 44 |
+
<div class="mp-eyebrow mb-2" style="color: var(--mp-brass);">Heads up</div>
|
| 45 |
+
Usernames are claimed on first login. <span class="mp-italic">There's no password</span> —
|
| 46 |
+
anyone with your username can log in as you. This will change in a future version. Don't use this
|
| 47 |
+
for anything you actually care about.
|
|
|
|
| 48 |
</div>
|
| 49 |
</div>
|
| 50 |
{% endblock %}
|
|
@@ -3,7 +3,8 @@
|
|
| 3 |
{% block title %}New character — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
-
<
|
|
|
|
| 7 |
<p class="text-sm text-neutral-400 mb-6">
|
| 8 |
We'll generate their memories from the backstory in the background. Takes ~30–60s.
|
| 9 |
</p>
|
|
@@ -132,7 +133,7 @@
|
|
| 132 |
</div>
|
| 133 |
|
| 134 |
<div class="pt-2">
|
| 135 |
-
<button class="
|
| 136 |
Create character
|
| 137 |
</button>
|
| 138 |
<a href="/" class="ml-3 text-sm text-neutral-400 hover:text-white">Cancel</a>
|
|
|
|
| 3 |
{% block title %}New character — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
+
<div class="mp-eyebrow mb-2">Drafting</div>
|
| 7 |
+
<h1 class="mp-display text-[36px] leading-[1] tracking-tight mb-2">New character</h1>
|
| 8 |
<p class="text-sm text-neutral-400 mb-6">
|
| 9 |
We'll generate their memories from the backstory in the background. Takes ~30–60s.
|
| 10 |
</p>
|
|
|
|
| 133 |
</div>
|
| 134 |
|
| 135 |
<div class="pt-2">
|
| 136 |
+
<button class="mp-btn mp-btn-brass" style="padding: 0.6rem 1.2rem;">
|
| 137 |
Create character
|
| 138 |
</button>
|
| 139 |
<a href="/" class="ml-3 text-sm text-neutral-400 hover:text-white">Cancel</a>
|
|
@@ -9,140 +9,146 @@
|
|
| 9 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/chessboard-js/1.0.0/chessboard-1.0.0.min.js"></script>
|
| 10 |
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
| 11 |
<style>
|
| 12 |
-
|
| 13 |
-
.black-3c85d { background-color: #525866 !important; }
|
| 14 |
-
.highlight-last { box-shadow: inset 0 0 0 3px rgba(16, 185, 129, 0.8); }
|
| 15 |
-
|
| 16 |
.memory-ribbon { transition: opacity 0.8s ease-in-out; }
|
| 17 |
.memory-ribbon.fading { opacity: 0.35; }
|
| 18 |
.memory-detail { display: none; }
|
| 19 |
.memory-item:hover .memory-detail { display: block; }
|
| 20 |
.memory-item.expanded .memory-detail { display: block; }
|
|
|
|
| 21 |
|
| 22 |
-
|
| 23 |
-
background: linear-gradient(180deg, rgba(71,85,105,0.35), rgba(51,65,85,0.25));
|
| 24 |
-
border-left: 2px solid #60a5fa;
|
| 25 |
-
}
|
| 26 |
-
.chat-bubble-player {
|
| 27 |
-
background: linear-gradient(180deg, rgba(29,78,216,0.2), rgba(29,78,216,0.1));
|
| 28 |
-
border-left: 2px solid #f59e0b;
|
| 29 |
-
margin-left: 1.5rem;
|
| 30 |
-
}
|
| 31 |
-
.chat-bubble-player.in-transit { opacity: 0.6; }
|
| 32 |
-
|
| 33 |
-
/* Connection status pill */
|
| 34 |
-
.conn-live { background: rgba(16, 185, 129, 0.18); color: #6ee7b7; }
|
| 35 |
-
.conn-retry { background: rgba(245, 158, 11, 0.18); color: #fcd34d; }
|
| 36 |
-
.conn-lost { background: rgba(244, 63, 94, 0.18); color: #fda4af; }
|
| 37 |
-
|
| 38 |
-
/* Disconnect overlay */
|
| 39 |
#disconnect-overlay {
|
| 40 |
-
position: fixed; inset: 0; background: rgba(
|
| 41 |
z-index: 50; display: none; align-items: center; justify-content: center;
|
|
|
|
| 42 |
}
|
| 43 |
#disconnect-overlay.visible { display: flex; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
</style>
|
| 45 |
{% endblock %}
|
| 46 |
|
| 47 |
{% block content %}
|
| 48 |
|
| 49 |
-
<div id="alert-banner" class="hidden mb-5 rounded border px-4 py-3 text-sm"></div>
|
| 50 |
|
| 51 |
-
<div class="flex items-start gap-6 mb-
|
| 52 |
-
<div class="
|
|
|
|
|
|
|
| 53 |
<div class="flex-1">
|
| 54 |
-
<
|
| 55 |
-
<
|
|
|
|
| 56 |
{{ character.short_description }}
|
| 57 |
-
· playing at ~{{ match.character_elo_at_start }} Elo
|
| 58 |
-
· you're <span id="player-color">{{ match.player_color.value }}</span>
|
| 59 |
-
</p>
|
| 60 |
-
<p class="text-xs text-neutral-500 mt-1">
|
| 61 |
-
mood: <span id="emotion-indicator" class="font-medium text-neutral-300">—</span>
|
| 62 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
</div>
|
| 64 |
-
<div class="text-right text-sm">
|
| 65 |
-
<div id="match-status" class="
|
| 66 |
{% if match.status.value == "in_progress" %}
|
| 67 |
-
<span class="text-
|
| 68 |
{% elif match.status.value == "completed" %}
|
| 69 |
-
<span class="text-
|
| 70 |
{% else %}
|
| 71 |
-
<span class="text-
|
| 72 |
{% endif %}
|
| 73 |
</div>
|
| 74 |
-
<div class="text-
|
| 75 |
{{ match.result.value if match.result else "" }}
|
| 76 |
</div>
|
| 77 |
-
<div id="conn-pill" class="
|
| 78 |
connecting…
|
| 79 |
</div>
|
| 80 |
-
<div id="spectator-count-pill" class="hidden mt-
|
| 81 |
<span id="spectator-count">0</span> watching
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
</div>
|
| 85 |
|
| 86 |
-
<div class="grid grid-cols-1 lg:grid-cols-[minmax(0,
|
| 87 |
<div>
|
| 88 |
-
<div
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
| 93 |
</div>
|
| 94 |
</div>
|
| 95 |
-
<div id="memory-ribbon" class="hidden mt-
|
| 96 |
-
|
| 97 |
-
<
|
|
|
|
| 98 |
</div>
|
| 99 |
</div>
|
| 100 |
|
| 101 |
-
<div class="flex flex-col gap-
|
| 102 |
<div>
|
| 103 |
-
<h2 class="
|
| 104 |
-
<div id="chat-log" class="
|
| 105 |
-
<div class="text-
|
|
|
|
|
|
|
| 106 |
</div>
|
| 107 |
-
<div class="mt-2
|
| 108 |
-
<input id="chat-input" type="text" maxlength="500"
|
| 109 |
-
|
|
|
|
| 110 |
</div>
|
| 111 |
</div>
|
| 112 |
|
| 113 |
<div id="crowd-panel" class="hidden">
|
| 114 |
<div class="flex items-center justify-between mb-2">
|
| 115 |
-
<h2 class="
|
| 116 |
-
<button id="crowd-mute-btn" class="text-[10px] text-
|
| 117 |
</div>
|
| 118 |
-
<div id="crowd-log" class="
|
| 119 |
-
<div class="mt-1 text-[10px] text-
|
| 120 |
</div>
|
| 121 |
|
| 122 |
<div>
|
| 123 |
-
<h2 class="
|
| 124 |
-
<ol id="move-list" class="
|
| 125 |
-
</ol>
|
| 126 |
</div>
|
| 127 |
</div>
|
| 128 |
</div>
|
| 129 |
|
| 130 |
-
<div id="postmatch-panel" class="hidden mt-
|
| 131 |
-
<div class="
|
| 132 |
<span id="postmatch-headline">{{ character.name }} is reflecting on the match…</span>
|
| 133 |
</div>
|
| 134 |
-
<ul id="postmatch-steps" class="text-
|
| 135 |
</div>
|
| 136 |
|
| 137 |
<div id="disconnect-overlay">
|
| 138 |
-
<div class="
|
| 139 |
-
<div class="
|
| 140 |
-
<div class="text-
|
| 141 |
Your connection to {{ character.name }} dropped. The match will be abandoned in
|
| 142 |
-
<span id="disconnect-countdown" class="
|
| 143 |
unless you reconnect.
|
| 144 |
</div>
|
| 145 |
-
<div class="text-
|
| 146 |
</div>
|
| 147 |
</div>
|
| 148 |
|
|
|
|
| 9 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/chessboard-js/1.0.0/chessboard-1.0.0.min.js"></script>
|
| 10 |
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
| 11 |
<style>
|
| 12 |
+
/* Play-page-specific only — shared chat/conn/board styles live in base.html. */
|
|
|
|
|
|
|
|
|
|
| 13 |
.memory-ribbon { transition: opacity 0.8s ease-in-out; }
|
| 14 |
.memory-ribbon.fading { opacity: 0.35; }
|
| 15 |
.memory-detail { display: none; }
|
| 16 |
.memory-item:hover .memory-detail { display: block; }
|
| 17 |
.memory-item.expanded .memory-detail { display: block; }
|
| 18 |
+
.highlight-last { box-shadow: inset 0 0 0 3px var(--mp-brass-bright); }
|
| 19 |
|
| 20 |
+
/* Disconnect overlay — fixed, covers everything, brass-bordered modal. */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
#disconnect-overlay {
|
| 22 |
+
position: fixed; inset: 0; background: rgba(8, 10, 9, 0.88);
|
| 23 |
z-index: 50; display: none; align-items: center; justify-content: center;
|
| 24 |
+
backdrop-filter: blur(2px);
|
| 25 |
}
|
| 26 |
#disconnect-overlay.visible { display: flex; }
|
| 27 |
+
|
| 28 |
+
/* Board framing — a thin brass frame around the whole chessboard. */
|
| 29 |
+
.mp-board-frame {
|
| 30 |
+
padding: 10px;
|
| 31 |
+
background:
|
| 32 |
+
linear-gradient(180deg, rgba(201,166,107,0.08), rgba(201,166,107,0.02)),
|
| 33 |
+
var(--mp-surface-2);
|
| 34 |
+
border: 1px solid var(--mp-brass-dim);
|
| 35 |
+
position: relative;
|
| 36 |
+
}
|
| 37 |
+
.mp-board-frame::before {
|
| 38 |
+
content: ''; position: absolute; inset: 4px;
|
| 39 |
+
border: 1px solid rgba(201,166,107,0.18); pointer-events: none;
|
| 40 |
+
}
|
| 41 |
</style>
|
| 42 |
{% endblock %}
|
| 43 |
|
| 44 |
{% block content %}
|
| 45 |
|
| 46 |
+
<div id="alert-banner" class="hidden mb-5 rounded-sm border px-4 py-3 text-sm"></div>
|
| 47 |
|
| 48 |
+
<div class="flex items-start gap-6 mb-6 mp-enter mp-enter-1">
|
| 49 |
+
<div class="shrink-0 w-16 h-16 flex items-center justify-center border border-[var(--mp-brass-dim)] bg-[var(--mp-surface-2)] rounded-sm text-4xl leading-none">
|
| 50 |
+
{{ character.avatar_emoji or "♟" }}
|
| 51 |
+
</div>
|
| 52 |
<div class="flex-1">
|
| 53 |
+
<div class="mp-eyebrow mb-1">Board · {{ match.player_color.value }}</div>
|
| 54 |
+
<h1 class="mp-display text-[32px] leading-[1] tracking-tight">{{ character.name }}</h1>
|
| 55 |
+
<p class="text-[13px] text-[var(--mp-ink-muted)] mt-2 mp-clamp-2 max-w-xl">
|
| 56 |
{{ character.short_description }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
</p>
|
| 58 |
+
<div class="mt-2 flex items-center gap-4 text-[11px] mp-mono uppercase tracking-[0.12em] text-[var(--mp-ink-faint)]">
|
| 59 |
+
<span>~{{ match.character_elo_at_start }} Elo</span>
|
| 60 |
+
<span>·</span>
|
| 61 |
+
<span>mood <span id="emotion-indicator" class="text-[var(--mp-ink-muted)]">—</span></span>
|
| 62 |
+
</div>
|
| 63 |
</div>
|
| 64 |
+
<div class="text-right text-sm shrink-0">
|
| 65 |
+
<div id="match-status" class="mp-display text-[17px]">
|
| 66 |
{% if match.status.value == "in_progress" %}
|
| 67 |
+
<span class="text-[var(--mp-felt-bright)]">In progress</span>
|
| 68 |
{% elif match.status.value == "completed" %}
|
| 69 |
+
<span class="text-[var(--mp-brass-bright)]">Finished</span>
|
| 70 |
{% else %}
|
| 71 |
+
<span class="text-[#E79E9B]">Abandoned</span>
|
| 72 |
{% endif %}
|
| 73 |
</div>
|
| 74 |
+
<div class="text-[var(--mp-ink-faint)] text-[11px] mp-mono uppercase tracking-[0.12em] mt-1" id="match-result">
|
| 75 |
{{ match.result.value if match.result else "" }}
|
| 76 |
</div>
|
| 77 |
+
<div id="conn-pill" class="conn-pill conn-retry mt-3">
|
| 78 |
connecting…
|
| 79 |
</div>
|
| 80 |
+
<div id="spectator-count-pill" class="hidden mt-2 text-[10px] mp-mono uppercase tracking-[0.15em]" style="color: var(--mp-ink-blue-alt);">
|
| 81 |
<span id="spectator-count">0</span> watching
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
</div>
|
| 85 |
|
| 86 |
+
<div class="grid grid-cols-1 lg:grid-cols-[minmax(0,500px)_minmax(0,1fr)] gap-6 mp-enter mp-enter-2">
|
| 87 |
<div>
|
| 88 |
+
<div class="mp-board-frame">
|
| 89 |
+
<div id="board" class="w-full max-w-[480px]"></div>
|
| 90 |
+
</div>
|
| 91 |
+
<div class="mt-4 flex items-center gap-3">
|
| 92 |
+
<button id="resign-btn" class="mp-btn mp-btn-danger">Resign</button>
|
| 93 |
+
<div id="thinking-banner" class="hidden flex items-center gap-2 text-[13px] text-[var(--mp-brass-bright)]">
|
| 94 |
+
<span class="mp-livedot" style="background: var(--mp-brass-bright); box-shadow: 0 0 0 0 rgba(201,166,107,0.55);"></span>
|
| 95 |
+
<span>{{ character.name }} is thinking<span id="thinking-eta"></span>…</span>
|
| 96 |
</div>
|
| 97 |
</div>
|
| 98 |
+
<div id="memory-ribbon" class="hidden mt-5 text-[13px] italic text-[var(--mp-ink-muted)] border-l-2 pl-4 memory-ribbon"
|
| 99 |
+
style="border-color: var(--mp-brass-dim); background: linear-gradient(90deg, rgba(201,166,107,0.05), transparent);">
|
| 100 |
+
<span id="memory-ribbon-lede" class="mp-display">{{ character.name }} is reminded of something…</span>
|
| 101 |
+
<div id="memory-ribbon-items" class="mt-3 space-y-2"></div>
|
| 102 |
</div>
|
| 103 |
</div>
|
| 104 |
|
| 105 |
+
<div class="flex flex-col gap-5 min-h-[480px]">
|
| 106 |
<div>
|
| 107 |
+
<h2 class="mp-eyebrow mb-2">Table talk</h2>
|
| 108 |
+
<div id="chat-log" class="mp-panel rounded-sm text-sm p-4 h-[220px] overflow-y-auto space-y-2">
|
| 109 |
+
<div class="text-[12px] text-[var(--mp-ink-faint)] italic">
|
| 110 |
+
No messages yet. Press Enter to say something to {{ character.name }} — they can hear you even while thinking.
|
| 111 |
+
</div>
|
| 112 |
</div>
|
| 113 |
+
<div class="mt-2">
|
| 114 |
+
<input id="chat-input" type="text" maxlength="500"
|
| 115 |
+
placeholder="Say something to {{ character.name }}… (Enter to send)"
|
| 116 |
+
class="mp-input w-full" />
|
| 117 |
</div>
|
| 118 |
</div>
|
| 119 |
|
| 120 |
<div id="crowd-panel" class="hidden">
|
| 121 |
<div class="flex items-center justify-between mb-2">
|
| 122 |
+
<h2 class="mp-eyebrow">Crowd noise</h2>
|
| 123 |
+
<button id="crowd-mute-btn" class="text-[10px] mp-mono uppercase tracking-[0.15em] text-[var(--mp-ink-faint)] hover:text-[var(--mp-ink)]">mute</button>
|
| 124 |
</div>
|
| 125 |
+
<div id="crowd-log" class="mp-panel rounded-sm text-sm p-3 h-[140px] overflow-y-auto space-y-1.5"></div>
|
| 126 |
+
<div class="mt-1 text-[10px] text-[var(--mp-ink-ghost)] italic">{{ character.name }} can't hear them.</div>
|
| 127 |
</div>
|
| 128 |
|
| 129 |
<div>
|
| 130 |
+
<h2 class="mp-eyebrow mb-2">Moves</h2>
|
| 131 |
+
<ol id="move-list" class="mp-panel rounded-sm p-4 h-[220px] overflow-y-auto mp-mono text-[13px] text-[var(--mp-ink)]"></ol>
|
|
|
|
| 132 |
</div>
|
| 133 |
</div>
|
| 134 |
</div>
|
| 135 |
|
| 136 |
+
<div id="postmatch-panel" class="hidden mt-8 mp-panel rounded-sm px-5 py-4">
|
| 137 |
+
<div class="mp-display text-[17px] text-[var(--mp-ink)] mb-2">
|
| 138 |
<span id="postmatch-headline">{{ character.name }} is reflecting on the match…</span>
|
| 139 |
</div>
|
| 140 |
+
<ul id="postmatch-steps" class="text-[12px] text-[var(--mp-ink-muted)] space-y-1 mp-mono"></ul>
|
| 141 |
</div>
|
| 142 |
|
| 143 |
<div id="disconnect-overlay">
|
| 144 |
+
<div class="mp-panel-raised mp-framed rounded-sm p-7 max-w-md text-center" style="border-color: var(--mp-oxblood);">
|
| 145 |
+
<div class="mp-display italic text-[26px] mb-2" style="color: #E79E9B;">Connection lost.</div>
|
| 146 |
+
<div class="text-[13px] text-[var(--mp-ink-muted)] mb-4">
|
| 147 |
Your connection to {{ character.name }} dropped. The match will be abandoned in
|
| 148 |
+
<span id="disconnect-countdown" class="mp-mono text-[var(--mp-brass-bright)]">—:—</span>
|
| 149 |
unless you reconnect.
|
| 150 |
</div>
|
| 151 |
+
<div class="text-[11px] text-[var(--mp-ink-faint)] mp-mono uppercase tracking-[0.15em]">client is retrying automatically</div>
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
|
|
@@ -2,54 +2,54 @@
|
|
| 2 |
{% block title %}@{{ target.username }} — Metropolis Chess Club{% endblock %}
|
| 3 |
|
| 4 |
{% block content %}
|
| 5 |
-
<div class="mb-
|
| 6 |
-
<
|
| 7 |
-
<
|
|
|
|
| 8 |
{{ target.display_name }}
|
| 9 |
-
|
|
|
|
| 10 |
</p>
|
| 11 |
</div>
|
| 12 |
|
| 13 |
-
<
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
{% if recent_matches %}
|
| 16 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
| 17 |
{% for match, character in recent_matches %}
|
| 18 |
<a href="/matches/{{ match.id }}/summary"
|
| 19 |
-
class="flex items-center gap-4 rounded-
|
| 20 |
-
<div class="
|
|
|
|
|
|
|
| 21 |
<div class="flex-1 min-w-0">
|
| 22 |
-
<div class="
|
| 23 |
-
<div class="text-
|
| 24 |
-
{% if match.status.value == 'abandoned' %}
|
| 25 |
-
|
| 26 |
-
{% elif match.result %}
|
| 27 |
-
{{ match.result.value }}
|
| 28 |
-
{% endif %}
|
| 29 |
-
{% if match.ended_at %}· {{ match.ended_at.strftime('%Y-%m-%d') }}{% endif %}
|
| 30 |
</div>
|
| 31 |
</div>
|
| 32 |
</a>
|
| 33 |
{% endfor %}
|
| 34 |
</div>
|
| 35 |
{% else %}
|
| 36 |
-
<div class="
|
| 37 |
-
No finished matches to show.
|
| 38 |
</div>
|
| 39 |
{% endif %}
|
| 40 |
</section>
|
| 41 |
|
| 42 |
-
<section>
|
| 43 |
-
<h2 class="
|
| 44 |
{% if owned_characters %}
|
| 45 |
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 46 |
-
{% for c in owned_characters %}
|
| 47 |
-
{% include "_partials/character_card.html" %}
|
| 48 |
-
{% endfor %}
|
| 49 |
</div>
|
| 50 |
{% else %}
|
| 51 |
-
<div class="
|
| 52 |
-
No characters yet.
|
| 53 |
</div>
|
| 54 |
{% endif %}
|
| 55 |
</section>
|
|
|
|
| 2 |
{% block title %}@{{ target.username }} — Metropolis Chess Club{% endblock %}
|
| 3 |
|
| 4 |
{% block content %}
|
| 5 |
+
<div class="mb-8 mp-enter mp-enter-1">
|
| 6 |
+
<div class="mp-eyebrow mb-2">Member</div>
|
| 7 |
+
<h1 class="mp-display text-[38px] leading-[1] tracking-tight">@{{ target.username }}</h1>
|
| 8 |
+
<p class="text-[13px] text-[var(--mp-ink-muted)] mt-2">
|
| 9 |
{{ target.display_name }}
|
| 10 |
+
<span class="text-[var(--mp-ink-ghost)] mx-1">·</span>
|
| 11 |
+
<span class="mp-mono text-[12px]">joined {{ target.created_at.strftime('%Y-%m-%d') }}</span>
|
| 12 |
</p>
|
| 13 |
</div>
|
| 14 |
|
| 15 |
+
<div class="mp-brass-rule mb-6"></div>
|
| 16 |
+
|
| 17 |
+
<section class="mb-10 mp-enter mp-enter-2">
|
| 18 |
+
<h2 class="mp-eyebrow mb-3">Recent matches</h2>
|
| 19 |
{% if recent_matches %}
|
| 20 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
| 21 |
{% for match, character in recent_matches %}
|
| 22 |
<a href="/matches/{{ match.id }}/summary"
|
| 23 |
+
class="mp-panel-raised flex items-center gap-4 rounded-sm transition" style="padding: 0.9rem 1rem;">
|
| 24 |
+
<div class="shrink-0 w-11 h-11 flex items-center justify-center border border-[var(--mp-hairline-2)] bg-[var(--mp-surface-1)] rounded-sm text-xl leading-none">
|
| 25 |
+
{{ character.avatar_emoji or "♟" }}
|
| 26 |
+
</div>
|
| 27 |
<div class="flex-1 min-w-0">
|
| 28 |
+
<div class="mp-display text-[17px] truncate">{{ character.name }}</div>
|
| 29 |
+
<div class="text-[11px] mp-mono uppercase tracking-[0.1em] text-[var(--mp-ink-faint)] mt-1">
|
| 30 |
+
{% if match.status.value == 'abandoned' %}resigned{% elif match.result %}{{ match.result.value }}{% endif %}
|
| 31 |
+
{% if match.ended_at %}<span class="text-[var(--mp-ink-ghost)]"> · </span>{{ match.ended_at.strftime('%Y-%m-%d') }}{% endif %}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
</div>
|
| 33 |
</div>
|
| 34 |
</a>
|
| 35 |
{% endfor %}
|
| 36 |
</div>
|
| 37 |
{% else %}
|
| 38 |
+
<div class="mp-panel rounded-sm px-4 py-8 text-center text-[13px] text-[var(--mp-ink-faint)]">
|
| 39 |
+
<div class="mp-display italic">No finished matches to show.</div>
|
| 40 |
</div>
|
| 41 |
{% endif %}
|
| 42 |
</section>
|
| 43 |
|
| 44 |
+
<section class="mp-enter mp-enter-3">
|
| 45 |
+
<h2 class="mp-eyebrow mb-3">Characters created</h2>
|
| 46 |
{% if owned_characters %}
|
| 47 |
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 48 |
+
{% for c in owned_characters %}{% include "_partials/character_card.html" %}{% endfor %}
|
|
|
|
|
|
|
| 49 |
</div>
|
| 50 |
{% else %}
|
| 51 |
+
<div class="mp-panel rounded-sm px-4 py-8 text-center text-[13px] text-[var(--mp-ink-faint)]">
|
| 52 |
+
<div class="mp-display italic">No characters yet.</div>
|
| 53 |
</div>
|
| 54 |
{% endif %}
|
| 55 |
</section>
|
|
@@ -3,61 +3,48 @@
|
|
| 3 |
{% block title %}Settings — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
-
<div class="max-w-xl">
|
| 7 |
-
<
|
| 8 |
-
<
|
| 9 |
-
|
|
|
|
| 10 |
</p>
|
| 11 |
|
| 12 |
{% if error %}
|
| 13 |
-
<div class="mb-
|
| 14 |
-
{{ error }}
|
| 15 |
-
</div>
|
| 16 |
{% endif %}
|
| 17 |
|
| 18 |
<form method="post" action="/settings" class="space-y-6">
|
| 19 |
-
<div>
|
| 20 |
-
<label class="
|
| 21 |
-
<input name="display_name" value="{{ player.display_name }}" maxlength="80"
|
| 22 |
-
|
| 23 |
-
<p class="text-xs text-neutral-500 mt-1">Shown on your profile and match records. Free-form.</p>
|
| 24 |
</div>
|
| 25 |
|
| 26 |
-
<fieldset class="
|
| 27 |
-
<legend class="px-2
|
| 28 |
-
<p class="text-
|
| 29 |
Characters whose rating exceeds this are hidden from you.
|
| 30 |
</p>
|
| 31 |
{% set cur = player.max_content_rating.value if player.max_content_rating.value is defined else player.max_content_rating %}
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
<
|
| 36 |
-
|
| 37 |
-
<div class="text-xs text-neutral-500">Kid-friendly. No swearing, no adult themes.</div>
|
| 38 |
-
</div>
|
| 39 |
-
</label>
|
| 40 |
-
<label class="flex items-start gap-3 cursor-pointer">
|
| 41 |
-
<input type="radio" name="max_content_rating" value="mature" {% if cur == 'mature' %}checked{% endif %}
|
| 42 |
-
class="mt-1 accent-emerald-400" />
|
| 43 |
-
<div>
|
| 44 |
-
<div class="text-sm font-medium">Mature</div>
|
| 45 |
-
<div class="text-xs text-neutral-500">Adult tone allowed. Mild swearing, adult humor. No explicit content.</div>
|
| 46 |
-
</div>
|
| 47 |
-
</label>
|
| 48 |
-
<label class="flex items-start gap-3 cursor-pointer">
|
| 49 |
-
<input type="radio" name="max_content_rating" value="unrestricted" {% if cur == 'unrestricted' %}checked{% endif %}
|
| 50 |
-
class="mt-1 accent-emerald-400" />
|
| 51 |
<div>
|
| 52 |
-
<div class="text-
|
| 53 |
-
<div class="text-
|
| 54 |
</div>
|
| 55 |
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</fieldset>
|
| 57 |
|
| 58 |
-
<button class="
|
| 59 |
-
Save
|
| 60 |
-
</button>
|
| 61 |
</form>
|
| 62 |
</div>
|
| 63 |
{% endblock %}
|
|
|
|
| 3 |
{% block title %}Settings — Metropolis Chess Club{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
+
<div class="max-w-xl mp-enter mp-enter-1">
|
| 7 |
+
<div class="mp-eyebrow mb-2">Member · @{{ player.username }}</div>
|
| 8 |
+
<h1 class="mp-display text-[36px] leading-[1] tracking-tight mb-2">Settings</h1>
|
| 9 |
+
<p class="text-[13px] text-[var(--mp-ink-muted)] mb-6">
|
| 10 |
+
Username is permanent in this release. Rating changes apply to new matches only — they don't interrupt a game in progress.
|
| 11 |
</p>
|
| 12 |
|
| 13 |
{% if error %}
|
| 14 |
+
<div class="mb-5 rounded-sm border p-3 text-sm" style="border-color: rgba(168,66,63,0.45); background: rgba(168,66,63,0.08); color: #E79E9B;">{{ error }}</div>
|
|
|
|
|
|
|
| 15 |
{% endif %}
|
| 16 |
|
| 17 |
<form method="post" action="/settings" class="space-y-6">
|
| 18 |
+
<div class="mp-panel rounded-sm p-5">
|
| 19 |
+
<label class="mp-eyebrow block mb-2">Display name</label>
|
| 20 |
+
<input name="display_name" value="{{ player.display_name }}" maxlength="80" class="mp-input w-full" />
|
| 21 |
+
<p class="text-[11px] text-[var(--mp-ink-faint)] mt-2">Shown on your profile and match records. Free-form.</p>
|
|
|
|
| 22 |
</div>
|
| 23 |
|
| 24 |
+
<fieldset class="mp-panel rounded-sm p-5">
|
| 25 |
+
<legend class="mp-eyebrow px-2" style="color: var(--mp-brass);">Content rating</legend>
|
| 26 |
+
<p class="text-[11px] text-[var(--mp-ink-faint)] mb-4 mt-1">
|
| 27 |
Characters whose rating exceeds this are hidden from you.
|
| 28 |
</p>
|
| 29 |
{% set cur = player.max_content_rating.value if player.max_content_rating.value is defined else player.max_content_rating %}
|
| 30 |
+
|
| 31 |
+
{% macro rating_row(key, title, desc) %}
|
| 32 |
+
<label class="flex items-start gap-3 cursor-pointer py-2">
|
| 33 |
+
<input type="radio" name="max_content_rating" value="{{ key }}" {% if cur == key %}checked{% endif %}
|
| 34 |
+
class="mt-1 accent-[var(--mp-brass)]" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
<div>
|
| 36 |
+
<div class="mp-display text-[16px] text-[var(--mp-ink)]">{{ title }}</div>
|
| 37 |
+
<div class="text-[12px] text-[var(--mp-ink-faint)] mt-0.5">{{ desc }}</div>
|
| 38 |
</div>
|
| 39 |
</label>
|
| 40 |
+
{% endmacro %}
|
| 41 |
+
|
| 42 |
+
{{ rating_row('family', 'Family', 'Kid-friendly. No swearing, no adult themes.') }}
|
| 43 |
+
{{ rating_row('mature', 'Mature', 'Adult tone allowed. Mild swearing, adult humor. No explicit content.') }}
|
| 44 |
+
{{ rating_row('unrestricted', 'Unrestricted', 'Character authors take full responsibility. The LLM still refuses genuinely harmful content.') }}
|
| 45 |
</fieldset>
|
| 46 |
|
| 47 |
+
<button class="mp-btn mp-btn-brass" style="padding: 0.65rem 1.4rem;">Save</button>
|
|
|
|
|
|
|
| 48 |
</form>
|
| 49 |
</div>
|
| 50 |
{% endblock %}
|
|
@@ -4,141 +4,143 @@
|
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
<div class="flex items-start gap-6 mb-8">
|
| 10 |
-
<div class="
|
|
|
|
|
|
|
| 11 |
<div class="flex-1">
|
| 12 |
-
<div class="
|
| 13 |
-
<h1 class="text-
|
| 14 |
-
<p class="text-
|
| 15 |
</div>
|
| 16 |
-
<a href="/" class="
|
| 17 |
-
← Back home
|
| 18 |
-
</a>
|
| 19 |
</div>
|
| 20 |
|
| 21 |
-
{# --- Outcome + Elo
|
| 22 |
|
| 23 |
-
<section class="
|
|
|
|
| 24 |
<div class="flex items-start justify-between gap-6">
|
| 25 |
<div>
|
| 26 |
-
<div class="
|
| 27 |
-
<div class="text-
|
| 28 |
-
{% if player_outcome == 'win' %}text-
|
| 29 |
-
{% elif player_outcome == 'loss' %}text-
|
| 30 |
-
{% elif player_outcome == 'draw' %}text-
|
| 31 |
-
{% else %}text-
|
| 32 |
-
{% if player_outcome == 'win' %}You won
|
| 33 |
-
{% elif player_outcome == 'loss' %}You lost
|
| 34 |
-
{% elif player_outcome == 'draw' %}
|
| 35 |
-
{% elif player_outcome == 'resigned' %}You resigned
|
| 36 |
{% else %}{{ player_outcome or 'unresolved' }}{% endif %}
|
| 37 |
</div>
|
| 38 |
-
<div class="text-
|
|
|
|
|
|
|
| 39 |
</div>
|
| 40 |
|
| 41 |
<div class="text-right">
|
| 42 |
-
<div class="
|
| 43 |
-
<div class="text-
|
| 44 |
-
<span class="text-
|
| 45 |
-
<span class="mx-2 text-
|
| 46 |
-
<span class="{% if elo_delta_applied and elo_delta_applied > 0 %}text-
|
| 47 |
-
{% elif elo_delta_applied and elo_delta_applied < 0 %}text-
|
| 48 |
-
{% else %}text-
|
| 49 |
{{ elo_after }}
|
| 50 |
</span>
|
| 51 |
</div>
|
| 52 |
{% if elo_delta_applied is not none %}
|
| 53 |
-
<div class="text-
|
| 54 |
-
{% if elo_delta_applied > 0 %}text-
|
| 55 |
-
{% elif elo_delta_applied < 0 %}text-
|
| 56 |
-
{% else %}text-
|
| 57 |
{{ "+" if elo_delta_applied > 0 else "" }}{{ elo_delta_applied }}
|
| 58 |
-
{% if floor_raised %}· floor
|
| 59 |
</div>
|
| 60 |
{% endif %}
|
| 61 |
</div>
|
| 62 |
</div>
|
| 63 |
|
| 64 |
-
{# Elo math breakdown. #}
|
| 65 |
{% if elo_breakdown %}
|
| 66 |
-
<div class="mt-
|
| 67 |
-
<div class="
|
| 68 |
-
<div class="space-y-1">
|
| 69 |
-
<div>Outcome
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
<
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
{% endif %}
|
| 79 |
-
<div>Raw
|
| 80 |
-
<div>× 0.1 gain, clamped ±30
|
| 81 |
</div>
|
| 82 |
</div>
|
| 83 |
{% endif %}
|
| 84 |
</section>
|
| 85 |
|
| 86 |
-
{# --- Generated memories -------------------------
|
| 87 |
|
| 88 |
{% if generated_memories %}
|
| 89 |
-
<section class="mb-6">
|
| 90 |
-
<h2 class="
|
| 91 |
-
|
| 92 |
-
</h2>
|
| 93 |
-
<div class="space-y-3">
|
| 94 |
{% for mem in generated_memories %}
|
| 95 |
-
<article class="rounded-
|
| 96 |
-
<p class="text-
|
| 97 |
-
<div class="mt-
|
| 98 |
{% for trig in mem.triggers[:6] %}
|
| 99 |
-
<span class="
|
| 100 |
{% endfor %}
|
| 101 |
-
<span class="ml-
|
| 102 |
valence {{ "%+.1f"|format(mem.emotional_valence) }}
|
| 103 |
</span>
|
| 104 |
</div>
|
| 105 |
</article>
|
| 106 |
{% endfor %}
|
| 107 |
</div>
|
| 108 |
-
<p class="text-
|
| 109 |
Next time you play {{ character.name }}, these may surface.
|
| 110 |
</p>
|
| 111 |
</section>
|
| 112 |
{% elif analysis_status == 'running' or analysis_status == 'pending' %}
|
| 113 |
-
<section class="mb-6
|
|
|
|
| 114 |
{{ character.name }} is still reflecting on the match. Refresh in a moment.
|
| 115 |
</section>
|
| 116 |
{% else %}
|
| 117 |
-
<section class="mb-6
|
| 118 |
{{ character.name }} didn't form a new memory from this one.
|
| 119 |
</section>
|
| 120 |
{% endif %}
|
| 121 |
|
| 122 |
-
{# --- Opponent note --------------------
|
| 123 |
|
| 124 |
{% if narrative_summary %}
|
| 125 |
-
<section class="mb-6 rounded-
|
| 126 |
-
<div class="
|
| 127 |
-
|
| 128 |
-
</div>
|
| 129 |
-
<p class="text-neutral-200 italic leading-relaxed">"{{ narrative_summary }}"</p>
|
| 130 |
</section>
|
| 131 |
{% endif %}
|
| 132 |
|
| 133 |
-
{# --- Critical moments ------------------------------------------------ #}
|
| 134 |
|
| 135 |
{% if critical_moments %}
|
| 136 |
<section class="mb-6">
|
| 137 |
-
<h2 class="
|
| 138 |
-
<ul class="space-y-
|
| 139 |
{% for cm in critical_moments %}
|
| 140 |
-
<li class="flex gap-
|
| 141 |
-
<span class="
|
| 142 |
<span>{{ cm.label }}</span>
|
| 143 |
</li>
|
| 144 |
{% endfor %}
|
|
@@ -146,19 +148,12 @@
|
|
| 146 |
</section>
|
| 147 |
{% endif %}
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
<div class="mt-8 flex gap-3 border-t border-neutral-800 pt-6">
|
| 152 |
<form method="POST" action="/play/{{ character.id }}">
|
| 153 |
-
<button class="
|
| 154 |
-
Rematch
|
| 155 |
-
</button>
|
| 156 |
</form>
|
| 157 |
-
<a href="/" class="
|
| 158 |
-
Pick someone else
|
| 159 |
-
</a>
|
| 160 |
</div>
|
| 161 |
|
| 162 |
</div>
|
| 163 |
-
|
| 164 |
{% endblock %}
|
|
|
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
|
| 7 |
+
{# Summary page — framed like a private dossier. Wax-seal masthead, Elo
|
| 8 |
+
ledger, memories as italicised excerpts, opponent's note set in quotes. #}
|
| 9 |
+
|
| 10 |
+
<div class="max-w-3xl mx-auto mp-enter mp-enter-1">
|
| 11 |
|
| 12 |
<div class="flex items-start gap-6 mb-8">
|
| 13 |
+
<div class="shrink-0 w-20 h-20 flex items-center justify-center border border-[var(--mp-brass-dim)] bg-[var(--mp-surface-2)] rounded-sm text-5xl leading-none">
|
| 14 |
+
{{ character.avatar_emoji or "♟" }}
|
| 15 |
+
</div>
|
| 16 |
<div class="flex-1">
|
| 17 |
+
<div class="mp-eyebrow mb-2">Match against</div>
|
| 18 |
+
<h1 class="mp-display text-[36px] leading-[1] tracking-tight">{{ character.name }}</h1>
|
| 19 |
+
<p class="text-[13px] text-[var(--mp-ink-muted)] mt-2 mp-clamp-2">{{ character.short_description }}</p>
|
| 20 |
</div>
|
| 21 |
+
<a href="/" class="mp-btn mp-btn-ghost self-start">← Home</a>
|
|
|
|
|
|
|
| 22 |
</div>
|
| 23 |
|
| 24 |
+
{# --- Outcome + Elo ledger --------------------------------------------- #}
|
| 25 |
|
| 26 |
+
<section class="mp-panel-raised mp-framed rounded-sm p-7 mb-6 mp-enter mp-enter-2 relative">
|
| 27 |
+
<span class="mp-frame-tl"></span><span class="mp-frame-br"></span>
|
| 28 |
<div class="flex items-start justify-between gap-6">
|
| 29 |
<div>
|
| 30 |
+
<div class="mp-eyebrow mb-2">Result</div>
|
| 31 |
+
<div class="mp-display text-[34px] leading-none tracking-tight
|
| 32 |
+
{% if player_outcome == 'win' %}text-[var(--mp-felt-bright)]
|
| 33 |
+
{% elif player_outcome == 'loss' %}text-[#E79E9B]
|
| 34 |
+
{% elif player_outcome == 'draw' %}text-[var(--mp-ink)]
|
| 35 |
+
{% else %}text-[var(--mp-brass-bright)]{% endif %}">
|
| 36 |
+
{% if player_outcome == 'win' %}You won.
|
| 37 |
+
{% elif player_outcome == 'loss' %}You lost.
|
| 38 |
+
{% elif player_outcome == 'draw' %}A draw.
|
| 39 |
+
{% elif player_outcome == 'resigned' %}You resigned.
|
| 40 |
{% else %}{{ player_outcome or 'unresolved' }}{% endif %}
|
| 41 |
</div>
|
| 42 |
+
<div class="text-[12px] text-[var(--mp-ink-faint)] mt-2 mp-mono uppercase tracking-[0.12em]">
|
| 43 |
+
{{ match.move_count }} half-moves · {{ character.name }}'s side: {{ char_color }}
|
| 44 |
+
</div>
|
| 45 |
</div>
|
| 46 |
|
| 47 |
<div class="text-right">
|
| 48 |
+
<div class="mp-eyebrow mb-2">{{ character.name }}'s Elo</div>
|
| 49 |
+
<div class="mp-display text-[28px] leading-none">
|
| 50 |
+
<span class="text-[var(--mp-ink-faint)]">{{ elo_before }}</span>
|
| 51 |
+
<span class="mx-2 text-[var(--mp-ink-ghost)]">→</span>
|
| 52 |
+
<span class="{% if elo_delta_applied and elo_delta_applied > 0 %}text-[var(--mp-felt-bright)]
|
| 53 |
+
{% elif elo_delta_applied and elo_delta_applied < 0 %}text-[#E79E9B]
|
| 54 |
+
{% else %}text-[var(--mp-ink)]{% endif %}">
|
| 55 |
{{ elo_after }}
|
| 56 |
</span>
|
| 57 |
</div>
|
| 58 |
{% if elo_delta_applied is not none %}
|
| 59 |
+
<div class="text-[11px] mt-2 mp-mono uppercase tracking-[0.12em]
|
| 60 |
+
{% if elo_delta_applied > 0 %}text-[var(--mp-felt-bright)]
|
| 61 |
+
{% elif elo_delta_applied < 0 %}text-[#E79E9B]
|
| 62 |
+
{% else %}text-[var(--mp-ink-faint)]{% endif %}">
|
| 63 |
{{ "+" if elo_delta_applied > 0 else "" }}{{ elo_delta_applied }}
|
| 64 |
+
{% if floor_raised %}<span class="text-[var(--mp-ink-faint)]">· floor +25</span>{% endif %}
|
| 65 |
</div>
|
| 66 |
{% endif %}
|
| 67 |
</div>
|
| 68 |
</div>
|
| 69 |
|
|
|
|
| 70 |
{% if elo_breakdown %}
|
| 71 |
+
<div class="mt-6 pt-5 border-t border-[var(--mp-hairline)]">
|
| 72 |
+
<div class="mp-eyebrow mb-3">How the delta was computed</div>
|
| 73 |
+
<div class="mp-mono text-[12px] text-[var(--mp-ink-muted)] space-y-1.5">
|
| 74 |
+
<div>Outcome <span class="text-[var(--mp-ink)]">{{ "+" if elo_breakdown.outcome > 0 else "" }}{{ elo_breakdown.outcome }}</span>
|
| 75 |
+
<span class="text-[var(--mp-ink-faint)]">
|
| 76 |
+
{% if elo_breakdown.outcome > 0 %}(win){% elif elo_breakdown.outcome < 0 %}(loss){% else %}(draw){% endif %}
|
| 77 |
+
</span>
|
| 78 |
+
</div>
|
| 79 |
+
<div>Move quality <span class="text-[var(--mp-ink)]">{{ "+" if elo_breakdown.move_quality > 0 else "" }}{{ elo_breakdown.move_quality }}</span>
|
| 80 |
+
<span class="text-[var(--mp-ink-faint)]">(eval loss, both sides)</span>
|
| 81 |
+
</div>
|
| 82 |
+
{% if elo_breakdown.short_halved %}<div class="italic text-[var(--mp-ink-faint)]">Short match ({{ match.move_count }} < 10 moves): halved</div>{% endif %}
|
| 83 |
+
{% if elo_breakdown.rage_quit %}<div class="italic text-[var(--mp-ink-faint)]">Rage-quit: move quality skipped</div>{% endif %}
|
| 84 |
+
<div>Raw <span class="text-[var(--mp-ink)]">{{ "+" if elo_breakdown.raw > 0 else "" }}{{ elo_breakdown.raw }}</span></div>
|
| 85 |
+
<div>× 0.1 gain, clamped ±30 ⇒ <span class="text-[var(--mp-brass-bright)] font-semibold">{{ "+" if elo_delta_applied > 0 else "" }}{{ elo_delta_applied }}</span></div>
|
| 86 |
</div>
|
| 87 |
</div>
|
| 88 |
{% endif %}
|
| 89 |
</section>
|
| 90 |
|
| 91 |
+
{# --- Generated memories — italicised excerpts ------------------------- #}
|
| 92 |
|
| 93 |
{% if generated_memories %}
|
| 94 |
+
<section class="mb-6 mp-enter mp-enter-3">
|
| 95 |
+
<h2 class="mp-eyebrow mb-3">What {{ character.name }} will remember</h2>
|
| 96 |
+
<div class="space-y-4">
|
|
|
|
|
|
|
| 97 |
{% for mem in generated_memories %}
|
| 98 |
+
<article class="mp-panel rounded-sm p-5" style="border-left: 3px solid var(--mp-brass);">
|
| 99 |
+
<p class="mp-display italic text-[17px] text-[var(--mp-ink)] leading-relaxed">"{{ mem.narrative_text }}"</p>
|
| 100 |
+
<div class="mt-4 flex flex-wrap items-center gap-2 text-[11px] mp-mono uppercase tracking-[0.1em]">
|
| 101 |
{% for trig in mem.triggers[:6] %}
|
| 102 |
+
<span class="mp-chip" style="background: rgba(201,166,107,0.08); color: var(--mp-brass); border-color: rgba(201,166,107,0.25);">{{ trig }}</span>
|
| 103 |
{% endfor %}
|
| 104 |
+
<span class="ml-auto text-[var(--mp-ink-faint)] normal-case tracking-normal">
|
| 105 |
valence {{ "%+.1f"|format(mem.emotional_valence) }}
|
| 106 |
</span>
|
| 107 |
</div>
|
| 108 |
</article>
|
| 109 |
{% endfor %}
|
| 110 |
</div>
|
| 111 |
+
<p class="text-[12px] text-[var(--mp-ink-faint)] italic mt-4 mp-display">
|
| 112 |
Next time you play {{ character.name }}, these may surface.
|
| 113 |
</p>
|
| 114 |
</section>
|
| 115 |
{% elif analysis_status == 'running' or analysis_status == 'pending' %}
|
| 116 |
+
<section class="mb-6 mp-panel rounded-sm p-5 text-[13px] text-[var(--mp-ink-muted)]">
|
| 117 |
+
<span class="mp-livedot" style="background: var(--mp-brass-bright);"></span>
|
| 118 |
{{ character.name }} is still reflecting on the match. Refresh in a moment.
|
| 119 |
</section>
|
| 120 |
{% else %}
|
| 121 |
+
<section class="mb-6 mp-panel rounded-sm p-5 text-[13px] text-[var(--mp-ink-muted)]">
|
| 122 |
{{ character.name }} didn't form a new memory from this one.
|
| 123 |
</section>
|
| 124 |
{% endif %}
|
| 125 |
|
| 126 |
+
{# --- Opponent's note — set like a margin quotation -------------------- #}
|
| 127 |
|
| 128 |
{% if narrative_summary %}
|
| 129 |
+
<section class="mb-6 mp-panel-raised rounded-sm p-6" style="border-left: 3px solid var(--mp-felt);">
|
| 130 |
+
<div class="mp-eyebrow mb-3">{{ character.name }}'s note about you</div>
|
| 131 |
+
<p class="mp-display italic text-[18px] text-[var(--mp-ink)] leading-relaxed">"{{ narrative_summary }}"</p>
|
|
|
|
|
|
|
| 132 |
</section>
|
| 133 |
{% endif %}
|
| 134 |
|
| 135 |
+
{# --- Critical moments ------------------------------------------------- #}
|
| 136 |
|
| 137 |
{% if critical_moments %}
|
| 138 |
<section class="mb-6">
|
| 139 |
+
<h2 class="mp-eyebrow mb-3">Turning points</h2>
|
| 140 |
+
<ul class="space-y-2 text-[13px] text-[var(--mp-ink-muted)]">
|
| 141 |
{% for cm in critical_moments %}
|
| 142 |
+
<li class="flex gap-3">
|
| 143 |
+
<span class="mp-mono text-[var(--mp-brass)] shrink-0">{{ cm.move_number }}.</span>
|
| 144 |
<span>{{ cm.label }}</span>
|
| 145 |
</li>
|
| 146 |
{% endfor %}
|
|
|
|
| 148 |
</section>
|
| 149 |
{% endif %}
|
| 150 |
|
| 151 |
+
<div class="mt-10 pt-6 border-t border-[var(--mp-hairline)] flex gap-3">
|
|
|
|
|
|
|
| 152 |
<form method="POST" action="/play/{{ character.id }}">
|
| 153 |
+
<button class="mp-btn mp-btn-brass">Rematch</button>
|
|
|
|
|
|
|
| 154 |
</form>
|
| 155 |
+
<a href="/" class="mp-btn mp-btn-ghost self-center">Pick someone else</a>
|
|
|
|
|
|
|
| 156 |
</div>
|
| 157 |
|
| 158 |
</div>
|
|
|
|
| 159 |
{% endblock %}
|
|
@@ -9,99 +9,96 @@
|
|
| 9 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/chessboard-js/1.0.0/chessboard-1.0.0.min.js"></script>
|
| 10 |
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
| 11 |
<style>
|
| 12 |
-
|
| 13 |
-
.black-3c85d { background-color: #525866 !important; }
|
| 14 |
.memory-ribbon { transition: opacity 0.8s ease-in-out; }
|
| 15 |
.memory-ribbon.fading { opacity: 0.35; }
|
| 16 |
-
.
|
| 17 |
-
|
| 18 |
-
|
|
|
|
| 19 |
}
|
| 20 |
-
.
|
| 21 |
-
|
| 22 |
-
border
|
| 23 |
-
margin-left: 1.5rem;
|
| 24 |
}
|
| 25 |
-
.chat-bubble-spectator {
|
| 26 |
-
background: rgba(15, 23, 42, 0.4);
|
| 27 |
-
border-left: 2px solid #a78bfa;
|
| 28 |
-
}
|
| 29 |
-
.chat-bubble-spectator.in-transit { opacity: 0.6; }
|
| 30 |
-
.conn-live { background: rgba(16, 185, 129, 0.18); color: #6ee7b7; }
|
| 31 |
-
.conn-retry { background: rgba(245, 158, 11, 0.18); color: #fcd34d; }
|
| 32 |
-
.conn-lost { background: rgba(244, 63, 94, 0.18); color: #fda4af; }
|
| 33 |
</style>
|
| 34 |
{% endblock %}
|
| 35 |
|
| 36 |
{% block content %}
|
| 37 |
-
<div class="flex items-start gap-6 mb-
|
| 38 |
-
<div class="
|
|
|
|
|
|
|
| 39 |
<div class="flex-1">
|
| 40 |
-
<
|
| 41 |
-
<
|
|
|
|
|
|
|
|
|
|
| 42 |
{{ character.short_description }}
|
| 43 |
-
· playing at ~{{ match.character_elo_at_start }} Elo
|
| 44 |
-
· spectating as <span class="text-indigo-300">@{{ player.username }}</span>
|
| 45 |
-
</p>
|
| 46 |
-
<p class="text-xs text-neutral-500 mt-1">
|
| 47 |
-
mood: <span id="emotion-indicator" class="font-medium text-neutral-300">—</span>
|
| 48 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
</div>
|
| 50 |
-
<div class="text-right text-sm">
|
| 51 |
-
<div id="match-status" class="
|
| 52 |
{% if match.status.value == "in_progress" %}
|
| 53 |
-
<span class="text-
|
| 54 |
{% elif match.status.value == "completed" %}
|
| 55 |
-
<span class="text-
|
| 56 |
{% else %}
|
| 57 |
-
<span class="text-
|
| 58 |
{% endif %}
|
| 59 |
</div>
|
| 60 |
-
<div class="text-
|
| 61 |
{{ match.result.value if match.result else "" }}
|
| 62 |
</div>
|
| 63 |
-
<div id="conn-pill" class="
|
| 64 |
-
|
| 65 |
-
</div>
|
| 66 |
-
<div id="spectator-count" class="mt-1 text-[10px] text-neutral-500 uppercase tracking-wider">— watching</div>
|
| 67 |
</div>
|
| 68 |
</div>
|
| 69 |
|
| 70 |
-
<div class="grid grid-cols-1 lg:grid-cols-[minmax(0,
|
| 71 |
<div>
|
| 72 |
-
<div
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
| 76 |
</div>
|
| 77 |
</div>
|
| 78 |
|
| 79 |
-
<div class="flex flex-col gap-
|
| 80 |
<div>
|
| 81 |
-
<h2 class="
|
| 82 |
-
<div id="chat-log" class="
|
| 83 |
-
<div class="text-
|
| 84 |
</div>
|
| 85 |
</div>
|
| 86 |
|
| 87 |
<div>
|
| 88 |
<div class="flex items-center justify-between mb-2">
|
| 89 |
-
<h2 class="
|
| 90 |
-
<span class="text-[10px] text-
|
| 91 |
</div>
|
| 92 |
-
<div id="spectator-log" class="
|
| 93 |
-
<div class="text-
|
| 94 |
</div>
|
| 95 |
-
<div class="mt-2
|
| 96 |
-
<input id="spec-input" type="text" maxlength="500" placeholder="Crowd chat (Enter to send)…"
|
| 97 |
-
class="flex-1 rounded bg-neutral-900 border border-neutral-800 px-3 py-1.5 text-sm focus:outline-none focus:border-indigo-600" />
|
| 98 |
</div>
|
| 99 |
</div>
|
| 100 |
|
| 101 |
<div>
|
| 102 |
-
<h2 class="
|
| 103 |
-
<ol id="move-list" class="
|
| 104 |
-
</ol>
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
</div>
|
|
|
|
| 9 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/chessboard-js/1.0.0/chessboard-1.0.0.min.js"></script>
|
| 10 |
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
| 11 |
<style>
|
| 12 |
+
/* Chat + connection styling is inherited from base.html. */
|
|
|
|
| 13 |
.memory-ribbon { transition: opacity 0.8s ease-in-out; }
|
| 14 |
.memory-ribbon.fading { opacity: 0.35; }
|
| 15 |
+
.mp-board-frame {
|
| 16 |
+
padding: 10px;
|
| 17 |
+
background: linear-gradient(180deg, rgba(201,166,107,0.08), rgba(201,166,107,0.02)), var(--mp-surface-2);
|
| 18 |
+
border: 1px solid var(--mp-brass-dim); position: relative;
|
| 19 |
}
|
| 20 |
+
.mp-board-frame::before {
|
| 21 |
+
content: ''; position: absolute; inset: 4px;
|
| 22 |
+
border: 1px solid rgba(201,166,107,0.18); pointer-events: none;
|
|
|
|
| 23 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
</style>
|
| 25 |
{% endblock %}
|
| 26 |
|
| 27 |
{% block content %}
|
| 28 |
+
<div class="flex items-start gap-6 mb-6 mp-enter mp-enter-1">
|
| 29 |
+
<div class="shrink-0 w-16 h-16 flex items-center justify-center border border-[var(--mp-brass-dim)] bg-[var(--mp-surface-2)] rounded-sm text-4xl leading-none">
|
| 30 |
+
{{ character.avatar_emoji or "♟" }}
|
| 31 |
+
</div>
|
| 32 |
<div class="flex-1">
|
| 33 |
+
<div class="mp-eyebrow mb-1">Spectating</div>
|
| 34 |
+
<h1 class="mp-display text-[30px] leading-[1]">
|
| 35 |
+
{{ character.name }} <span class="mp-italic text-[var(--mp-ink-muted)] text-[22px]">vs</span> <span class="mp-mono text-[18px] text-[var(--mp-ink-muted)] align-middle">@{{ match_owner.username }}</span>
|
| 36 |
+
</h1>
|
| 37 |
+
<p class="text-[13px] text-[var(--mp-ink-muted)] mt-2 mp-clamp-2 max-w-xl">
|
| 38 |
{{ character.short_description }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
</p>
|
| 40 |
+
<div class="mt-2 flex items-center gap-4 text-[11px] mp-mono uppercase tracking-[0.12em] text-[var(--mp-ink-faint)]">
|
| 41 |
+
<span>~{{ match.character_elo_at_start }} Elo</span>
|
| 42 |
+
<span>·</span>
|
| 43 |
+
<span>mood <span id="emotion-indicator" class="text-[var(--mp-ink-muted)]">—</span></span>
|
| 44 |
+
<span>·</span>
|
| 45 |
+
<span style="color: var(--mp-ink-blue-alt);">you: @{{ player.username }}</span>
|
| 46 |
+
</div>
|
| 47 |
</div>
|
| 48 |
+
<div class="text-right text-sm shrink-0">
|
| 49 |
+
<div id="match-status" class="mp-display text-[17px]">
|
| 50 |
{% if match.status.value == "in_progress" %}
|
| 51 |
+
<span class="text-[var(--mp-felt-bright)]">In progress</span>
|
| 52 |
{% elif match.status.value == "completed" %}
|
| 53 |
+
<span class="text-[var(--mp-brass-bright)]">Finished</span>
|
| 54 |
{% else %}
|
| 55 |
+
<span class="text-[#E79E9B]">Abandoned</span>
|
| 56 |
{% endif %}
|
| 57 |
</div>
|
| 58 |
+
<div class="text-[var(--mp-ink-faint)] text-[11px] mp-mono uppercase tracking-[0.12em] mt-1" id="match-result">
|
| 59 |
{{ match.result.value if match.result else "" }}
|
| 60 |
</div>
|
| 61 |
+
<div id="conn-pill" class="conn-pill conn-retry mt-3">connecting…</div>
|
| 62 |
+
<div id="spectator-count" class="mt-2 text-[10px] mp-mono uppercase tracking-[0.15em]" style="color: var(--mp-ink-blue-alt);">— watching</div>
|
|
|
|
|
|
|
| 63 |
</div>
|
| 64 |
</div>
|
| 65 |
|
| 66 |
+
<div class="grid grid-cols-1 lg:grid-cols-[minmax(0,500px)_minmax(0,1fr)] gap-6 mp-enter mp-enter-2">
|
| 67 |
<div>
|
| 68 |
+
<div class="mp-board-frame">
|
| 69 |
+
<div id="board" class="w-full max-w-[480px]"></div>
|
| 70 |
+
</div>
|
| 71 |
+
<div id="memory-ribbon" class="hidden mt-5 text-[13px] italic text-[var(--mp-ink-muted)] border-l-2 pl-4 memory-ribbon"
|
| 72 |
+
style="border-color: var(--mp-brass-dim); background: linear-gradient(90deg, rgba(201,166,107,0.05), transparent);">
|
| 73 |
+
<span id="memory-ribbon-lede" class="mp-display">{{ character.name }} is reminded of something…</span>
|
| 74 |
+
<div id="memory-ribbon-items" class="mt-3 space-y-2"></div>
|
| 75 |
</div>
|
| 76 |
</div>
|
| 77 |
|
| 78 |
+
<div class="flex flex-col gap-5 min-h-[480px]">
|
| 79 |
<div>
|
| 80 |
+
<h2 class="mp-eyebrow mb-2">Table talk</h2>
|
| 81 |
+
<div id="chat-log" class="mp-panel rounded-sm text-sm p-4 h-[220px] overflow-y-auto space-y-2">
|
| 82 |
+
<div class="text-[12px] text-[var(--mp-ink-faint)] italic">No messages yet.</div>
|
| 83 |
</div>
|
| 84 |
</div>
|
| 85 |
|
| 86 |
<div>
|
| 87 |
<div class="flex items-center justify-between mb-2">
|
| 88 |
+
<h2 class="mp-eyebrow">Crowd noise</h2>
|
| 89 |
+
<span class="text-[10px] text-[var(--mp-ink-ghost)] italic">the character can't hear you</span>
|
| 90 |
</div>
|
| 91 |
+
<div id="spectator-log" class="mp-panel rounded-sm text-sm p-3 h-[180px] overflow-y-auto space-y-2">
|
| 92 |
+
<div class="text-[12px] text-[var(--mp-ink-faint)] italic">Say something…</div>
|
| 93 |
</div>
|
| 94 |
+
<div class="mt-2">
|
| 95 |
+
<input id="spec-input" type="text" maxlength="500" placeholder="Crowd chat (Enter to send)…" class="mp-input w-full" />
|
|
|
|
| 96 |
</div>
|
| 97 |
</div>
|
| 98 |
|
| 99 |
<div>
|
| 100 |
+
<h2 class="mp-eyebrow mb-2">Moves</h2>
|
| 101 |
+
<ol id="move-list" class="mp-panel rounded-sm p-4 h-[180px] overflow-y-auto mp-mono text-[13px] text-[var(--mp-ink)]"></ol>
|
|
|
|
| 102 |
</div>
|
| 103 |
</div>
|
| 104 |
</div>
|
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Metropolis design system (Phase 3c polish pass)
|
| 2 |
+
|
| 3 |
+
A small, intentional vocabulary — not a framework. Tokens live in `app/web/templates/base.html`;
|
| 4 |
+
Tailwind (CDN) supplies the utility classes around them. No build step.
|
| 5 |
+
|
| 6 |
+
## Aesthetic direction
|
| 7 |
+
|
| 8 |
+
**"A discreet old-world chess café, crossed with a precise digital instrument."**
|
| 9 |
+
Brass and ink and felt and parchment, under warm incandescent light. Dark theme with warm undertones (not cool neutrals). Serifs for voice, mono for numbers, sans for body. Restraint beats density.
|
| 10 |
+
|
| 11 |
+
- Character cards feel like **member dossiers or trading cards**, not SaaS tiles.
|
| 12 |
+
- The board is the **hero** on the play page — framed in brass.
|
| 13 |
+
- Leaderboards are treated like **a club's posted standings**.
|
| 14 |
+
- The post-match summary is the **payoff screen** — half dossier, half quotation.
|
| 15 |
+
|
| 16 |
+
## Color tokens
|
| 17 |
+
|
| 18 |
+
Defined as CSS custom properties on `:root`. Reference via `var(--mp-…)`.
|
| 19 |
+
|
| 20 |
+
| Token | Value | Use |
|
| 21 |
+
| ------------------------ | ----------- | -------------------------------------------------------- |
|
| 22 |
+
| `--mp-bg` | `#0F1412` | Page background (felt in shadow) |
|
| 23 |
+
| `--mp-surface-1` | `#151B19` | Primary panel |
|
| 24 |
+
| `--mp-surface-2` | `#1C2320` | Raised card |
|
| 25 |
+
| `--mp-surface-3` | `#232A27` | Hover lift |
|
| 26 |
+
| `--mp-hairline` | `#2E3430` | Subtle border |
|
| 27 |
+
| `--mp-hairline-2` | `#3A4038` | Stronger border |
|
| 28 |
+
| `--mp-ink` | `#EDE4CE` | Primary text (parchment) |
|
| 29 |
+
| `--mp-ink-muted` | `#B5AD96` | Secondary text |
|
| 30 |
+
| `--mp-ink-faint` | `#7F7966` | Captions, metadata |
|
| 31 |
+
| `--mp-ink-ghost` | `#4C483E` | Placeholders, dividers-in-text |
|
| 32 |
+
| `--mp-brass` | `#C9A66B` | Primary accent — links, highlights, CTA |
|
| 33 |
+
| `--mp-brass-bright` | `#E0C38F` | Emphasis / live-state / hover |
|
| 34 |
+
| `--mp-brass-dim` | `#8E7649` | Borders under brass elements |
|
| 35 |
+
| `--mp-felt` | `#2F6B5C` | In-progress / success |
|
| 36 |
+
| `--mp-felt-bright` | `#4FA48D` | Live indicators |
|
| 37 |
+
| `--mp-oxblood` | `#A8423F` | Resign / danger |
|
| 38 |
+
| `--mp-ink-blue` | `#5879A3` | Spectator / secondary informational |
|
| 39 |
+
| `--mp-ink-blue-alt` | `#8DA4C3` | Spectator emphasis |
|
| 40 |
+
| `--mp-rating-*` | (see base) | Family / mature / unrestricted chips |
|
| 41 |
+
|
| 42 |
+
## Typography
|
| 43 |
+
|
| 44 |
+
| Role | Font | Notes |
|
| 45 |
+
| ------- | --------------------- | --------------------------------------------------------------------- |
|
| 46 |
+
| Display | **Fraunces** | Optical sizing + SOFT/WONK variable axes for subtle character. Use `.mp-display` (standard), `.mp-italic` (quotations), `.mp-display-tight` (headings). |
|
| 47 |
+
| Body | **IBM Plex Sans** | Default on `<body>`. Weights 300/400/500/600 available. |
|
| 48 |
+
| Mono | **IBM Plex Mono** | Elo, move lists, usernames, eyebrow labels. `.mp-mono`. |
|
| 49 |
+
|
| 50 |
+
Scale:
|
| 51 |
+
- H1 pages: `text-[36–44px]` in `.mp-display` with `leading-[0.95–1]` and `tracking-tight`.
|
| 52 |
+
- H2 sections: `.mp-eyebrow` — 10px mono uppercase with `0.18em` tracking. Use instead of Tailwind's `text-sm uppercase tracking-wider`.
|
| 53 |
+
- Body copy: `text-[13–14px]` with `text-[var(--mp-ink-muted)]`.
|
| 54 |
+
- Display quotations / lede italic: `mp-display mp-italic`.
|
| 55 |
+
|
| 56 |
+
## Motion
|
| 57 |
+
|
| 58 |
+
Restraint is the rule. One choreographed page entrance, a few subtle hovers, no bouncy everything.
|
| 59 |
+
|
| 60 |
+
- **Page entry**: `.mp-enter` + `.mp-enter-1/2/3` staggered reveal (fade + 8px slide-up, cubic-bezier ease, 520ms). Apply to the two or three primary sections of each page.
|
| 61 |
+
- **Live pulse**: `.mp-livedot` — 6px dot breathing at 2s cadence. Used for live-match indicators, `agent_thinking` badge, ongoing post-match.
|
| 62 |
+
- **Hovers**: cards and rows lift via `.mp-panel-raised:hover` (surface + border transition). Links shift to `--mp-brass-bright` on hover.
|
| 63 |
+
- **Custom easing**: `--mp-ease: cubic-bezier(0.22, 0.61, 0.36, 1)` — snappy decel, not bouncy.
|
| 64 |
+
|
| 65 |
+
## Components
|
| 66 |
+
|
| 67 |
+
### Panel primitives
|
| 68 |
+
- `.mp-panel` — flat panel, 1px hairline border.
|
| 69 |
+
- `.mp-panel-raised` — hover-able card (lifts on hover).
|
| 70 |
+
- `.mp-framed` — adds brass corner marks (top-right + bottom-left by default; pair with `.mp-frame-tl` + `.mp-frame-br` spans for all four).
|
| 71 |
+
|
| 72 |
+
### Buttons
|
| 73 |
+
- `.mp-btn` + one of: `.mp-btn-brass` (primary, brushed-brass gradient with inset highlight), `.mp-btn-ghost` (outlined), `.mp-btn-danger` (oxblood, for resign/delete).
|
| 74 |
+
|
| 75 |
+
### Chips
|
| 76 |
+
- `.mp-chip` — 10px mono uppercase pill, used for rating badges, result chips, status.
|
| 77 |
+
- Rating chips use `--mp-rating-*` tokens.
|
| 78 |
+
- Result chips: `{{ result_chip(m) }}` macro renders Live/Resigned/Draw/White/Black with the right palette.
|
| 79 |
+
|
| 80 |
+
### Inputs
|
| 81 |
+
- `.mp-input` — engraved field. Brass border on focus. Pair with `.mp-eyebrow` labels.
|
| 82 |
+
|
| 83 |
+
### Board frame (play + watch)
|
| 84 |
+
- `.mp-board-frame` — padded brass-bordered wrapper around `chessboard.js`. Double-ruled via `::before` pseudo-element to evoke a framed set.
|
| 85 |
+
|
| 86 |
+
### Connection pill (play + watch)
|
| 87 |
+
- `.conn-pill` + `.conn-live` / `.conn-retry` / `.conn-lost` — uppercase 10px mono pills with semantic coloring.
|
| 88 |
+
|
| 89 |
+
### Chat bubbles (play + watch)
|
| 90 |
+
- `.chat-bubble-agent` — felt-green left border.
|
| 91 |
+
- `.chat-bubble-player` — brass left border, indented.
|
| 92 |
+
- `.chat-bubble-spectator` — blue-grey left border. `.in-transit` fades to 0.55 opacity (optimistic render).
|
| 93 |
+
|
| 94 |
+
### Jinja partials
|
| 95 |
+
Shared under `app/web/templates/_partials/`:
|
| 96 |
+
- `_macros.html` — `rating_badge`, `character_state_badge`, `visibility_badge`, `preset_badge`, `result_chip`.
|
| 97 |
+
- `character_card.html` — member-dossier card, used on `/`, `/discovery`, and player profile.
|
| 98 |
+
- `match_row.html` — live / recent match row on `/discovery`.
|
| 99 |
+
|
| 100 |
+
## Anti-patterns (deliberately avoided)
|
| 101 |
+
|
| 102 |
+
- **Inter / Roboto / system sans** — overused, generic. Fraunces + IBM Plex Sans is distinctive and recognisable.
|
| 103 |
+
- **Cool greys (`#737373` ilk)** — feel clinical. The palette uses warm ink (`#B5AD96`) that matches parchment.
|
| 104 |
+
- **Purple-blue gradient tiles on white** — textbook AI-slop. We use dark felt with brass accents instead.
|
| 105 |
+
- **Emoji-heavy icons** — restricted to `avatar_emoji` on characters (they're the character's portrait, framed) and the occasional `›` chevron. No sprinkled decorative emoji.
|
| 106 |
+
- **Animated everything** — motion is reserved for the page entry, live states, and hover. No confetti, no continuous ambient motion.
|
| 107 |
+
|
| 108 |
+
## Where the polish is applied
|
| 109 |
+
|
| 110 |
+
| Page | Level of polish |
|
| 111 |
+
| -------------------------------- | ------------------------------------------------------------------ |
|
| 112 |
+
| `/` (Characters) | Full — new header, card partial, brass rule, enter choreography. |
|
| 113 |
+
| `/discovery` | Full — eyebrows, three sectioned surfaces, reused partials. |
|
| 114 |
+
| `/matches/{id}` (Play) | Full — board in brass frame, typography, thinking state, disconnect overlay. |
|
| 115 |
+
| `/matches/{id}/watch` (Spectate) | Full — read-only variant, crowd-noise panel with blue accent. |
|
| 116 |
+
| `/matches/{id}/summary` | Full — payoff screen: dossier-style result, italicised memories, brass accent on opponent note. |
|
| 117 |
+
| `/leaderboard/characters` | Full — library-catalog tables, rank highlighted for top 3 in brass. |
|
| 118 |
+
| `/leaderboard/players` | Full — same treatment, current user row highlighted. |
|
| 119 |
+
| `/characters/{id}` (Detail) | Header polished; hall-of-fame + memory sections on new tokens; body form sections inherit typography but still use Tailwind neutral classes. |
|
| 120 |
+
| `/characters/new`, `/edit` | Light — new heading + brass submit button; form body inherits typography from base. |
|
| 121 |
+
| `/settings` | Full — panel groupings, brass radio accent. |
|
| 122 |
+
| `/login`, `/landing` | Full — typographic hero, framed form, member-entrance eyebrow. |
|
| 123 |
+
| `/players/{username}` (Profile) | Full — dossier header, match rows, character card grid. |
|
| 124 |
+
|
| 125 |
+
## Known gaps (for a follow-up polish)
|
| 126 |
+
|
| 127 |
+
- Character-detail memory ribbon section (body area below hero) still uses Tailwind neutral classes. Readable; out of palette.
|
| 128 |
+
- `new.html`/`edit.html` form body (inputs, fieldsets, sliders) not yet fully converted to `.mp-input` + `.mp-panel` idioms — the brass submit button + new header bring them into the system but the fields themselves retain the Phase 3a styling.
|
| 129 |
+
- `rating_hidden.html` (shown when a character exceeds the viewer's rating) untouched — low-traffic page.
|
| 130 |
+
- No responsive breakpoint review yet — the layout degrades gracefully on mobile thanks to Tailwind's `grid-cols-1` fallbacks but there's no dedicated mobile pass.
|