Re-theme the app: token-based CSS and a full face lift

CSS refactor: every color, font, radius, and shadow is now defined once
in theme.css; all other styles derive translucent variants with
color-mix(), so re-theming means editing one file. Removed the
duplicate core.css/style.css import chains, dead styles, and the
Bootstrap/Tailwind orphan classes that were silently unstyled
(the main reason the splash page looked broken).

New "Lantern & Brine" look: brass and parchment on a lantern-lit
ink-navy night, Pirata One wordmark with Alegreya SC/Sans body fonts
(self-hosted via fontsource — they were previously referenced but
never loaded), parchment-faced playing cards, and a rebuilt splash
page. Inline color styles in components were replaced with semantic
classes (prompt-box, notice-banner, status chips, etc.), the rulebook's
embedded palette was updated to match, and the leftover Vite favicon
and "frontend" page title were replaced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 06:50:00 -07:00
parent ba6bf35579
commit 0744893e7b
36 changed files with 1442 additions and 1185 deletions

View File

@@ -171,15 +171,15 @@
<div class="view-header text-center">
<h2>Character Sheet Creation</h2>
<p>Fill in your Rat Records, delegate questions to your crew, and prepare your Math Magic Techniques.</p>
<div class="flex justify-center items-center gap-2 text-sm text-gray-500 mt-2">
<input type="checkbox" id="devModeToggle" bind:checked={devMode} class="accent-gold"/>
<div class="dev-toggle">
<input type="checkbox" id="devModeToggle" bind:checked={devMode}/>
<label for="devModeToggle">Dev Mode</label>
</div>
</div>
{#if devMode}
<div class="bg-dark-900 border border-gold p-4 rounded-lg flex justify-between items-center mb-6 max-w-4xl mx-auto">
<span class="text-gold font-bold">🛠 Dev Mode Tools</span>
<div class="dev-banner">
<span class="text-accent font-bold">🛠 Dev Mode Tools</span>
<button
class="btn btn-primary"
on:click={autoFillAll}
@@ -262,7 +262,7 @@
<div class="author-label"> {getPlayerName(state.player.other_hate_from_player_id)}</div>
</div>
{:else}
<p class="mb-4 text-sm text-gray-400">Assigning crewmates to write your relationships...</p>
<p class="mb-4 text-sm text-muted">Assigning crewmates to write your relationships...</p>
{/if}
</div>
</div>
@@ -295,7 +295,7 @@
{/if}
{/each}
{#if state.players.filter(p => (p.other_like_from_player_id === state.player.id && !p.other_like) || (p.other_hate_from_player_id === state.player.id && !p.other_hate)).length === 0}
<p class="inbox-empty-message text-gray-400 italic">No pending tasks! Wait for other players to assign you tasks.</p>
<p class="inbox-empty-message text-muted italic">No pending tasks! Wait for other players to assign you tasks.</p>
{/if}
</div>
</div>
@@ -346,7 +346,7 @@
{:else if !techniquesComplete}
{#if swappedTechniques.length === 3}
<div class="max-w-4xl mx-auto space-y-6">
<p class="text-gray-300">Drag and drop your assigned techniques onto your Face Cards:</p>
<p>Drag and drop your assigned techniques onto your Face Cards:</p>
{#if faceError}
<div class="alert alert-danger">{faceError}</div>
@@ -488,7 +488,7 @@
</div>
</div>
{:else}
<div class="text-center italic text-gray-400">
<div class="text-center italic text-muted">
Waiting for all players to submit techniques so they can be shuffled...
</div>
{/if}
@@ -512,15 +512,15 @@
<div class="crew-status-list">
<ul class="space-y-2 mt-4">
{#each state.players as p}
<li class="p-3 bg-dark-800 rounded flex justify-between items-center border-l-4 {p.tech_jack ? 'border-green-500' : 'border-gray-500'}">
<li class="status-row {p.tech_jack ? 'is-ready' : ''}">
<span>{displayName(p)}</span>
<span class="text-sm">
{#if p.tech_jack}
<span class="text-green-400">Ready</span>
<span class="text-success">Ready</span>
{:else if p.created_techniques && JSON.parse(p.created_techniques).length === 3}
<span class="text-yellow-400">Assigning Face Cards</span>
<span class="text-warning">Assigning Face Cards</span>
{:else}
<span class="text-gray-400">Writing Sheet</span>
<span class="text-muted">Writing Sheet</span>
{/if}
</span>
</li>

View File

@@ -48,29 +48,29 @@
.menu-toggle {
padding: 6px 14px;
border-radius: 16px;
background: rgba(10, 15, 25, 0.9);
border: 1px solid var(--glass-border-focus);
color: var(--neon-cyan);
background: color-mix(in srgb, var(--bg-deep) 92%, transparent);
border: 1px solid var(--edge-accent);
color: var(--accent);
font-size: 0.85rem;
font-weight: 700;
font-family: var(--font-heading);
cursor: pointer;
}
.menu-toggle:hover, .menu-toggle.open {
background: rgba(0, 242, 254, 0.1);
background: color-mix(in srgb, var(--accent) 12%, var(--bg-deep));
}
.menu-dropdown {
margin-top: 6px;
display: flex;
flex-direction: column;
background: rgba(10, 15, 25, 0.95);
border: 1px solid var(--glass-border-focus);
border-radius: 12px;
background: color-mix(in srgb, var(--bg-deep) 96%, transparent);
border: 1px solid var(--edge-accent);
border-radius: var(--radius-md);
overflow: hidden;
}
.menu-dropdown a {
padding: 8px 16px;
color: var(--neon-cyan);
color: var(--accent);
font-size: 0.85rem;
font-weight: 700;
text-decoration: none;
@@ -79,9 +79,9 @@
white-space: nowrap;
}
.menu-dropdown a:hover {
background: rgba(0, 242, 254, 0.1);
background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.menu-dropdown a + a {
border-top: 1px solid rgba(0, 242, 254, 0.15);
border-top: 1px solid var(--edge-soft);
}
</style>

View File

@@ -150,10 +150,10 @@
bottom: 20px;
right: 20px;
width: 380px;
background: rgba(10, 15, 25, 0.95);
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
background: color-mix(in srgb, var(--bg-deep) 95%, transparent);
border: 1px solid var(--edge);
border-radius: var(--radius-md);
box-shadow: var(--shadow-deep);
z-index: 1000;
display: flex;
flex-direction: column;
@@ -164,19 +164,19 @@
width: auto;
}
.toggle-log-btn {
background: var(--dark-bg, #1a1a2e);
color: white;
background: transparent;
color: var(--text);
border: none;
padding: 10px;
text-align: center;
cursor: pointer;
font-weight: bold;
border-radius: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius-md);
border-bottom: 1px solid var(--edge-soft);
font-family: var(--font-heading);
}
.toggle-log-btn:hover {
background: rgba(255, 255, 255, 0.1);
background: color-mix(in srgb, var(--text) 10%, transparent);
}
.log-content {
overflow-y: auto;
@@ -193,21 +193,21 @@
}
.log-top-note {
font-size: 0.75rem;
color: var(--text-muted, #94a3b8);
color: var(--text-muted);
font-style: italic;
}
.load-older-btn {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--text-muted, #94a3b8);
background: color-mix(in srgb, var(--text) 6%, transparent);
border: 1px solid var(--edge);
color: var(--text-muted);
font-size: 0.75rem;
padding: 4px 12px;
border-radius: 12px;
cursor: pointer;
}
.load-older-btn:hover {
background: rgba(255, 255, 255, 0.12);
color: var(--text-primary, #f1f5f9);
background: color-mix(in srgb, var(--text) 12%, transparent);
color: var(--text);
}
.log-entry {
display: grid;
@@ -215,12 +215,12 @@
gap: 8px;
align-items: baseline;
padding: 6px 8px;
background: rgba(255, 255, 255, 0.03);
border-left: 3px solid rgba(255, 255, 255, 0.15);
background: color-mix(in srgb, var(--text) 3%, transparent);
border-left: 3px solid var(--edge);
border-radius: 0 6px 6px 0;
font-size: 0.85rem;
line-height: 1.35;
color: #c3cede;
color: color-mix(in srgb, var(--text) 85%, var(--text-muted));
}
.log-icon {
font-size: 0.9rem;
@@ -230,13 +230,13 @@
}
.log-time {
font-size: 0.7rem;
color: var(--text-muted, #94a3b8);
color: var(--text-muted);
white-space: nowrap;
opacity: 0.8;
}
.log-empty {
padding: 10px;
color: var(--text-muted, #94a3b8);
color: var(--text-muted);
font-style: italic;
}
.jump-to-bottom {
@@ -244,37 +244,37 @@
bottom: 12px;
left: 50%;
transform: translateX(-50%);
background: var(--gold, #d4af37);
color: var(--text-dark, #0f172a);
background: var(--accent);
color: var(--text-inverse);
border: none;
border-radius: 16px;
padding: 5px 14px;
font-size: 0.8rem;
font-weight: 700;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
box-shadow: var(--shadow-soft);
}
.jump-to-bottom:hover {
filter: brightness(1.1);
}
/* Accent colors by event kind */
.log-kind-challenge { border-left-color: var(--red-suit, #ff2a5f); }
.log-kind-card { border-left-color: var(--neon-cyan, #00f2fe); }
.log-kind-challenge { border-left-color: var(--danger); }
.log-kind-card { border-left-color: var(--deep); }
.log-kind-captain,
.log-kind-tax,
.log-kind-rank,
.log-kind-victory { border-left-color: var(--gold, #d4af37); }
.log-kind-victory { border-left-color: var(--accent); }
.log-kind-scene,
.log-kind-phase { border-left-color: var(--neon-emerald, #00ff87); }
.log-kind-phase { border-left-color: var(--pirat); }
.log-kind-joker,
.log-kind-warning { border-left-color: #ff9f43; }
.log-kind-obstacle { border-left-color: #4aa3df; }
.log-kind-warning { border-left-color: var(--warning); }
.log-kind-obstacle { border-left-color: var(--deep); }
.log-kind-join,
.log-kind-vote,
.log-kind-objective { border-left-color: #9b8cff; }
.log-kind-objective { border-left-color: var(--mystic); }
.log-kind-victory {
background: rgba(212, 175, 55, 0.08);
color: var(--gold, #d4af37);
background: color-mix(in srgb, var(--accent) 9%, transparent);
color: var(--accent);
}
</style>

View File

@@ -7,16 +7,16 @@
$: crewDone = state.game.completed_crew_1 && state.game.completed_crew_2 && state.game.completed_crew_3;
</script>
<div class="dashboard-container" style="max-width: 800px; margin: 0 auto; padding: 2rem;">
<div class="card glass-panel" style="text-align: center; padding: 2rem;">
<h1 style="font-family: var(--font-heading); color: var(--gold);">🎉 The Story Has Ended!</h1>
<div class="gameover-container">
<div class="card glass-panel gameover-card">
<h1>🎉 The Story Has Ended!</h1>
<p class="section-desc" style="font-size: 1.1rem;">
The Pi-Rats of {state.game.crew_name || 'the crew'} party til they pass out in a pile.
Tell one last tale of how your Pi-Rat is remembered!
</p>
<div class="sheet-group" style="text-align: left; margin-top: 2rem;">
<h3 style="color: var(--gold);">Crew Objectives</h3>
<div class="sheet-group">
<h3 class="text-accent">Crew Objectives</h3>
<div class="objectives-checklist">
<label class="checkbox-label"><input type="checkbox" checked={state.game.completed_crew_1} disabled> <span>Steal a Ship</span></label>
<label class="checkbox-label"><input type="checkbox" checked={state.game.completed_crew_2} disabled> <span>Choose a Captain</span></label>
@@ -29,16 +29,16 @@
{/if}
</div>
<div class="sheet-group" style="text-align: left; margin-top: 1.5rem;">
<h3 style="color: var(--gold);">The Crew</h3>
<div class="sheet-group">
<h3 class="text-accent">The Crew</h3>
{#each pirats as p}
<div style="background: rgba(0,0,0,0.2); padding: 0.75rem; border-radius: 6px; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<div class="crew-epitaph-row">
<span>
{#if p.is_ghost}👻{:else if p.is_dead}💀{:else}🐀{/if}
<strong>{displayName(p)}</strong> (Rank {p.rank})
{#if p.id === state.game.captain_player_id}<span style="color: var(--gold);"> ⭐ Captain</span>{/if}
{#if p.id === state.game.captain_player_id}<span class="text-accent"> ⭐ Captain</span>{/if}
</span>
<span style="font-size: 0.85rem; color: var(--text-muted);">
<span class="objectives-summary">
{p.completed_personal_1 ? '🔫 Gat' : '— no gat'} ·
{p.completed_personal_2 ? '📛 Named' : '— nameless'} ·
{p.completed_personal_3 ? '⚰️ Story complete' : '⛵ Still sailing'}

View File

@@ -65,7 +65,7 @@
</button>
</div>
<div class="mt-4">
<a href="/#/game/{state.game.id}/admin" class="text-sm text-silver underline hover:text-white">Access Admin Panel directly</a>
<a href="/#/game/{state.game.id}/admin" class="text-sm text-muted underline">Access Admin Panel directly</a>
</div>
</div>
{/if}

View File

@@ -27,36 +27,34 @@
<div class="obstacles-container" id="scene-obstacles-container">
<!-- TOP STATUS BAR: Scene Info and Captain -->
<div class="scene-status-banner glass-panel" style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(7, 11, 18, 0.6); border: 1px solid rgba(212, 175, 55, 0.2); border-radius: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<!-- Captain Badge -->
<div>
{#if captain}
<span class="captain-badge" style="background: rgba(212, 175, 55, 0.15); border: 1px solid var(--gold); color: var(--gold); padding: 0.4rem 1rem; border-radius: 20px; font-size: 1rem; font-weight: 700; font-family: var(--font-heading); display: inline-flex; align-items: center; gap: 0.5rem;">
🏴‍☠️ Captain: {displayName(captain)} (Rank {captain.rank}){isCaptain ? ' — You!' : ''}
<div class="scene-status-banner">
<!-- Captain Badge -->
<div>
{#if captain}
<span class="captain-badge">
🏴‍☠️ Captain: {displayName(captain)} (Rank {captain.rank}){isCaptain ? ' — You!' : ''}
</span>
{:else}
<span class="captain-badge vacant">
🏴‍☠️ Captain: None{state.game.completed_crew_1 ? '' : ' (steal a ship first!)'}
</span>
{/if}
</div>
<!-- Active Scene Roster Mini-list -->
<div class="roster-chips">
<span class="roster-label">Active Roster:</span>
{#each state.players as p}
{#if p.role === 'deep'}
<span class="player-chip deep-chip">
🌊 {displayName(p)} (Deep)
</span>
{:else}
<span class="captain-badge" style="background: rgba(255, 255, 255, 0.05); border: 1px dashed rgba(255, 255, 255, 0.2); color: var(--text-muted); padding: 0.4rem 1rem; border-radius: 20px; font-size: 1rem; display: inline-flex; align-items: center; gap: 0.5rem;">
🏴‍☠️ Captain: None{state.game.completed_crew_1 ? '' : ' (steal a ship first!)'}
{:else if p.role === 'pirat'}
<span class="player-chip pirat-chip {p.is_ghost ? 'is-ghost' : ''} {p.is_dead ? 'is-dead' : ''} {p.id === state.game.captain_player_id ? 'is-captain' : ''}">
{#if p.is_ghost}👻{:else if p.is_dead}💀{:else}🐀{/if} {displayName(p)} (Rank {p.rank}){p.id === state.game.captain_player_id ? '' : ''}
</span>
{/if}
</div>
<!-- Active Scene Roster Mini-list -->
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
<span style="font-size: 0.85rem; color: var(--text-muted); font-family: var(--font-heading); margin-right: 0.25rem;">Active Roster:</span>
{#each state.players as p}
{#if p.role === 'deep'}
<span class="player-chip deep-chip" style="font-size: 0.8rem; padding: 0.25rem 0.6rem; border-radius: 12px; margin: 0; display: inline-flex; align-items: center; gap: 0.25rem;">
🌊 {displayName(p)} (Deep)
</span>
{:else if p.role === 'pirat'}
<span class="player-chip pirat-chip" style="font-size: 0.8rem; padding: 0.25rem 0.6rem; border-radius: 12px; margin: 0; display: inline-flex; align-items: center; gap: 0.25rem; {p.is_ghost ? 'border-color: #7890a8; color: #a0b0c0; background: rgba(120, 150, 180, 0.1);' : ''} {p.is_dead ? 'border-color: var(--red-suit); color: var(--red-suit); background: rgba(255, 42, 95, 0.1);' : ''} {p.id === state.game.captain_player_id ? 'border-color: var(--gold); color: var(--gold); background: rgba(212, 175, 55, 0.1);' : ''}">
{#if p.is_ghost}👻{:else if p.is_dead}💀{:else}🐀{/if} {displayName(p)} (Rank {p.rank}){p.id === state.game.captain_player_id ? ' ⭐' : ''}
</span>
{/if}
{/each}
</div>
{/each}
</div>
</div>

View File

@@ -52,12 +52,12 @@
<p class="description">Select your role for the upcoming scene. There must be at least one Pi-Rat and one Deep player. If you played the Deep in the last scene, you must play a Pi-Rat this scene!</p>
{#if state.game.current_scene_number === 1}
<div class="glass-panel" style="max-width: 700px; margin: 0 auto 1.5rem; padding: 1rem 1.5rem; border: 1px dashed var(--gold); border-radius: 8px; background: rgba(212, 175, 55, 0.08); text-align: left;">
<h4 style="color: var(--gold); margin: 0 0 0.5rem 0; font-family: var(--font-heading);">🚢 First Scene Framing</h4>
<div class="notice-banner accent">
<h4>🚢 First Scene Framing</h4>
<p class="info-text" style="margin: 0; font-size: 0.95rem;">
The first scene must establish that the Pi-Rats are <strong>on a ship they can steal</strong> (or have an immediate opportunity to steal one). <strong>Don't start far from water!</strong>
{#if state.player.role === 'deep'}
<br/><span style="color: var(--gold);">You're playing the Deep — once the scene starts, describe where the crew is and what ship is ripe for the taking before calling any Challenges.</span>
<br/><span class="text-accent">You're playing the Deep — once the scene starts, describe where the crew is and what ship is ripe for the taking before calling any Challenges.</span>
{/if}
</p>
</div>
@@ -85,16 +85,16 @@
<div class="role-restrictions mt-4">
{#if !allowedRoles.includes('pirat')}
{#if state.game.current_scene_number === 1}
<p class="text-red-400 text-sm italic">You start at Rank 3, so you must play The Deep for the first scene!</p>
<p class="text-danger text-sm italic">You start at Rank 3, so you must play The Deep for the first scene!</p>
{:else}
<p class="text-red-400 text-sm italic">You are the only eligible player for The Deep this scene!</p>
<p class="text-danger text-sm italic">You are the only eligible player for The Deep this scene!</p>
{/if}
{/if}
{#if !allowedRoles.includes('deep')}
{#if state.game.current_scene_number === 1}
<p class="text-red-400 text-sm italic">You start at Rank 1, so you must play your Pi-Rat for the first scene!</p>
<p class="text-danger text-sm italic">You start at Rank 1, so you must play your Pi-Rat for the first scene!</p>
{:else}
<p class="text-red-400 text-sm italic">You played The Deep last scene, so you must play a Pi-Rat!</p>
<p class="text-danger text-sm italic">You played The Deep last scene, so you must play a Pi-Rat!</p>
{/if}
{/if}
</div>
@@ -106,7 +106,7 @@
<div class="roster-list" id="scene-roster-list">
{#each state.players as p}
<div class="roster-item">
<span class="player-name">{displayName(p)} <span class="text-sm text-gray-400">(Rank {p.rank})</span></span>
<span class="player-name">{displayName(p)} <span class="text-sm text-muted">(Rank {p.rank})</span></span>
{#if p.role === 'pirat'}
<span class="role-badge pirat">Pi-Rat</span>
{:else if p.role === 'deep'}
@@ -132,13 +132,13 @@
{starting ? 'Starting Scene...' : 'Confirm Roles & Shuffle Deck'}
</button>
{:else if !allRolesAssigned}
<p class="text-gray-400 italic mt-4">Waiting for all players to choose a role...</p>
<p class="text-muted italic mt-4">Waiting for all players to choose a role...</p>
{:else if !deepPlayer}
<p class="text-red-400 italic mt-4">Someone must play as The Deep!</p>
<p class="text-danger italic mt-4">Someone must play as The Deep!</p>
{:else if !piratPlayer}
<p class="text-red-400 italic mt-4">Someone must play as a Pi-Rat!</p>
<p class="text-danger italic mt-4">Someone must play as a Pi-Rat!</p>
{:else}
<p class="text-gray-400 italic mt-4">Waiting for The Deep or Creator to start the scene...</p>
<p class="text-muted italic mt-4">Waiting for The Deep or Creator to start the scene...</p>
{/if}
</div>
</div>

View File

@@ -184,67 +184,67 @@
{#if state.player.is_dead && !state.player.is_ghost}
<!-- Death Fate Panel -->
<div class="card glass-panel death-fate-card" style="border: 2px solid var(--red-suit); background: rgba(255, 42, 95, 0.05); padding: 2.5rem; margin: 2rem auto; max-width: 650px; border-radius: 12px; box-shadow: 0 0 20px rgba(255,42,95,0.15); text-align: center;">
<h2 style="color: var(--red-suit); font-family: var(--font-heading); margin-bottom: 1rem; font-size: 2rem;">💀 Your Pi-Rat Has Died!</h2>
<p class="description" style="font-size: 1.15rem; color: var(--text-primary); margin-bottom: 2rem; line-height: 1.6;">
<div class="glass-panel death-fate-card">
<h2>💀 Your Pi-Rat Has Died!</h2>
<p class="description">
Your story arc is complete! You went out in a blaze of glory (or retired honorably). You must now choose how your journey continues:
</p>
{#if !isRolling}
<div style="display: flex; gap: 1.5rem; justify-content: center; flex-wrap: wrap;">
<button class="btn btn-secondary btn-large glow-effect" on:click={becomeGhost} style="border: 1px solid var(--text-muted); font-size: 1.1rem; padding: 0.8rem 2rem;">
<div class="fate-choices">
<button class="btn btn-secondary btn-large glow-effect" on:click={becomeGhost}>
👻 Remain as a Ghost
</button>
<button class="btn btn-primary btn-large glow-effect" on:click={() => isRolling = true} style="background: var(--gold); border: 1px solid var(--gold); color: black; font-size: 1.1rem; padding: 0.8rem 2rem;">
<button class="btn btn-primary btn-large glow-effect" on:click={() => isRolling = true}>
🐀 Roll a New Character
</button>
</div>
<p class="info-text" style="margin-top: 1.5rem; font-style: italic; color: var(--text-muted);">
<p class="info-text italic" style="margin-top: 1.5rem;">
Ghosts look back into the living world, watch over friends, and help their crew. New recruits start fresh at Rank 1 with surprise Secret Techniques and a name based on their smell.
</p>
{:else}
<div class="creation-form-wrapper" style="text-align: left; background: rgba(0,0,0,0.2); padding: 1.5rem; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05);">
<h3 style="color: var(--gold); font-family: var(--font-heading); margin-bottom: 1.5rem; border-bottom: 1px solid rgba(212,175,55,0.2); padding-bottom: 0.5rem;">Create Your New Recruit</h3>
<div class="creation-form-wrapper">
<h3>Create Your New Recruit</h3>
{#if rollingError}
<div class="alert alert-danger" style="margin-bottom: 1.5rem;">{rollingError}</div>
{/if}
<div class="form-group" style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-primary); font-family: var(--font-heading);">What does your Pi-Rat look like?</label>
<div class="input-row" style="display: flex; gap: 0.5rem;">
<input type="text" placeholder="e.g. Scruffy snout, wearing a tattered vest" bind:value={reRollDetails.avatar_look} required class="input-field" style="flex: 1; padding: 0.6rem; border-radius: 4px; border: 1px solid var(--glass-border); background: rgba(0,0,0,0.3); color: white;">
<button type="button" class="btn btn-secondary btn-small" on:click={() => reRollDetails.avatar_look = getSuggestion('look')} style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Suggest</button>
<div class="form-group">
<label>What does your Pi-Rat look like?</label>
<div class="input-row">
<input type="text" placeholder="e.g. Scruffy snout, wearing a tattered vest" bind:value={reRollDetails.avatar_look} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => reRollDetails.avatar_look = getSuggestion('look')}>Suggest</button>
</div>
</div>
<div class="form-group" style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-primary); font-family: var(--font-heading);">What does your Pi-Rat smell like? (Determines name)</label>
<div class="input-row" style="display: flex; gap: 0.5rem;">
<input type="text" placeholder="e.g. Spiced rum, salty cheese, gunpowder" bind:value={reRollDetails.avatar_smell} required class="input-field" style="flex: 1; padding: 0.6rem; border-radius: 4px; border: 1px solid var(--glass-border); background: rgba(0,0,0,0.3); color: white;">
<button type="button" class="btn btn-secondary btn-small" on:click={() => reRollDetails.avatar_smell = getSuggestion('smell')} style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Suggest</button>
<div class="form-group">
<label>What does your Pi-Rat smell like? (Determines name)</label>
<div class="input-row">
<input type="text" placeholder="e.g. Spiced rum, salty cheese, gunpowder" bind:value={reRollDetails.avatar_smell} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => reRollDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>
</div>
</div>
<div class="form-group" style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-primary); font-family: var(--font-heading);">What were your first words after transforming?</label>
<div class="input-row" style="display: flex; gap: 0.5rem;">
<input type="text" placeholder="e.g. Shiver my decimals!" bind:value={reRollDetails.first_words} required class="input-field" style="flex: 1; padding: 0.6rem; border-radius: 4px; border: 1px solid var(--glass-border); background: rgba(0,0,0,0.3); color: white;">
<button type="button" class="btn btn-secondary btn-small" on:click={() => reRollDetails.first_words = getSuggestion('first_words')} style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Suggest</button>
<div class="form-group">
<label>What were your first words after transforming?</label>
<div class="input-row">
<input type="text" placeholder="e.g. Shiver my decimals!" bind:value={reRollDetails.first_words} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => reRollDetails.first_words = getSuggestion('first_words')}>Suggest</button>
</div>
</div>
<div class="form-group" style="margin-bottom: 1.5rem;">
<label style="display: block; font-weight: bold; margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-primary); font-family: var(--font-heading);">Is your Pi-Rat good at Math?</label>
<div class="input-row" style="display: flex; gap: 0.5rem;">
<input type="text" placeholder="e.g. Thinks Pi is a dessert" bind:value={reRollDetails.good_at_math} required class="input-field" style="flex: 1; padding: 0.6rem; border-radius: 4px; border: 1px solid var(--glass-border); background: rgba(0,0,0,0.3); color: white;">
<button type="button" class="btn btn-secondary btn-small" on:click={() => reRollDetails.good_at_math = getSuggestion('good_at_math')} style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Suggest</button>
<div class="form-group">
<label>Is your Pi-Rat good at Math?</label>
<div class="input-row">
<input type="text" placeholder="e.g. Thinks Pi is a dessert" bind:value={reRollDetails.good_at_math} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => reRollDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>
</div>
</div>
<div style="display: flex; gap: 1rem; margin-top: 1.5rem;">
<button class="btn btn-secondary" on:click={() => isRolling = false} style="flex: 1; padding: 0.75rem;">Cancel</button>
<button class="btn btn-primary" on:click={submitReRoll} style="flex: 2; padding: 0.75rem; background: var(--gold); border: 1px solid var(--gold); color: black;">Submit New Character</button>
<div class="form-actions">
<button class="btn btn-secondary" on:click={() => isRolling = false}>Cancel</button>
<button class="btn btn-primary" on:click={submitReRoll}>Submit New Character</button>
</div>
</div>
{/if}
@@ -350,8 +350,8 @@
</div>
{#if isOverLimit}
<div style="border: 1px solid var(--red-suit); border-radius: 4px; padding: 1rem; margin: 1rem 0; background: rgba(255, 42, 95, 0.1);">
<p style="color: var(--red-suit); font-weight: bold; margin: 0;">⚠️ Discard Required: You have selected {keepCards.length} cards to keep, which exceeds your maximum hand limit of {maxHandSize}. Please discard at least {keepCards.length - maxHandSize} card(s).</p>
<div class="notice-banner danger" style="margin: 1rem 0; max-width: none;">
<p class="text-danger font-bold" style="margin: 0;">⚠️ Discard Required: You have selected {keepCards.length} cards to keep, which exceeds your maximum hand limit of {maxHandSize}. Please discard at least {keepCards.length - maxHandSize} card(s).</p>
</div>
{/if}
@@ -374,7 +374,7 @@
{#each state.players as p}
<li>
<strong>{displayName(p)}:</strong> Rank {p.rank}
{#if p.role === 'deep'}<span class="deep-badge">Deep</span>{:else if p.is_ghost}<span class="ghost-badge" style="background: rgba(120, 150, 180, 0.15); border: 1px solid #7890a8; color: #a0b0c0; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 700; margin-left: 0.5rem; display: inline-block;">Ghost</span>{:else}<span class="pirat-badge">Pi-Rat</span>{/if}
{#if p.role === 'deep'}<span class="deep-badge">Deep</span>{:else if p.is_ghost}<span class="ghost-badge">Ghost</span>{:else}<span class="pirat-badge">Pi-Rat</span>{/if}
</li>
{/each}
</ul>
@@ -414,7 +414,7 @@
{/if}
{#if state.player.is_creator && state.game.phase === 'between_scenes'}
<div style="margin-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1rem;">
<div class="end-story-box">
<p class="info-text" style="font-size: 0.85rem;">All good stories end. When the crew agrees the legend is complete:</p>
<button on:click={finishGame} class="btn btn-danger">🏴‍☠️ End the Story</button>
</div>

View File

@@ -62,9 +62,9 @@
{#each openChallenges as ch}
{@const chPlays = JSON.parse(ch.plays || '[]')}
{@const chObstacleIds = JSON.parse(ch.obstacle_ids || '[]')}
<div class="glass-panel" style="margin-bottom: 1rem; padding: 1rem; border: 1px solid var(--red-suit, #ff2a5f); border-radius: 8px; background: rgba(255, 42, 95, 0.06);">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<h4 style="margin: 0; font-family: var(--font-heading);">
<div class="challenge-item">
<div class="challenge-head">
<h4>
{#if ch.challenge_type === 'pvp'}
⚔️ Duel: {playerName(ch.challenger_player_id)} vs {playerName(ch.target_player_id)}
{:else}
@@ -72,7 +72,7 @@
{/if}
</h4>
{#if state.player.role === 'deep' && ch.challenge_type === 'deep'}
<div style="display: flex; gap: 0.5rem;">
<div class="challenge-actions">
<button class="btn btn-primary" on:click={() => resolveChallenge(ch.id)} disabled={ch.tax_state === 'requested'}>Resolve</button>
<button class="btn btn-secondary" on:click={() => cancelChallenge(ch.id)}>Withdraw</button>
</div>
@@ -101,10 +101,10 @@
<!-- Tax: pending request aimed at me -->
{#if ch.tax_state === 'requested'}
{#if myTaxRequest && myTaxRequest.id === ch.id}
<div style="margin-top: 0.75rem; padding: 0.75rem; border: 1px dashed var(--gold); border-radius: 6px;">
<p style="margin: 0 0 0.5rem 0;"><strong>{playerName(ch.target_player_id)}</strong> calls a <strong>{ch.tax_type === 'gat' ? 'Gat' : 'Name'} Tax</strong> on you: take this Challenge for them!</p>
<div class="tax-callout">
<p><strong>{playerName(ch.target_player_id)}</strong> calls a <strong>{ch.tax_type === 'gat' ? 'Gat' : 'Name'} Tax</strong> on you: take this Challenge for them!</p>
<p class="info-text" style="font-size: 0.85rem;">Accept: you get one random card from their hand and attempt the Challenge. Refuse: you hand over your {ch.tax_type === 'gat' ? 'Gat' : 'Name'} — they keep it if they succeed!</p>
<div style="display: flex; gap: 0.5rem;">
<div class="challenge-actions">
<button class="btn btn-primary" on:click={() => respondTax(ch.id, true)}>Accept</button>
<button class="btn btn-danger" on:click={() => respondTax(ch.id, false)}>Refuse</button>
</div>
@@ -116,8 +116,8 @@
<!-- Tax: option for the challenged player -->
{#if ch.challenge_type === 'deep' && ch.acting_player_id === state.player.id && !ch.tax_state && !state.player.tax_banned && taxType(state.player) && eligibleTaxTargets().length > 0}
<div style="margin-top: 0.75rem; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<span class="info-text" style="font-size: 0.9rem;">No {taxType(state.player)}? Make it someone else's problem:</span>
<div class="challenge-actions" style="margin-top: 0.75rem;">
<span class="info-text" style="font-size: 0.9rem; margin: 0;">No {taxType(state.player)}? Make it someone else's problem:</span>
<select class="select-field" bind:value={taxTargetId} style="max-width: 200px;">
<option value="">Pick a crewmate...</option>
{#each eligibleTaxTargets() as p}
@@ -132,7 +132,7 @@
{#if myPvpDefense && myPvpDefense.id === ch.id}
<div style="margin-top: 0.75rem;">
<p style="margin: 0 0 0.5rem 0;"><strong>Defend yourself!</strong> Pick a card:</p>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<div class="challenge-actions">
{#each hand.filter(c => !isJoker(c)) as card}
<button class="btn btn-secondary" on:click={() => pvpDefend(ch.id, card)}>{getCardDisplay(card)}</button>
{/each}

View File

@@ -55,32 +55,32 @@
{/if}
{#if state.player.is_dead}
<div class="sheet-group prompt-box" style="background: rgba(255, 42, 95, 0.1); border: 1px dashed var(--red-suit); padding: 1rem; border-radius: 8px; margin-bottom: 1rem; text-align: center;">
<h4 style="color: var(--red-suit); margin-top: 0; font-family: var(--font-heading); font-size: 1.1rem; font-weight: 700;">💀 You have Died / Retired!</h4>
<div class="prompt-box danger text-center">
<h4>💀 You have Died / Retired!</h4>
<p class="info-text" style="margin-bottom: 0; font-size: 0.9rem;">Your story arc is complete. You will choose to become a Ghost or roll a new character during the upkeep phase between scenes.</p>
</div>
{/if}
{#if state.player.is_ghost}
<div class="sheet-group prompt-box" style="background: rgba(120, 150, 180, 0.1); border: 1px dashed #7890a8; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; text-align: center;">
<h4 style="color: #a0b0c0; margin-top: 0; font-family: var(--font-heading); font-size: 1.1rem; font-weight: 700;">👻 Playing as a Ghost</h4>
<div class="prompt-box ghostly text-center">
<h4>👻 Playing as a Ghost</h4>
<p class="info-text" style="margin-bottom: 0; font-size: 0.9rem;">You are in the Ghost World (grayscale view). You cannot interact well with the living but you can still help them resolve challenges!</p>
</div>
{/if}
{#if state.player.needs_name}
<div class="sheet-group prompt-box" style="background: rgba(212, 175, 55, 0.1); border: 1px dashed var(--gold); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
<h4 style="color: var(--gold); margin-top: 0;">🏴‍☠️ You Earned a Name!</h4>
<div class="prompt-box accent">
<h4>🏴‍☠️ You Earned a Name!</h4>
<p class="info-text" style="margin-bottom: 0.5rem;">You completed your second objective and ranked up! What is your new Pirate Name?</p>
<div style="display: flex; gap: 0.5rem;">
<input type="text" bind:value={newNameInput} class="form-control" placeholder="Enter new name...">
<div class="input-row">
<input type="text" bind:value={newNameInput} class="input-field" placeholder="Enter new name...">
<button class="btn btn-gold" on:click={submitNewName} disabled={!newNameInput.trim()}>Claim Name</button>
</div>
</div>
{/if}
{#if state.player.tax_banned}
<div class="sheet-group prompt-box" style="background: rgba(255, 42, 95, 0.08); border: 1px dashed var(--red-suit); padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; text-align: center;">
<div class="prompt-box danger text-center">
<p class="info-text" style="margin: 0; font-size: 0.9rem;">🚫 You failed a refused Tax — no more Gat/Name Taxes for you this scene.</p>
</div>
{/if}
@@ -104,7 +104,7 @@
<!-- Duel another Pi-Rat -->
{#if !state.player.is_dead && duelTargets.length > 0}
<div class="sheet-group margin-top">
<h4 style="color: var(--gold);">⚔️ Duel a Pi-Rat</h4>
<h4>⚔️ Duel a Pi-Rat</h4>
<p class="info-text" style="font-size: 0.85rem;">Challenge a crewmate (e.g. for the Captaincy)! Your card becomes a temporary Obstacle they must beat. Both cards are discarded afterwards.</p>
<select class="select-field" bind:value={pvpOpponentId} style="width: 100%; margin-bottom: 0.5rem;">
<option value="">Challenge whom?</option>
@@ -123,7 +123,7 @@
{/if}
<div class="sheet-group margin-top">
<h4 style="color: var(--gold);">Crew Objectives</h4>
<h4>Crew Objectives</h4>
<div class="objectives-checklist">
<label class="checkbox-label">
<input type="checkbox" checked={state.game.completed_crew_1} disabled>
@@ -141,7 +141,7 @@
</div>
<div class="sheet-group margin-top">
<h4 style="color: var(--gold);">Personal Objectives</h4>
<h4>Personal Objectives</h4>
<div class="objectives-checklist">
<label class="checkbox-label">
<input type="checkbox" checked={state.player.completed_personal_1} disabled>

View File

@@ -62,7 +62,7 @@
}
</script>
<div class="card glass-panel deep-panel-card" style="max-height: 80vh; overflow-y: auto;">
<div class="card glass-panel deep-panel-card">
<h3 class="deep-text text-center">🌊 Deep Control Panel</h3>
{#if error}
@@ -71,7 +71,7 @@
<!-- Call a Challenge -->
<div class="sheet-group margin-top">
<h4 style="color: var(--gold);">⚔️ Call a Challenge</h4>
<h4>⚔️ Call a Challenge</h4>
<p class="info-text" style="font-size: 0.85rem;">Establish the stakes, pick a Pi-Rat, and apply one or more Obstacles. They play one card per Obstacle — beating one passes; each failure breeds a complication.</p>
<select class="select-field" bind:value={challengeTargetId} style="width: 100%; margin-bottom: 0.5rem;">
<option value="">Challenge which Pi-Rat?</option>
@@ -88,7 +88,7 @@
</label>
{/each}
</div>
<input type="text" class="form-control" placeholder="Stakes (optional): what happens on success/failure?" bind:value={challengeStakes} style="width: 100%; margin-bottom: 0.5rem;">
<input type="text" class="input-field" placeholder="Stakes (optional): what happens on success/failure?" bind:value={challengeStakes} style="width: 100%; margin-bottom: 0.5rem;">
<button class="btn btn-primary btn-full" on:click={createChallenge}
disabled={!challengeTargetId || !Object.values(selectedObstacles).some(v => v)}>
Call Challenge
@@ -97,7 +97,7 @@
<!-- Captain Assignment -->
<div class="sheet-group margin-top">
<h4 style="color: var(--gold);">🏴‍☠️ Captaincy</h4>
<h4>🏴‍☠️ Captaincy</h4>
<p class="info-text" style="font-size: 0.85rem;">The Captain holds power until they step down, lose a duel, die, or the ship is lost. Reassign after a leadership duel or resignation.</p>
<div style="display: flex; gap: 0.5rem;">
<select class="select-field" bind:value={captainSelectId} style="flex: 1;">
@@ -111,7 +111,7 @@
</div>
<div class="sheet-group margin-top">
<h4 style="color: var(--gold);">Crew Objectives</h4>
<h4>Crew Objectives</h4>
<div class="objectives-checklist">
<label class="checkbox-label">
<input type="checkbox" checked={state.game.completed_crew_1} on:change={() => toggleObjective(state.player.id, 'crew_1')}>
@@ -129,10 +129,10 @@
</div>
<div class="sheet-group margin-top">
<h4 style="color: var(--gold);">Pi-Rat Personal Objectives</h4>
<h4>Pi-Rat Personal Objectives</h4>
<p class="info-text">You control completion. Only mark in sequence (1 -> 2 -> 3).</p>
{#each scenePirats as p}
<div style="background: rgba(0,0,0,0.2); padding: 0.5rem; border-radius: 4px; margin-bottom: 0.5rem;">
<div class="objective-player-block">
<strong>{displayName(p)}</strong>
<div class="objectives-checklist" style="margin-top: 0.25rem;">
<label class="checkbox-label"><input type="checkbox" checked={p.completed_personal_1} on:change={() => toggleObjective(p.id, 'personal_1')}> <span>1. Gat</span></label>
@@ -144,7 +144,7 @@
</div>
<!-- End Scene Button -->
<div class="scene-upkeep-controls text-center" style="margin-top: 1rem; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1rem;">
<div class="scene-upkeep-controls text-center">
<p class="info-text">End the scene once every Pi-Rat has faced a Challenge and had a chance at an Objective, or the Obstacle List is empty.</p>
<button on:click={endScene} class="btn btn-danger btn-large btn-full glow-effect">
End Scene & Proceed to Ranking

View File

@@ -62,8 +62,7 @@
{@const success_count = column_cards.filter(c => c.success).length}
{@const is_completed = success_count >= state.players.length}
{@const in_challenge = challengedObstacleIds.has(obs.id)}
<div class="obstacle-item suit-{obs.suit.toLowerCase()} {is_completed ? 'completed' : ''}"
style={in_challenge ? 'box-shadow: 0 0 0 2px var(--red-suit, #ff2a5f);' : ''}
<div class="obstacle-item suit-{obs.suit.toLowerCase()} {is_completed ? 'completed' : ''} {in_challenge ? 'in-challenge' : ''}"
data-obstacle-id={obs.id}
on:dragover={(e) => { if (!is_completed) e.preventDefault(); }}
on:dragenter={(e) => { if (!is_completed) e.currentTarget.classList.add("drag-over"); }}
@@ -78,11 +77,11 @@
else playCard(obs.id, cardCode);
}
}}>
<div class="obstacle-card-wrapper" style="display: flex; justify-content: center; align-items: center;">
<div class="obstacle-card-wrapper">
<Card card={active_card_code} size="medium" title="Active Card: {getCardDisplay(active_card_code)}" />
</div>
<div class="obstacle-details">
<h4>{obs.title} {#if in_challenge}<span style="color: var(--red-suit, #ff2a5f); font-size: 0.8rem;">⚔️ In Challenge</span>{/if}</h4>
<h4>{obs.title} {#if in_challenge}<span class="in-challenge-tag">⚔️ In Challenge</span>{/if}</h4>
<p class="desc">{obs.description}</p>
</div>
@@ -102,7 +101,7 @@
<!-- Successes Tracker -->
<div class="obstacle-successes-display text-center">
<span class="val-label">Successes</span>
<span class="val-number" style="font-size: 1.5rem;">
<span class="val-number">
{success_count} / {state.players.length}
</span>
</div>
@@ -121,8 +120,8 @@
</div>
{#if is_completed && state.player.role === 'deep'}
<div class="text-center" style="margin-top: 1rem;">
<button class="btn btn-success" on:click={() => clearObstacle(obs.id)} style="width: 100%; border: 1px solid rgba(255,255,255,0.2);">Clear Obstacle</button>
<div class="clear-obstacle-row text-center">
<button class="btn btn-success btn-full" on:click={() => clearObstacle(obs.id)}>Clear Obstacle</button>
</div>
{/if}
</div>