Every card wears its FAQ seal; library cards enlarge above the booklet

The FAQ affordance moves onto the card itself: a small dark seal in
the corner of any card with official rulings, everywhere cards render
— hand, inspector, peek, final reveal, discard pile, card library —
opening the rulings modal on tap (the link, never the text: card
faces stay verbatim card text). The separate inspector buttons and
library chips retire in its favor.

And clicking a library card mid-game no longer summons the App-level
inspector beneath the help modal, greyed out and useless — the
library enlarges its card in its own overlay, centered above the
booklet, seal included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 15:34:43 -04:00
co-authored by Claude Fable 5
parent 4833a229e1
commit 5375941ed3
5 changed files with 184 additions and 39 deletions
+5 -25
View File
@@ -757,7 +757,7 @@
</header>
<div class="discard-grid">
{#each [...view.discardPile].reverse() as card (card.instanceId)}
<Card {card} onclick={() => (peekCard = card)} />
<Card {card} onclick={() => (peekCard = card)} onfaq={(id) => (faqCardId = id)} />
{/each}
{#if view.discardPile.length === 0}
<span class="discard-empty">Nothing has been cast away yet.</span>
@@ -770,11 +770,8 @@
{#if peekCard}
<div class="inspector" role="img" aria-label="displayed card, enlarged">
<button class="inspector-close" onclick={() => { peekCard = null; peekCreatureId = null; }} aria-label="hide card">×</button>
<div class="inspector-card"><Card card={peekCard} /></div>
<div class="inspector-card"><Card card={peekCard} onfaq={(id) => (faqCardId = id)} /></div>
{#if peekNote}<div class="peek-note">{peekNote}</div>{/if}
{#if cardDef(peekCard.cardId).faqRulings.length > 0}
<button class="faq-link" onclick={() => (faqCardId = peekCard!.cardId)}>Official FAQ</button>
{/if}
</div>
{/if}
@@ -1340,11 +1337,8 @@
<div class="inspector" role="img" aria-label="selected card, enlarged">
<button class="inspector-close" onclick={() => (inspectorHidden = true)} aria-label="hide enlarged card">×</button>
<div class="inspector-card">
<Card card={selectedCard} />
<Card card={selectedCard} onfaq={(id) => (faqCardId = id)} />
</div>
{#if cardDef(selectedCard.cardId).faqRulings.length > 0}
<button class="faq-link" onclick={() => (faqCardId = selectedCard!.cardId)}>Official FAQ</button>
{/if}
</div>
{/if}
@@ -1364,7 +1358,7 @@
</span>
<div class="reveal-cards">
{#each view.revealedHands[p.id] ?? [] as card (card.instanceId)}
<Card {card} onclick={() => (peekCard = card)} />
<Card {card} onclick={() => (peekCard = card)} onfaq={(id) => (faqCardId = id)} />
{/each}
{#if (view.revealedHands[p.id] ?? []).length === 0}
<span class="reveal-empty">empty-handed</span>
@@ -1389,6 +1383,7 @@
glowing={interruptCards.some((c) => c.instanceId === card.instanceId)}
charges={view.wandCharges[card.instanceId] ?? null}
onclick={() => selectCard(card)}
onfaq={(id) => (faqCardId = id)}
/>
{/each}
</div>
@@ -1988,21 +1983,6 @@
}
.discard-empty { font-family: "Caveat", cursive; color: #8a7a5e; font-size: 1.1rem; }
.faq-link {
display: block;
margin: 0.35rem 0 0 auto;
background: #2b2218;
color: #e8dfc6;
border: none;
border-radius: 3px;
font-family: "Oswald", sans-serif;
font-size: 0.62rem;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 0.25rem 0.5rem;
cursor: pointer;
}
.faq-link:hover { background: #43331f; }
.peek-note {
margin-top: 0.4rem;
background: #2b2218;
+41 -1
View File
@@ -11,6 +11,7 @@
glowing = false,
charges = null,
onclick,
onfaq,
}: {
card: CardInstance;
selected?: boolean;
@@ -20,6 +21,8 @@
/** Playable RIGHT NOW out of turn — a golden pulse invites the tap. */
glowing?: boolean;
charges?: number | null;
/** When set, cards with official FAQ rulings wear a tappable FAQ seal. */
onfaq?: (cardId: string) => void;
onclick?: () => void;
} = $props();
@@ -75,6 +78,13 @@
{#if displayed}
<span class="badge">DISPLAYED{charges != null ? ` · ${charges}` : ""}</span>
{/if}
{#if onfaq && def.faqRulings.length > 0}
<span
class="faq-seal" role="button" tabindex="-1"
onclick={(ev) => { ev.stopPropagation(); onfaq?.(card.cardId); }}
onkeydown={() => {}}
>FAQ</span>
{/if}
</button>
<style>
@@ -202,6 +212,21 @@
white-space: nowrap;
}
.faq-seal {
position: absolute;
bottom: 0.15rem;
right: 0.2rem;
background: #2b2218;
color: #e8dfc6;
font-family: "Oswald", sans-serif;
font-size: 0.42rem;
letter-spacing: 0.12em;
padding: 0.1rem 0.25rem;
border-radius: 2px;
cursor: pointer;
opacity: 0.85;
}
.faq-seal:hover { opacity: 1; }
.card.glowing {
box-shadow: 0 0 0 2px #c9a72a, 0 0 14px 3px rgba(201, 167, 42, 0.55);
animation: card-glow 1.4s ease-in-out infinite;
@@ -211,7 +236,22 @@
50% { box-shadow: 0 0 0 2px #c9a72a, 0 0 6px 1px rgba(201, 167, 42, 0.3); }
}
@media (prefers-reduced-motion: reduce) {
.card.glowing { animation: none; }
.faq-seal {
position: absolute;
bottom: 0.15rem;
right: 0.2rem;
background: #2b2218;
color: #e8dfc6;
font-family: "Oswald", sans-serif;
font-size: 0.42rem;
letter-spacing: 0.12em;
padding: 0.1rem 0.25rem;
border-radius: 2px;
cursor: pointer;
opacity: 0.85;
}
.faq-seal:hover { opacity: 1; }
.card.glowing { animation: none; }
.card { transition: none; }
.card:hover, .card.selected, .card.attached { transform: none; }
}
+27 -13
View File
@@ -19,6 +19,7 @@
// svelte-ignore state_referenced_locally -- the initial tab is intentionally a one-time value
let tab = $state<"play" | "rules" | "cards" | "about" | "tally">(initialTab);
let faqCardId = $state<string | null>(null);
let libPeek = $state<string | null>(null);
function openTally() {
tab = "tally";
@@ -50,6 +51,14 @@
<svelte:window {onkeydown} />
{#if libPeek}
<div class="lib-peek-scrim" role="button" tabindex="-1" onclick={() => (libPeek = null)} onkeydown={() => {}}>
<div class="lib-peek">
<Card card={{ instanceId: `peek-${libPeek}`, cardId: libPeek }} onfaq={(id) => (faqCardId = id)} />
</div>
</div>
{/if}
{#if faqCardId}
<Faq cardId={faqCardId} onclose={() => (faqCardId = null)} />
{/if}
@@ -174,13 +183,14 @@
<div class="card-grid">
{#each filtered as def (def.id)}
<div class="card-slot">
<Card card={{ instanceId: `lib-${def.id}`, cardId: def.id }} />
<Card
card={{ instanceId: `lib-${def.id}`, cardId: def.id }}
onclick={() => (libPeek = def.id)}
onfaq={(id) => (faqCardId = id)}
/>
<span class="card-meta">
×{def.quantity}{def.alsoIn?.length ? ` (+${def.alsoIn[0]!.quantity} exp)` : ""}
· {def.set === "basic" ? "base" : "exp 1"}
{#if def.faqRulings.length > 0}
· <button class="faq-chip" onclick={() => (faqCardId = def.id)}>FAQ</button>
{/if}
</span>
</div>
{/each}
@@ -305,16 +315,20 @@
.card-slot { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; }
.card-slot :global(.card) { cursor: default; }
.card-slot :global(.card:hover) { transform: none; box-shadow: 0 3px 8px rgba(10, 8, 4, 0.45); }
.faq-chip {
background: none;
border: none;
padding: 0;
font: inherit;
color: #8a4a1f;
text-decoration: underline;
text-decoration-style: dotted;
cursor: pointer;
.lib-peek-scrim {
position: fixed;
inset: 0;
background: rgba(10, 12, 16, 0.55);
display: grid;
place-items: center;
z-index: 55;
}
.lib-peek :global(.card) {
transform: scale(2.1);
cursor: default;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.7);
}
.lib-peek :global(.card:hover) { transform: scale(2.1); }
.card-meta {
font-family: "Courier Prime", monospace;
font-size: 0.68rem;