adventure: give the realm a map, a board and a history

Everything Pete published so far was either the present moment (the roster,
the Siege bar) or one thing that happened (a dispatch, a run report). None of
it said what this place IS — how many zones there are, which are harder, or
whether anybody has ever actually beaten them.

Three pages off one snapshot, because they are one question and every number
on all three comes off the same scan of the same run history upstream:

  /adventure/realm      the world, in difficulty order, with who is inside it
  /adventure/standings  the board, plus Pete's own duel record
  /adventure/firsts     the hall of firsts, as a dated history

The map is deliberately not who_map.go's layout engine. That lays out a graph,
and the realm has no edges — zones aren't connected, you pick one and go.
Forcing a graph onto a set would imply a topology the game doesn't have. What
it has is an order, so tier bands are what get drawn.

A zone nobody has ever cleared looks different rather than saying so in small
text, and that styling keys off the clear count, never off whether a name is
attached — otherwise an opt-out would silently redraw a conquered place as one
nobody has come out of.

The per-kind first dot goes through a custom property instead of a
.firsts-entry-zone::before rule: input.css is in Tailwind's content glob, so a
hand-written class survives the purge only if its literal name can be lifted
out of the file, and a name glued to ::before cannot. It failed silently.

Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
This commit is contained in:
prosolis
2026-07-24 17:54:49 -07:00
parent b4a276da36
commit 1dfd3ac9fb
12 changed files with 1745 additions and 7 deletions
+133
View File
@@ -2864,4 +2864,137 @@ html[data-room] .pete-felt {
the document instead of trapping the whole run in a 26rem window that a
reader has to find the edge of before they can move through it. */
.runlog-full { max-height: none; overflow-y: visible; }
/* ── The realm: map, board, hall of firsts ──────────────────────────────
These are hand-written component classes, not generated utilities, and that
matters: a Tailwind class built from a template value gets purged out of the
stylesheet and fails SILENTLY. .standings-tier-{{.DeepestTier}} is exactly
that shape, so all six variants are spelled out below rather than composed.
If a seventh tier ever ships, it needs a line here.
The realm's accent is the adventure purple, unlike the Siege's ember — the
Siege is an emergency, the realm is the standing shape of the world. */
/* Tabs across the four standing pages. */
.realm-tab {
font-weight: 600;
color: color-mix(in srgb, var(--ink) 55%, transparent);
border-bottom: 2px solid transparent;
padding-bottom: 0.15rem;
transition: color 0.15s ease, border-color 0.15s ease;
}
.realm-tab:hover { color: var(--ink); }
.realm-tab-on {
color: #6d4bd8;
border-bottom-color: #6d4bd8;
}
/* The one place in this block a raw purple is set as a foreground rather than
mixed into --ink, so the one place that needs the night override the
existing .text-theme-adventure rule already carries. Everything else here
goes through color-mix and lands readable in all four phases by itself. */
html[data-phase="night"] .realm-tab-on {
color: #baa9eb;
border-bottom-color: #baa9eb;
}
/* A zone card. The default is a place people go: readable, unremarkable. */
.realm-zone {
border-radius: 1.25rem;
padding: 1rem 1.15rem;
background: var(--card);
border: 2px solid color-mix(in srgb, var(--ink) 10%, transparent);
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
transition: border-color 0.15s ease, transform 0.15s ease;
}
.realm-zone:hover { transform: translateY(-1px); }
/* A zone NOBODY has ever cleared. This is the one piece of styling on the page
that has to carry a fact on its own, without being read: a visitor scanning
the map should see where the realm ends before they read a word of it.
Desaturated, dashed, recessed — the opposite of the busy state below. */
.realm-zone-unbeaten {
background: color-mix(in srgb, var(--ink) 5%, var(--card));
border-color: color-mix(in srgb, var(--ink) 18%, transparent);
border-style: dashed;
}
.realm-unbeaten-line {
color: color-mix(in srgb, var(--ink) 52%, transparent);
font-style: italic;
letter-spacing: 0.01em;
}
/* Somebody is in there right now. It wins over the unbeaten styling by source
order, deliberately: a party currently inside a place nobody has ever beaten
is the most interesting card on the page and should read as live, not dead. */
.realm-zone-busy {
border-color: color-mix(in srgb, #6d4bd8 45%, transparent);
border-style: solid;
background: color-mix(in srgb, #6d4bd8 5%, var(--card));
}
/* The postgame band, drawn apart from the rest of the map: it is gated content
and the page should look like it changes character there. */
.realm-band-postgame {
background: color-mix(in srgb, var(--ink) 6%, var(--card));
border: 2px dashed color-mix(in srgb, var(--ink) 20%, transparent);
}
/* The board's deepest-tier chip. Six literal classes on purpose — see the note
at the top of this block. The ramp runs cool-to-hot so a column of them
reads as a gradient of how far people have got. */
.standings-tier {
display: inline-flex; align-items: center;
font-size: 11px; font-weight: 700; line-height: 1;
border-radius: 9999px; padding: 0.25rem 0.5rem;
border: 1px solid transparent;
}
.standings-tier-1 { color: color-mix(in srgb, #6b7280 70%, var(--ink)); background: color-mix(in srgb, #6b7280 12%, var(--card)); border-color: color-mix(in srgb, #6b7280 30%, transparent); }
.standings-tier-2 { color: color-mix(in srgb, #3fa66a 65%, var(--ink)); background: color-mix(in srgb, #3fa66a 12%, var(--card)); border-color: color-mix(in srgb, #3fa66a 30%, transparent); }
.standings-tier-3 { color: color-mix(in srgb, #2f7fd0 65%, var(--ink)); background: color-mix(in srgb, #2f7fd0 12%, var(--card)); border-color: color-mix(in srgb, #2f7fd0 30%, transparent); }
.standings-tier-4 { color: color-mix(in srgb, #8b5cf6 65%, var(--ink)); background: color-mix(in srgb, #8b5cf6 12%, var(--card)); border-color: color-mix(in srgb, #8b5cf6 32%, transparent); }
.standings-tier-5 { color: color-mix(in srgb, #e0562f 65%, var(--ink)); background: color-mix(in srgb, #e0562f 13%, var(--card)); border-color: color-mix(in srgb, #e0562f 34%, transparent); }
.standings-tier-6 { color: color-mix(in srgb, #c9a227 72%, var(--ink)); background: color-mix(in srgb, #c9a227 15%, var(--card)); border-color: color-mix(in srgb, #c9a227 40%, transparent); font-weight: 800; }
/* A realm-first count. Gold, because it is the one number on the board that
can never go up for anybody else once it has been claimed. */
.standings-firsts {
color: color-mix(in srgb, #c9a227 72%, var(--ink));
font-weight: 700;
}
/* The hall of firsts: a ruled ledger with a spine, so a long list reads as a
record rather than as a feed. */
.firsts-ledger {
border-left: 2px solid color-mix(in srgb, var(--ink) 12%, transparent);
padding-left: 1.15rem;
}
.firsts-entry {
position: relative;
padding: 0.7rem 0;
border-bottom: 1px solid color-mix(in srgb, var(--ink) 7%, transparent);
}
.firsts-entry:last-child { border-bottom: 0; }
.firsts-entry::before {
content: "";
position: absolute;
left: -1.45rem; top: 1.15rem;
width: 0.5rem; height: 0.5rem;
border-radius: 9999px;
background: var(--first-dot, color-mix(in srgb, var(--ink) 25%, var(--card)));
}
/* The per-kind dot colour goes through a custom property rather than through
a `.firsts-entry-zone::before` rule, and that is a purge fix, not a style
preference. tailwind.config.js has input.css itself in its content glob, so
a hand-written component class survives the purge only when the extractor
can lift its literal name out of this file — and a class name glued to
`::before` does not extract. It fails SILENTLY, which is how it got noticed
here only because the rule was grepped for afterwards. Any new
`.realm-*`/`.firsts-*` variant wants a plain-selector declaration for the
same reason. */
.firsts-entry-zone { --first-dot: #6d4bd8; }
.firsts-entry-treasure { --first-dot: #c9a227; }
@media (prefers-reduced-motion: reduce) {
.realm-zone:hover { transform: none; }
}
}