Design pass: the client becomes Eric's table

The design system is the physical set itself, taken from the scans:
card-stock cream, warm ink, and the six wizard marker colors, laid on
a dark leather tabletop. Cards in your hand are now the actual cards —
vertical corner rails (ATTACK / L.O.S. / ADJACENT), condensed gothic
titles, brown body text on cream, number cards as big bordered
numerals with per-value border styles; selecting lifts a card from
the fan. The board matches the physical tiles: ink-outlined white
brick walls on cracked flagstone, plank doors, eight-point home stars
in each wizard's color, swirled treasure discs, warp arrows. The
sidebar is paper: a turn slip, the rulebook's own hand-scrawled
life-points score sheet (Caveat over ruled lines), and a typewritten
chronicle that scrolls itself. The lobby is the box lid. Type:
Oswald condensed display / Archivo Narrow body / Courier Prime log.
Buttons are ink stamps. Board height-caps so board, actions, and the
full hand share one screen; responsive stack below 900px; reduced
motion respected. Verified with live screenshots at 1440x900.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 23:21:03 -04:00
co-authored by Claude Fable 5
parent c9524a37db
commit 9501a85be4
4 changed files with 705 additions and 243 deletions
+199
View File
@@ -0,0 +1,199 @@
<script lang="ts">
import { cardDef } from "@wizwar/engine";
import type { CardInstance } from "@wizwar/engine";
let {
card,
selected = false,
attached = false,
marked = false,
displayed = false,
charges = null,
onclick,
}: {
card: CardInstance;
selected?: boolean;
attached?: boolean;
marked?: boolean;
displayed?: boolean;
charges?: number | null;
onclick?: () => void;
} = $props();
const def = $derived(cardDef(card.cardId));
const isNumber = $derived(def.cardType === "number");
const leftRail = $derived.by(() => {
switch (def.cardType) {
case "attack": return "ATTACK";
case "neutral": return "NEUTRAL";
case "counteraction": return "COUNTERACTION";
case "neutral/counteraction": return "NEUTRAL/COUNTERACTION";
case "trap": return "TRAP";
default: return "";
}
});
const rightRail = $derived(def.los ? "L.O.S." : def.adjacent ? "ADJACENT" : "");
</script>
<button
class="card"
class:selected
class:attached
class:marked
class:number={isNumber}
{onclick}
title={def.text ?? def.name}
>
{#if isNumber}
<span class="corner tl">{def.value}</span>
<span class="numeral" data-value={def.value}>{def.value}</span>
<span class="brand">Wiz-War</span>
<span class="corner br">{def.value}</span>
{:else}
{#if leftRail}<span class="rail left">{leftRail}</span>{/if}
{#if rightRail}<span class="rail right">{rightRail}</span>{/if}
<span class="name">{def.name}</span>
{#if def.text}<span class="text">{def.text}</span>{/if}
{/if}
{#if displayed}
<span class="badge">DISPLAYED{charges != null ? ` · ${charges}` : ""}</span>
{/if}
</button>
<style>
.card {
position: relative;
width: 8.2rem;
height: 11.4rem;
flex: 0 0 auto;
background:
radial-gradient(ellipse at 30% 20%, rgba(255, 252, 240, 0.5), transparent 60%),
#e9e1cb;
border: 1px solid #b3a687;
border-radius: 7px;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.5) inset,
0 3px 8px rgba(10, 8, 4, 0.45);
color: #5f4a33;
padding: 0.55rem 1.05rem 0.5rem;
text-align: center;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
transition: transform 120ms ease, box-shadow 120ms ease;
}
.card:hover {
transform: translateY(-0.55rem);
box-shadow: 0 8px 18px rgba(10, 8, 4, 0.55);
z-index: 3;
}
.card:focus-visible {
outline: 2px solid #d8b23a;
outline-offset: 2px;
}
.card.selected {
transform: translateY(-0.8rem);
box-shadow:
0 0 0 2.5px #d8b23a,
0 10px 20px rgba(10, 8, 4, 0.6);
}
.card.attached {
transform: translateY(-0.5rem);
box-shadow: 0 0 0 2.5px #2e7d32, 0 8px 16px rgba(10, 8, 4, 0.55);
}
.card.marked {
box-shadow: 0 0 0 2.5px #b3372b, 0 4px 10px rgba(10, 8, 4, 0.5);
opacity: 0.85;
}
.rail {
position: absolute;
top: 0.55rem;
font-family: "Oswald", sans-serif;
font-size: 0.42rem;
font-weight: 500;
letter-spacing: 0.14em;
color: #4a3a26;
writing-mode: vertical-lr;
text-orientation: upright;
line-height: 1;
max-height: 9.5rem;
overflow: hidden;
}
.rail.left { left: 0.28rem; }
.rail.right { right: 0.28rem; }
.name {
font-family: "Oswald", sans-serif;
font-weight: 600;
font-size: 0.78rem;
line-height: 1.05;
letter-spacing: 0.02em;
text-transform: uppercase;
color: #43331f;
margin: 0.35rem 0 0.3rem;
}
.text {
font-family: "Archivo Narrow", sans-serif;
font-size: 0.56rem;
line-height: 1.28;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 9;
line-clamp: 9;
-webkit-box-orient: vertical;
}
/* Number cards: a big numeral in a bordered plate, like the originals. */
.numeral {
font-family: "Oswald", sans-serif;
font-weight: 700;
font-size: 3.4rem;
line-height: 1;
color: #43331f;
margin-top: 1.6rem;
padding: 0.4rem 1rem 0.5rem;
border: 2px solid #5f4a33;
}
.numeral[data-value="3"] { border-style: dashed; }
.numeral[data-value="4"] { border-radius: 12px; }
.numeral[data-value="5"] { border-style: double; border-width: 4px; }
.numeral[data-value="6"] { border-style: none; outline: 2px dotted #5f4a33; outline-offset: 4px; }
.brand {
font-family: "Oswald", sans-serif;
font-size: 0.6rem;
letter-spacing: 0.06em;
margin-top: 0.45rem;
}
.corner {
position: absolute;
font-family: "Oswald", sans-serif;
font-weight: 600;
font-size: 0.75rem;
}
.corner.tl { top: 0.3rem; left: 0.45rem; }
.corner.br { bottom: 0.3rem; right: 0.45rem; transform: rotate(180deg); }
.badge {
position: absolute;
bottom: 0.3rem;
left: 50%;
transform: translateX(-50%);
background: #43331f;
color: #e9e1cb;
font-family: "Oswald", sans-serif;
font-size: 0.45rem;
letter-spacing: 0.12em;
padding: 0.12rem 0.4rem;
border-radius: 2px;
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
.card:hover, .card.selected, .card.attached { transform: none; }
}
</style>