The adventure purple was unreadable on night, and measuring the family showed it was not alone: every one of the eleven .text-theme-* colours lands between 1.08:1 (eu, effectively invisible) and 3.12:1 (finance) on night's #2d365a. They were all picked against a light card and never checked against a dark one. Same hue, lifted lightness, saturation floored so the dull ones stay a colour instead of going grey — all now >=5.5:1. Night only. Dusk and dawn are lit cards despite their names, so they keep today's values exactly and the light phases are pixel-identical. Lego can't stay pillar-box red and be legible on navy; a red light enough to pass reads as salmon, which is the honest trade against a red nobody can see. Also adds --warn as a phase variable for the new "inert" chip, for the same reason --ink is one. Tailwind's dark: variant cannot do this job: darkMode is unconfigured, so dark: follows the OS's prefers-color-scheme, which knows nothing about which phase Pete is showing. That mismatch left the chip at 2.34:1 on a dark phase under a light OS, and 1.55:1 on a light phase under a dark OS — half the combinations unreadable, decided by a setting outside the page. Now 4.52-7.04 across all four phases, OS irrelevant. Still open, and NOT fixed here: text-[color:var(--ink)]/NN appears not to compile — there is no such rule in output.css, and the item description computes to a fixed rgb(74,46,42) that ignores the phase entirely. If that holds it is a pre-existing site-wide no-op, not confined to these panels. Recorded rather than guessed at.
331 lines
17 KiB
HTML
331 lines
17 KiB
HTML
{{define "title"}}{{.Mark.Name}} — {{.SiteTitle}}{{end}}
|
||
|
||
{{/* One item, wherever it's sitting. Worn, backpack, and vault all show the
|
||
same facts; only the panel around them differs. The description and the
|
||
effect line come from gogobee already resolved — Pete renders them, it does
|
||
not compute them. "inert" is the honest word for a worn item that wants a
|
||
bond and hasn't got one: it's on you and doing nothing. */}}
|
||
{{define "itemrow"}}
|
||
<li>
|
||
<div class="flex items-baseline justify-between gap-3">
|
||
<span class="flex-1 font-semibold">{{.Name}}{{if .Temper}} <span class="text-theme-adventure">+{{.Temper}}</span>{{end}}</span>
|
||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">{{if .Tier}}T{{.Tier}}{{end}}{{if .Value}}{{if .Tier}} · {{end}}{{.Value}}g{{end}}</span>
|
||
</div>
|
||
{{if or .Slot .SkillSource .Attunement}}
|
||
<div class="flex flex-wrap gap-1.5 mt-1">
|
||
{{if .Slot}}<span class="text-[11px] rounded-full bg-[color:var(--ink)]/5 px-2 py-0.5 text-[color:var(--ink)]/60">{{.Slot}}</span>{{end}}
|
||
{{if .SkillSource}}<span class="text-[11px] rounded-full bg-[color:var(--ink)]/5 px-2 py-0.5 text-[color:var(--ink)]/60">{{.SkillSource}}</span>{{end}}
|
||
{{if .Attunement}}{{if .Attuned}}<span class="text-[11px] rounded-full bg-theme-adventure/20 px-2 py-0.5 text-theme-adventure font-semibold">bonded</span>{{else if .Worn}}<span class="text-[11px] rounded-full bg-amber-400/20 px-2 py-0.5 text-[color:var(--warn)] font-semibold">inert · no bond free</span>{{else}}<span class="text-[11px] rounded-full bg-[color:var(--ink)]/10 px-2 py-0.5 text-[color:var(--ink)]/60">needs a bond</span>{{end}}{{end}}
|
||
</div>
|
||
{{end}}
|
||
{{if .Desc}}<p class="text-xs text-[color:var(--ink)]/55 mt-1 leading-snug">{{.Desc}}</p>{{end}}
|
||
{{if .Effect}}<p class="text-xs text-theme-adventure/80 mt-0.5">{{.Effect}}</p>{{end}}
|
||
</li>
|
||
{{end}}
|
||
|
||
{{define "main"}}
|
||
<article class="mt-2 mb-10 max-w-3xl mx-auto" id="who" data-token="{{.Mark.Token}}">
|
||
<nav class="mb-4">
|
||
<a href="/adventure" class="inline-flex items-center gap-1.5 text-sm font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] transition">
|
||
<span aria-hidden="true">←</span> The board
|
||
</a>
|
||
</nav>
|
||
|
||
<header class="rounded-3xl bg-theme-adventure text-white p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
||
<div class="absolute -top-6 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">{{if .Mark.OnRun}}⚔{{else}}🏠{{end}}</div>
|
||
<div class="relative">
|
||
<p class="text-sm uppercase tracking-[0.2em] opacity-80">adventurer</p>
|
||
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-2 leading-tight">{{.Mark.Name}}</h1>
|
||
<p class="mt-2 opacity-90">Level {{.Mark.Level}} {{.Mark.ClassRace}}</p>
|
||
<p class="mt-4 text-xs uppercase tracking-wider opacity-75" id="who-where">
|
||
{{if .Mark.OnRun}}{{.Mark.Where}}{{else}}In town{{if .Mark.Idle}} · {{.Mark.Idle}}{{end}}{{end}}
|
||
</p>
|
||
</div>
|
||
</header>
|
||
|
||
{{if .HasDetail}}
|
||
<!-- Stats + gear: public, the same anonymity model as the board. -->
|
||
<section class="mt-8 grid gap-6 sm:grid-cols-2">
|
||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||
<h2 class="font-display text-xl font-bold mb-4">Vitals</h2>
|
||
<div class="flex items-baseline justify-between">
|
||
<span class="text-sm uppercase tracking-wider text-[color:var(--ink)]/50">Hit points</span>
|
||
<span class="font-semibold" id="who-hp">{{.Detail.HPCurrent}} / {{.Detail.HPMax}}{{if .Detail.TempHP}} <span class="text-theme-adventure">+{{.Detail.TempHP}}</span>{{end}}</span>
|
||
</div>
|
||
<div class="mt-2 h-2 rounded-full bg-[color:var(--ink)]/10 overflow-hidden">
|
||
<div class="h-full rounded-full bg-theme-adventure transition-all" id="who-hp-bar" style="width:0%"></div>
|
||
</div>
|
||
<div class="mt-4 flex items-baseline justify-between">
|
||
<span class="text-sm uppercase tracking-wider text-[color:var(--ink)]/50">Armor class</span>
|
||
<span class="font-semibold" id="who-ac">{{.Detail.ArmorClass}}</span>
|
||
</div>
|
||
|
||
<div class="mt-5 grid grid-cols-3 gap-2">
|
||
{{range .Abilities}}
|
||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50">{{.Label}}</div>
|
||
<div class="font-display text-lg font-bold leading-none mt-1">{{.Score}}</div>
|
||
<div class="text-xs text-[color:var(--ink)]/60">{{.ModStr}}</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
{{if .Mark.OnRun}}
|
||
<div class="mt-5 pt-4 border-t border-[color:var(--ink)]/10 space-y-1.5 text-sm" id="who-expedition">
|
||
{{if .Detail.Room}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Room</span><span class="font-semibold" id="who-room">{{.Detail.Room}}</span></div>{{end}}
|
||
<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Supplies</span><span class="font-semibold" id="who-supplies">{{.Detail.Supplies}}</span></div>
|
||
{{if .Detail.ThreatLevel}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Threat</span><span class="font-semibold" id="who-threat">{{.Detail.ThreatLevel}}</span></div>{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||
<h2 class="font-display text-xl font-bold mb-4">Gear</h2>
|
||
{{if .Detail.Gear}}
|
||
<ul class="space-y-2">
|
||
{{range .Detail.Gear}}
|
||
<li class="flex items-baseline justify-between gap-3">
|
||
<span class="text-xs uppercase tracking-wider text-[color:var(--ink)]/45 w-16 shrink-0">{{.Slot}}</span>
|
||
<span class="font-semibold flex-1">{{.Name}}{{if .Masterwork}} <span title="masterwork">★</span>{{end}}</span>
|
||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">T{{.Tier}}{{if .Condition}} · {{.Condition}}%{{end}}</span>
|
||
</li>
|
||
{{end}}
|
||
</ul>
|
||
{{else}}
|
||
<p class="text-sm text-[color:var(--ink)]/50">Traveling light — nothing equipped.</p>
|
||
{{end}}
|
||
</div>
|
||
</section>
|
||
{{else}}
|
||
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||
<p class="text-sm text-[color:var(--ink)]/60">No detailed sheet on file for this adventurer yet — check back after the next snapshot.</p>
|
||
</section>
|
||
{{end}}
|
||
|
||
{{if .HasHistory}}
|
||
<!-- The record. Public, like the dispatches it's counted from — this is the
|
||
same information the /adventure feed already printed, only as numbers
|
||
instead of as forty separate sentences. Not a gogobee snapshot: Pete
|
||
counted these itself out of the facts it kept. -->
|
||
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||
<h2 class="font-display text-xl font-bold mb-4">The record</h2>
|
||
|
||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
||
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.BossKills}}</div>
|
||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">bosses down</div>
|
||
</div>
|
||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
||
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.ZoneClears}}</div>
|
||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">zones cleared</div>
|
||
</div>
|
||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
||
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.Deaths}}</div>
|
||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">death{{if ne .Trophies.Deaths 1}}s{{end}}</div>
|
||
</div>
|
||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
||
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.Retreats}}</div>
|
||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">walked out</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{if or .Trophies.BossFirsts .Trophies.ZoneFirsts}}
|
||
<p class="mt-3 text-sm text-theme-adventure font-semibold">
|
||
★ {{.Trophies.BossFirsts}} realm-first boss{{if ne .Trophies.BossFirsts 1}}es{{end}}{{if .Trophies.ZoneFirsts}}, {{.Trophies.ZoneFirsts}} first clear{{if ne .Trophies.ZoneFirsts 1}}s{{end}}{{end}} — nobody had done it before.
|
||
</p>
|
||
{{end}}
|
||
|
||
<div class="mt-6 grid gap-6 sm:grid-cols-2">
|
||
{{if .Trophies.Bosses}}
|
||
<div>
|
||
<h3 class="font-display text-lg font-bold mb-3">Bosses fought</h3>
|
||
<ul class="space-y-1.5 text-sm max-h-56 overflow-y-auto pr-1">
|
||
{{range .Trophies.Bosses}}
|
||
<li class="flex items-baseline justify-between gap-3">
|
||
<span class="flex-1">{{.Boss}}{{if .First}} <span class="text-theme-adventure" title="first in the realm to clear it">★</span>{{end}}</span>
|
||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">×{{.Kills}}</span>
|
||
</li>
|
||
{{end}}
|
||
</ul>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{if .Trophies.Zones}}
|
||
<div>
|
||
<h3 class="font-display text-lg font-bold mb-3">Ground covered</h3>
|
||
<ul class="space-y-1.5 text-sm max-h-56 overflow-y-auto pr-1">
|
||
{{range .Trophies.Zones}}
|
||
<li class="flex items-baseline justify-between gap-3">
|
||
<span class="flex-1">{{.Zone}}{{if .First}} <span class="text-theme-adventure" title="first clear in the realm">★</span>{{end}}{{if .Region}} <span class="text-[color:var(--ink)]/45">{{.Region}}</span>{{end}}</span>
|
||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">×{{.Clears}}</span>
|
||
</li>
|
||
{{end}}
|
||
</ul>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
{{if .Trophies.Milestones}}
|
||
<div class="mt-6 pt-4 border-t border-[color:var(--ink)]/10">
|
||
<h3 class="font-display text-lg font-bold mb-3">Milestones</h3>
|
||
<div class="flex flex-wrap gap-2">
|
||
{{range .Trophies.Milestones}}
|
||
<span class="rounded-full bg-theme-adventure/10 text-theme-adventure text-xs font-semibold px-3 py-1">🏅 {{.}}</span>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</section>
|
||
|
||
<!-- The trail: every fact we have, newest first, each linking to the dispatch
|
||
that told it. -->
|
||
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||
<h2 class="font-display text-xl font-bold mb-4">The trail</h2>
|
||
<ol class="relative border-l-2 border-[color:var(--ink)]/10 ml-3 space-y-4">
|
||
{{range .Timeline}}
|
||
<li class="relative pl-6">
|
||
<span class="absolute -left-[13px] top-0.5 flex h-6 w-6 items-center justify-center rounded-full bg-[color:var(--card)] border-2 {{if .Notable}}border-theme-adventure{{else}}border-[color:var(--ink)]/10{{end}} text-xs" aria-hidden="true">{{.Emoji}}</span>
|
||
<a href="{{.Permalink}}" class="group block">
|
||
<div class="flex items-baseline justify-between gap-3">
|
||
<span class="font-semibold group-hover:text-theme-adventure transition {{if .Notable}}text-theme-adventure{{end}}">{{.Label}}</span>
|
||
<span class="text-xs text-[color:var(--ink)]/45 shrink-0">{{.When}}</span>
|
||
</div>
|
||
{{if .Line}}<p class="text-sm text-[color:var(--ink)]/60 mt-0.5">{{.Line}}</p>{{end}}
|
||
</a>
|
||
</li>
|
||
{{end}}
|
||
</ol>
|
||
{{if .MoreHistory}}
|
||
<p class="mt-5 pt-4 border-t border-[color:var(--ink)]/10 text-xs text-[color:var(--ink)]/45">
|
||
Showing the most recent {{len .Timeline}}. The counts above cover everything.
|
||
</p>
|
||
{{end}}
|
||
</section>
|
||
{{end}}
|
||
|
||
{{if .HasSelf}}
|
||
<!-- Owner-only: this is you. Inventory, vault, house, pets — private, served
|
||
only because your signed-in localpart owns this page's token. -->
|
||
<section class="mt-8">
|
||
<div class="flex items-center gap-2 mb-3">
|
||
<span class="rounded-full bg-theme-adventure text-white text-xs font-semibold px-3 py-1">Your adventurer</span>
|
||
<span class="text-xs text-[color:var(--ink)]/45">only you can see the panels below</span>
|
||
</div>
|
||
|
||
<div class="grid gap-6 sm:grid-cols-2">
|
||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||
<h2 class="font-display text-xl font-bold mb-4">Home</h2>
|
||
<div class="space-y-1.5 text-sm">
|
||
<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">House tier</span><span class="font-semibold">{{if .Self.House.Tier}}{{.Self.House.Tier}}{{else}}no house{{end}}</span></div>
|
||
{{if .Self.House.LoanBalance}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Loan balance</span><span class="font-semibold">{{.Self.House.LoanBalance}}</span></div>{{end}}
|
||
{{if .Self.House.Rate}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Rate</span><span class="font-semibold">{{.Self.House.Rate}}</span></div>{{end}}
|
||
{{if .Self.House.Autopay}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Autopay</span><span class="font-semibold">on</span></div>{{end}}
|
||
</div>
|
||
|
||
<h3 class="font-display text-lg font-bold mt-6 mb-3">Pets</h3>
|
||
{{if .Self.Pets}}
|
||
<ul class="space-y-2 text-sm">
|
||
{{range .Self.Pets}}
|
||
<li class="flex items-baseline justify-between gap-3">
|
||
<span class="font-semibold flex-1">{{if .Name}}{{.Name}}{{else}}your {{.Type}}{{end}} <span class="text-[color:var(--ink)]/45 font-normal">{{.Type}}</span></span>
|
||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">lv {{.Level}}{{if .ArmorTier}} · barding T{{.ArmorTier}}{{end}}</span>
|
||
</li>
|
||
{{end}}
|
||
</ul>
|
||
{{else}}
|
||
<p class="text-sm text-[color:var(--ink)]/50">No pets right now.</p>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||
{{if .Worn}}
|
||
<h2 class="font-display text-xl font-bold mb-1">Worn</h2>
|
||
<p class="text-xs text-[color:var(--ink)]/45 mb-3">{{.BondsUsed}} of 3 bonds in use</p>
|
||
<ul class="space-y-2.5 text-sm">
|
||
{{range .Worn}}{{template "itemrow" .}}{{end}}
|
||
</ul>
|
||
<h3 class="font-display text-lg font-bold mt-6 mb-3">Backpack</h3>
|
||
{{else}}
|
||
<h2 class="font-display text-xl font-bold mb-4">Backpack</h2>
|
||
{{end}}
|
||
{{if .Backpack}}
|
||
<ul class="space-y-2.5 text-sm max-h-72 overflow-y-auto pr-1">
|
||
{{range .Backpack}}{{template "itemrow" .}}{{end}}
|
||
</ul>
|
||
{{else}}
|
||
<p class="text-sm text-[color:var(--ink)]/50">Backpack's empty.</p>
|
||
{{end}}
|
||
|
||
{{if .VaultRows}}
|
||
<h3 class="font-display text-lg font-bold mt-6 mb-3">Vault</h3>
|
||
<ul class="space-y-2.5 text-sm max-h-52 overflow-y-auto pr-1">
|
||
{{range .VaultRows}}{{template "itemrow" .}}{{end}}
|
||
</ul>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
{{end}}
|
||
</article>
|
||
|
||
<script>
|
||
// The sheet is state, like the board: keep an open tab honest without a reload.
|
||
// Public detail only (HP, room, supplies) — the private panels stay as rendered.
|
||
(function () {
|
||
var root = document.getElementById('who');
|
||
if (!root) return;
|
||
var token = root.getAttribute('data-token');
|
||
|
||
function setBar() {
|
||
var el = document.getElementById('who-hp-bar');
|
||
var hp = document.getElementById('who-hp');
|
||
if (!el || !hp) return;
|
||
var m = hp.textContent.match(/(-?\d+)\s*\/\s*(\d+)/);
|
||
if (!m) return;
|
||
var cur = parseInt(m[1], 10), max = parseInt(m[2], 10);
|
||
var pct = max > 0 ? Math.max(0, Math.min(100, Math.round(cur / max * 100))) : 0;
|
||
el.style.width = pct + '%';
|
||
}
|
||
setBar();
|
||
|
||
function txt(id, v) { var el = document.getElementById(id); if (el && v != null) el.textContent = v; }
|
||
|
||
var timer = null;
|
||
function refresh() {
|
||
fetch('/api/adventure/who/' + encodeURIComponent(token), { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (data) {
|
||
if (!data) return;
|
||
if (!data.live) {
|
||
// Off the board entirely (expedition ended, opted out): nothing more to
|
||
// poll for, so stop the timer rather than hammer a token that's gone.
|
||
txt('who-where', 'Back in town');
|
||
if (timer) clearInterval(timer);
|
||
return;
|
||
}
|
||
var d = data.detail || {};
|
||
var mark = data.mark || {};
|
||
if (data.has_detail) {
|
||
var temp = d.temp_hp ? ' +' + d.temp_hp : '';
|
||
txt('who-hp', d.hp_current + ' / ' + d.hp_max + temp);
|
||
txt('who-ac', d.armor_class);
|
||
txt('who-room', d.room);
|
||
txt('who-supplies', d.supplies);
|
||
txt('who-threat', d.threat_level);
|
||
setBar();
|
||
}
|
||
// Keep the location line honest in both directions: a mark that came back
|
||
// to town while still on the board must stop showing its old expedition.
|
||
if (mark.OnRun) {
|
||
txt('who-where', mark.Where);
|
||
} else {
|
||
txt('who-where', 'In town' + (mark.Idle ? ' · ' + mark.Idle : ''));
|
||
}
|
||
})
|
||
.catch(function () { /* transient — next tick will do */ });
|
||
}
|
||
timer = setInterval(refresh, 60000);
|
||
})();
|
||
</script>
|
||
{{end}}
|