The casting panel: the rail becomes the spell while you build it

While a card is selected (or an attack demands an answer) on a wide
screen, the chronicle and chat yield their column to a casting panel:
the card at reading size, attached numbers and modifiers as cards with
their total, and the whole guidance strip — targeting hints, pickers,
discard, cancel — moved in beside them. Counteractions get the same
panel with the incoming card, its power, and every counter played so
far. The under-board strip remains for phones, where a rail below the
fold is no place to aim from. The guidance markup extracts into one
snippet rendered wherever the mode calls it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-30 16:12:12 -04:00
co-authored by Claude Fable 5
parent beaae4a0a4
commit 6189b4935c
+283 -195
View File
@@ -437,6 +437,16 @@
const TWO_CELL_CARDS = new Set(["trader", "dimensional-warp", "redirection"]);
const CREATURE_TARGET_CARDS = new Set(["mega-monster"]);
const MODIFIER_CARDS = new Set(["amplify", "add", "extend", "around-the-corner", "power-attack"]);
/** Casting panel: on wide screens the paper-rail's chronicle yields to
* the spell being built (or answered), controls and all — table talk
* returns the moment the cast resolves. Phones keep the under-board
* strips; a rail below the fold is no place to aim from. */
const wideQuery = typeof matchMedia === "undefined" ? null : matchMedia("(min-width: 901px)");
let wideScreen = $state(wideQuery?.matches ?? true);
wideQuery?.addEventListener("change", (e) => (wideScreen = e.matches));
const castPanelOn = $derived(wideScreen && !local.active &&
(selectedCard != null || (youMustRespond && !!view?.stack)));
/** The hand in the player's own order: drag a card onto another to take
* its place. The order lives only in this browser — the server's hand
* array is authority for contents, this for arrangement. */
@@ -2095,6 +2105,201 @@
</section>
{:else if view}
<div class="game" class:hand-left={prefs.handLeft}>
{#snippet tableGuidance()}
{#if youMustRespond || selectedDef || selectedCreature || youMustDiscard || discardMode || discardSelection.size > 0}
<div class="hint-strip">
{#if selectedCreature && !selectedDef}
{@const sc = view.creatures.find((c) => c.id === selectedCreature)}
{#if sc}
<span>Commanding <strong>{cardDef(sc.kind).name}</strong> — {creatureStats(sc)}.
Tap a square beside it to march, a target in its square to attack.</span>
{#if dmWarnCell}
<span class="hint-alert">⚠ it will claw YOU the moment it enters your square — tap again if you mean it</span>
{/if}
<button class="hint-cancel" onclick={() => (selectedCreature = null)}>done</button>
{/if}
{/if}
{#if youMustRespond && view.stack}
<span class="hint-alert">
{#if view.stack.defenderId === view.you && selectedCard?.cardId === "teleport"}
{pendingTeleport
? "Escaping by teleport — tap the marked square again to jump."
: "Escaping by teleport — tap a square within four spaces to mark it."}
{:else if view.stack.defenderId === view.you}
{view.stack.attackerId} attacks with
{attackingCreature ? `the ${cardDef(attackingCreature.kind).name}`
: view.stack.attackCard?.cardId === "illusionary-attack" && view.stack.params?.cardId
? `an illusionary ${cardDef(view.stack.params.cardId).name}`
: view.stack.attackCard ? cardDef(view.stack.attackCard.cardId).name
: view.stack.tearTreasure ? "a grab at your treasure" : "a punch"} —
tap a counteraction card, or
{:else}
{view.stack.waitingOn === view.you ? "They counter your spell — counter back, or" : ""}
{/if}
</span>
<button class="stamp tiny" onclick={pass}>let it resolve</button>
<button class="stamp tiny" onclick={askCouncil}>🎓 hints</button>
{/if}
{#if youMustDiscard}
<span class="hint-alert">Hand over the limit — mark cards and discard.</span>
{:else if discardMode && discardSelection.size === 0}
<span>Mark the cards to throw away, then confirm.</span>
{/if}
{#if discardSelection.size > 0}
<button class="stamp tiny" onclick={doDiscard}>Discard {discardSelection.size} marked</button>
{/if}
{#if discardMode || (discardSelection.size > 0 && !youMustDiscard)}
<button class="hint-cancel" onclick={cancelDiscardMode}>never mind</button>
{/if}
{#if selectedDef}
<strong class="hint-name">{selectedDef.name}</strong>
{#if pendingCellFor}
<span>— now click where {pendingCellFor} goes{selectedCard?.cardId === "mental-force"
? " (within three walked spaces of them — no sight needed)" : ""}</span>
{/if}
{#if selectedCard && EDGE_CARDS.has(selectedCard.cardId)}
<span>— {EDGE_HINTS[selectedCard.cardId] ?? "click a wall line"}</span>
{/if}
{#if selectedCard && WAND_IDS.has(selectedCard.cardId) && view.wandCharges[selectedCard.instanceId] == null && !attachedNumber}
<span>— first use sets the charges: tap a NUMBER card first, or cast bare for a single charge</span>
{:else if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")}
<span>— click a target, or a wall line to batter it{attachedNumber ? ` (powered by a ${numberTotal})` : ""}</span>
{/if}
{#if attachedMods.length > 0}
<span class="hint-mods">[+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}]</span>
{/if}
{#if attachedMods.some((m) => m.cardId === "power-attack")}
<label class="inline">burn
<select class="tier-pick" bind:value={paPoints} aria-label="life points to burn">
{#each [1, 2, 3, 4, 5] as n (n)}
{#if me && n < me.life}<option value={n}>{n}</option>{/if}
{/each}
</select>
life for +{paPoints} damage</label>
{/if}
{#if selectedCard?.cardId === "boobytrap"}
<span>— place 4 tokens ({trapCells.length}/4; the first is real)</span>
{/if}
{#if selectedCard?.cardId === "redirection"}
<span>{tradeFrom ? "— now the exit it should connect TO" : "— click the first exit"}</span>
{:else if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)}
<span>{tradeFrom ? "— now the second square" : "— click the first square"}</span>
{/if}
{#if selectedCard?.cardId === "teleport" && !youMustRespond}
<span>{pendingTeleport
? "— tap the marked square again to jump, or pick another"
: "— tap a destination (up to four squares, walls ignored)"}</span>
{#if pendingTeleport}
<button class="stamp tiny" onclick={confirmTeleport}>teleport!</button>
{/if}
{/if}
{#if selectedCard?.cardId === "mega-monster"}
<span>— double its</span>
<button class="stamp tiny" class:lit={megaBoost === "life"}
onclick={() => (megaBoost = "life")}>life-points</button>
<button class="stamp tiny" class:lit={megaBoost === "movement"}
onclick={() => (megaBoost = "movement")}>movement</button>
<span>— then tap the monster</span>
{/if}
{#if selectedCard?.cardId === "pick-lock" || selectedCard?.cardId === "master-key"}
<label class="inline"><input type="checkbox" bind:checked={holdDoor} /> hold the door open</label>
{/if}
{#if selectedCard?.cardId === "master-key" && isYourTurn &&
!me?.displayed.some((c) => c.instanceId === selectedCard!.instanceId)}
<button class="stamp tiny" onclick={castSelfWithNumber}>Display the key</button>
{/if}
{#if selectedCard?.cardId === "rotate-sector"}
<label class="inline"><input type="checkbox" bind:checked={rotateCW} /> clockwise</label>
<span>— click the sector</span>
{/if}
{#if selectedCard?.cardId === "relocate-sector"}
<span>{pendingSectorFrom
? (relocateGhosts?.length
? "— now tap a dashed landing beside the maze"
: "— that sector has nowhere legal to go; pick another")
: "— click the sector to move"}</span>
{/if}
{#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)}
<label class="inline">name <input class="text-input" list="card-name-options" bind:value={nameInput} placeholder="e.g. Fireball" /></label>
<datalist id="card-name-options">
{#each nameSuggestions as n (n)}<option value={n}></option>{/each}
</datalist>
{#if selectedCard.cardId === "deja-vu"}
<button class="stamp tiny" onclick={() => {
const id = nameToCardId(nameInput);
if (id && selectedCard) {
dispatch({ type: "cast", instanceId: selectedCard.instanceId, params: { cardId: id } });
clearSelection();
}
}}>Retrieve</button>
{:else}
<span>— then click the target</span>
{/if}
{/if}
{#if selectedCard?.cardId === "swap-meet"}
{#if !swapMeetTarget}
<span>— click the other trader</span>
{:else if myTradables.length === 0}
<span>— you carry no items to trade</span>
{:else if !swapMine}
<span>— trade away your:</span>
{#each myTradables as c (c.key)}
<button class="stamp tiny" onclick={() => (swapMine = c.key)}>{c.label}</button>
{/each}
{:else if theirTradables.length === 0}
<span>— they show no item you could claim</span>
{:else}
<span>— and take their:</span>
{#each theirTradables as c (c.key)}
<button class="stamp tiny" onclick={() => castSwapMeet(c.key)}>{c.label}</button>
{/each}
{/if}
{/if}
{#if selectedCard?.cardId === "waterbolt"}
<label class="inline">damage <input class="num-input" type="number" min="0" max={numberTotal} bind:value={wbDamage} /></label>
<span>(knockback {numberTotal - wbDamage})</span>
{/if}
{#if selectedCard?.cardId === "power-run"}
<label class="inline">life to trade <input class="num-input" type="number" min="1" max="10" bind:value={runPoints} /></label>
<button class="stamp tiny" onclick={castPowerRun}>Run!</button>
{/if}
{#if selectedCard && confirmCastable(selectedCard.cardId)}
<button class="stamp tiny" onclick={castSelfWithNumber}>
Cast{attachedNumber ? ` with the ${numberTotal}` : ""}
</button>
{/if}
{#if selectedCard && isMovableObject(selectedCard.cardId) && isYourTurn}
<button class="stamp tiny" onclick={() => {
if (selectedCard) dispatch({ type: "dropObject", instanceId: selectedCard.instanceId });
clearSelection();
}}>Drop it here</button>
{/if}
{#if selectedCard && isYourTurn && !youMustRespond}
<button class="stamp tiny discard-one" onclick={() => {
if (selectedCard) dispatch({ type: "discard", instanceIds: [selectedCard.instanceId] });
clearSelection();
}}>Discard it</button>
{/if}
{#if selectedCard?.cardId === "add"}
<span>— ADD rides a second NUMBER: play one number for movement,
then play another, and the Add spends itself</span>
{/if}
{#if selectedCard && isNumberCard(selectedCard.cardId)}
{@const needsAdd = view.turn.numberPlayedForMovement}
{@const haveAdd = view.yourHand.some((c) => c.cardId === "add")}
{#if needsAdd && !haveAdd}
<span>— a second movement number needs an ADD card</span>
{:else}
<button class="stamp tiny" onclick={playNumberForMovement}>
Play for +{cardDef(selectedCard.cardId).value} movement{needsAdd ? " (spends your Add)" : ""}
</button>
{/if}
{/if}
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
{/if}
</div>
{/if}
{/snippet}
<section class="board-zone">
<div class="table-stack" class:fpv-primary={prefs.liveFp && !!view.you && !net.spectating}>
{#if prefs.liveFp && view.you && !net.spectating}
@@ -2331,7 +2536,51 @@
</div>
</div>
<div class="chronicle" aria-label="game log" bind:this={chronicleEl}
{#if castPanelOn && view}
<div class="cast-panel" class:respond={youMustRespond} aria-label={youMustRespond ? "counteraction" : "casting"}>
{#if youMustRespond && view.stack}
<div class="cast-panel-head">⚔ incoming</div>
{#if view.stack.attackCard}
<div class="cast-panel-card"><Card card={view.stack.attackCard} onfaq={(id) => (faqCardId = id)} /></div>
{:else}
<div class="cast-panel-note">a physical blow — no card behind it</div>
{/if}
{#if view.stack.numberValue != null || view.stack.amplifyFactor > 1}
<div class="cast-panel-note">
{view.stack.numberValue != null ? `powered by a ${view.stack.numberValue}` : ""}
{view.stack.amplifyFactor > 1 ? ` amplified ×${view.stack.amplifyFactor}` : ""}
</div>
{/if}
{#if view.stack.counters.length > 0}
<div class="cast-panel-sub">counters so far</div>
<div class="cast-panel-minis">
{#each view.stack.counters as c (c.card.instanceId)}
<span class="cast-mini" class:nullified={c.nullified}>
<Card card={c.card} onclick={() => {}} /></span>
{/each}
</div>
{/if}
{:else if selectedCard}
<div class="cast-panel-card"><Card card={selectedCard} onfaq={(id) => (faqCardId = id)} /></div>
{#if attachedNumber || attachedMods.length > 0}
<div class="cast-panel-sub">riding along</div>
<div class="cast-panel-minis">
{#if attachedNumber}
<span class="cast-mini"><Card card={attachedNumber} onclick={() => {}} /></span>
{/if}
{#each attachedMods as m (m.instanceId)}
<span class="cast-mini"><Card card={m} onclick={() => {}} /></span>
{/each}
</div>
{#if attachedNumber}
<div class="cast-panel-note">number total {numberTotal}</div>
{/if}
{/if}
{/if}
{@render tableGuidance()}
</div>
{/if}
<div class="chronicle" class:tucked={castPanelOn} aria-label="game log" bind:this={chronicleEl}
onscroll={onChronicleScroll}>
{#if local.active}
{#each local.log as line, i (i)}
@@ -2355,7 +2604,7 @@
onclick={() => local.rollTableDie()}>🎲 roll the die</button>
</div>
{/if}
{#if !local.active && net.roomId && !net.spectating}
{#if !local.active && net.roomId && !net.spectating && !castPanelOn}
<form class="say-box" onsubmit={(e) => {
e.preventDefault();
const t = chatDraft.trim();
@@ -2391,198 +2640,8 @@
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
</div>
{/if}
{#if youMustRespond || selectedDef || selectedCreature || youMustDiscard || discardMode || discardSelection.size > 0}
<div class="hint-strip">
{#if selectedCreature && !selectedDef}
{@const sc = view.creatures.find((c) => c.id === selectedCreature)}
{#if sc}
<span>Commanding <strong>{cardDef(sc.kind).name}</strong> — {creatureStats(sc)}.
Tap a square beside it to march, a target in its square to attack.</span>
{#if dmWarnCell}
<span class="hint-alert">⚠ it will claw YOU the moment it enters your square — tap again if you mean it</span>
{/if}
<button class="hint-cancel" onclick={() => (selectedCreature = null)}>done</button>
{/if}
{/if}
{#if youMustRespond && view.stack}
<span class="hint-alert">
{#if view.stack.defenderId === view.you && selectedCard?.cardId === "teleport"}
{pendingTeleport
? "Escaping by teleport — tap the marked square again to jump."
: "Escaping by teleport — tap a square within four spaces to mark it."}
{:else if view.stack.defenderId === view.you}
{view.stack.attackerId} attacks with
{attackingCreature ? `the ${cardDef(attackingCreature.kind).name}`
: view.stack.attackCard?.cardId === "illusionary-attack" && view.stack.params?.cardId
? `an illusionary ${cardDef(view.stack.params.cardId).name}`
: view.stack.attackCard ? cardDef(view.stack.attackCard.cardId).name
: view.stack.tearTreasure ? "a grab at your treasure" : "a punch"} —
tap a counteraction card, or
{:else}
{view.stack.waitingOn === view.you ? "They counter your spell — counter back, or" : ""}
{/if}
</span>
<button class="stamp tiny" onclick={pass}>let it resolve</button>
<button class="stamp tiny" onclick={askCouncil}>🎓 hints</button>
{/if}
{#if youMustDiscard}
<span class="hint-alert">Hand over the limit — mark cards and discard.</span>
{:else if discardMode && discardSelection.size === 0}
<span>Mark the cards to throw away, then confirm.</span>
{/if}
{#if discardSelection.size > 0}
<button class="stamp tiny" onclick={doDiscard}>Discard {discardSelection.size} marked</button>
{/if}
{#if discardMode || (discardSelection.size > 0 && !youMustDiscard)}
<button class="hint-cancel" onclick={cancelDiscardMode}>never mind</button>
{/if}
{#if selectedDef}
<strong class="hint-name">{selectedDef.name}</strong>
{#if pendingCellFor}
<span>— now click where {pendingCellFor} goes{selectedCard?.cardId === "mental-force"
? " (within three walked spaces of them — no sight needed)" : ""}</span>
{/if}
{#if selectedCard && EDGE_CARDS.has(selectedCard.cardId)}
<span>— {EDGE_HINTS[selectedCard.cardId] ?? "click a wall line"}</span>
{/if}
{#if selectedCard && WAND_IDS.has(selectedCard.cardId) && view.wandCharges[selectedCard.instanceId] == null && !attachedNumber}
<span>— first use sets the charges: tap a NUMBER card first, or cast bare for a single charge</span>
{:else if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")}
<span>— click a target, or a wall line to batter it{attachedNumber ? ` (powered by a ${numberTotal})` : ""}</span>
{/if}
{#if attachedMods.length > 0}
<span class="hint-mods">[+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}]</span>
{/if}
{#if attachedMods.some((m) => m.cardId === "power-attack")}
<label class="inline">burn
<select class="tier-pick" bind:value={paPoints} aria-label="life points to burn">
{#each [1, 2, 3, 4, 5] as n (n)}
{#if me && n < me.life}<option value={n}>{n}</option>{/if}
{/each}
</select>
life for +{paPoints} damage</label>
{/if}
{#if selectedCard?.cardId === "boobytrap"}
<span> place 4 tokens ({trapCells.length}/4; the first is real)</span>
{/if}
{#if selectedCard?.cardId === "redirection"}
<span>{tradeFrom ? "— now the exit it should connect TO" : "— click the first exit"}</span>
{:else if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)}
<span>{tradeFrom ? "— now the second square" : "— click the first square"}</span>
{/if}
{#if selectedCard?.cardId === "teleport" && !youMustRespond}
<span>{pendingTeleport
? "— tap the marked square again to jump, or pick another"
: "— tap a destination (up to four squares, walls ignored)"}</span>
{#if pendingTeleport}
<button class="stamp tiny" onclick={confirmTeleport}>teleport!</button>
{/if}
{/if}
{#if selectedCard?.cardId === "mega-monster"}
<span> double its</span>
<button class="stamp tiny" class:lit={megaBoost === "life"}
onclick={() => (megaBoost = "life")}>life-points</button>
<button class="stamp tiny" class:lit={megaBoost === "movement"}
onclick={() => (megaBoost = "movement")}>movement</button>
<span> then tap the monster</span>
{/if}
{#if selectedCard?.cardId === "pick-lock" || selectedCard?.cardId === "master-key"}
<label class="inline"><input type="checkbox" bind:checked={holdDoor} /> hold the door open</label>
{/if}
{#if selectedCard?.cardId === "master-key" && isYourTurn &&
!me?.displayed.some((c) => c.instanceId === selectedCard!.instanceId)}
<button class="stamp tiny" onclick={castSelfWithNumber}>Display the key</button>
{/if}
{#if selectedCard?.cardId === "rotate-sector"}
<label class="inline"><input type="checkbox" bind:checked={rotateCW} /> clockwise</label>
<span> click the sector</span>
{/if}
{#if selectedCard?.cardId === "relocate-sector"}
<span>{pendingSectorFrom
? (relocateGhosts?.length
? "— now tap a dashed landing beside the maze"
: "— that sector has nowhere legal to go; pick another")
: "— click the sector to move"}</span>
{/if}
{#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)}
<label class="inline">name <input class="text-input" list="card-name-options" bind:value={nameInput} placeholder="e.g. Fireball" /></label>
<datalist id="card-name-options">
{#each nameSuggestions as n (n)}<option value={n}></option>{/each}
</datalist>
{#if selectedCard.cardId === "deja-vu"}
<button class="stamp tiny" onclick={() => {
const id = nameToCardId(nameInput);
if (id && selectedCard) {
dispatch({ type: "cast", instanceId: selectedCard.instanceId, params: { cardId: id } });
clearSelection();
}
}}>Retrieve</button>
{:else}
<span> then click the target</span>
{/if}
{/if}
{#if selectedCard?.cardId === "swap-meet"}
{#if !swapMeetTarget}
<span> click the other trader</span>
{:else if myTradables.length === 0}
<span> you carry no items to trade</span>
{:else if !swapMine}
<span> trade away your:</span>
{#each myTradables as c (c.key)}
<button class="stamp tiny" onclick={() => (swapMine = c.key)}>{c.label}</button>
{/each}
{:else if theirTradables.length === 0}
<span> they show no item you could claim</span>
{:else}
<span> and take their:</span>
{#each theirTradables as c (c.key)}
<button class="stamp tiny" onclick={() => castSwapMeet(c.key)}>{c.label}</button>
{/each}
{/if}
{/if}
{#if selectedCard?.cardId === "waterbolt"}
<label class="inline">damage <input class="num-input" type="number" min="0" max={numberTotal} bind:value={wbDamage} /></label>
<span>(knockback {numberTotal - wbDamage})</span>
{/if}
{#if selectedCard?.cardId === "power-run"}
<label class="inline">life to trade <input class="num-input" type="number" min="1" max="10" bind:value={runPoints} /></label>
<button class="stamp tiny" onclick={castPowerRun}>Run!</button>
{/if}
{#if selectedCard && confirmCastable(selectedCard.cardId)}
<button class="stamp tiny" onclick={castSelfWithNumber}>
Cast{attachedNumber ? ` with the ${numberTotal}` : ""}
</button>
{/if}
{#if selectedCard && isMovableObject(selectedCard.cardId) && isYourTurn}
<button class="stamp tiny" onclick={() => {
if (selectedCard) dispatch({ type: "dropObject", instanceId: selectedCard.instanceId });
clearSelection();
}}>Drop it here</button>
{/if}
{#if selectedCard && isYourTurn && !youMustRespond}
<button class="stamp tiny discard-one" onclick={() => {
if (selectedCard) dispatch({ type: "discard", instanceIds: [selectedCard.instanceId] });
clearSelection();
}}>Discard it</button>
{/if}
{#if selectedCard?.cardId === "add"}
<span> ADD rides a second NUMBER: play one number for movement,
then play another, and the Add spends itself</span>
{/if}
{#if selectedCard && isNumberCard(selectedCard.cardId)}
{@const needsAdd = view.turn.numberPlayedForMovement}
{@const haveAdd = view.yourHand.some((c) => c.cardId === "add")}
{#if needsAdd && !haveAdd}
<span> a second movement number needs an ADD card</span>
{:else}
<button class="stamp tiny" onclick={playNumberForMovement}>
Play for +{cardDef(selectedCard.cardId).value} movement{needsAdd ? " (spends your Add)" : ""}
</button>
{/if}
{/if}
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
{/if}
</div>
{#if !castPanelOn}
{@render tableGuidance()}
{/if}
<div class="actions">
@@ -2629,7 +2688,7 @@
{/if}
</div>
{#if selectedCard && !inspectorHidden}
{#if selectedCard && !inspectorHidden && !castPanelOn}
<div class="inspector">
<button class="inspector-close" onclick={() => (inspectorHidden = true)} aria-label="hide enlarged card">×</button>
<button class="inspector-card" title="read it full size"
@@ -2783,6 +2842,35 @@
.fb-note { font-size: 0.8rem; color: #6b5a41; font-style: italic; }
.fb-thanks { color: #43331f; padding: 0.6rem 0; }
.respond-card { display: inline-block; vertical-align: middle; margin-left: 0.4rem; zoom: 0.72; }
.chronicle.tucked { display: none; }
/* The casting panel wears the table's dark felt so the guidance strips
* keep their own colors; the cards bring their own paper. */
.cast-panel {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.45rem;
padding: 0.6rem;
border: 1px dashed rgba(233, 225, 203, 0.35);
border-radius: 4px;
}
.cast-panel.respond { border-color: #e8a03c; }
.cast-panel-head {
font-family: "Oswald", sans-serif;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #e8a03c;
text-align: center;
}
.cast-panel-card { zoom: 1.5; align-self: center; }
.cast-panel-sub { font-size: 0.78rem; color: #a49c86; text-align: center; }
.cast-panel-note { font-size: 0.85rem; color: #d8d2c0; font-style: italic; text-align: center; }
.cast-panel-minis { display: flex; flex-wrap: wrap; gap: 0.3rem; justify-content: center; }
.cast-mini { zoom: 0.55; }
.cast-mini.nullified { opacity: 0.45; filter: grayscale(0.8); }
.hand-slot { display: flex; cursor: grab; }
.hand-slot:active { cursor: grabbing; }
.mast-audience { font-size: 0.85rem; color: #a49c86; white-space: nowrap; }