This commit is contained in:
2026-04-01 18:31:33 +02:00
parent 6e23e32bb0
commit b5c7c5305a
95 changed files with 9609 additions and 2374 deletions
+591 -56
View File
@@ -1,11 +1,25 @@
<script>
<script lang="ts">
import { API_URL, WS_URL } from '$lib/api.js';
import { apiFetch } from '$lib/api.js';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
let profile = $state(null);
let profile: any = $state(null);
let loading = $state(true);
let wishlistText = $state('');
let wishlistSaving = $state(false);
let wishlistSaved = $state(false);
let wishlistError = $state('');
let friends: any[] = $state([]);
let proposals: any[] = $state([]);
let challenges: any[] = $state([]);
let confirmingRemove: Set<string> = $state(new Set());
let confirmingWithdraw: Set<string> = $state(new Set());
// Increments every second — passed to formatChallengeExpiry to force re-evaluation
let tick = $state(0);
const token = () => localStorage.getItem('token');
@@ -14,9 +28,81 @@
const res = await apiFetch(`${API_URL}/profile`);
if (res.status === 401) { goto('/auth'); return; }
profile = await res.json();
wishlistText = profile.trade_wishlist || '';
const friendsRes = await apiFetch(`${API_URL}/friends`);
if (friendsRes.ok) friends = await friendsRes.json();
const proposalsRes = await apiFetch(`${API_URL}/trade-proposals`);
if (proposalsRes.ok) proposals = await proposalsRes.json();
const challengesRes = await apiFetch(`${API_URL}/challenges`);
if (challengesRes.ok) challenges = await challengesRes.json();
loading = false;
const tickInterval = setInterval(() => { tick++; }, 1000);
const pollInterval = setInterval(async () => {
const [pRes, cRes] = await Promise.all([
apiFetch(`${API_URL}/trade-proposals`),
apiFetch(`${API_URL}/challenges`),
]);
if (pRes.ok) proposals = await pRes.json();
if (cRes.ok) challenges = await cRes.json();
}, 30_000);
return () => {
clearInterval(tickInterval);
clearInterval(pollInterval);
};
});
function formatExpiry(isoString: string) {
const d = new Date(isoString);
const diff = d.getTime() - Date.now();
if (diff < 0) return 'expired';
const hrs = Math.floor(diff / 3600000);
if (hrs < 1) return 'in < 1h';
if (hrs < 24) return `in ${hrs}h`;
return `in ${Math.floor(hrs / 24)}d`;
}
async function saveWishlist() {
wishlistSaving = true;
wishlistError = '';
wishlistSaved = false;
const res = await apiFetch(`${API_URL}/profile`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ trade_wishlist: wishlistText }),
});
wishlistSaving = false;
if (res.ok) {
wishlistSaved = true;
setTimeout(() => { wishlistSaved = false; }, 2500);
} else {
wishlistError = 'Failed to save.';
}
}
function formatChallengeExpiry(isoString: string, _tick?: number) {
const secs = Math.max(0, Math.floor((new Date(isoString).getTime() - Date.now()) / 1000));
if (secs <= 0) return 'expired';
if (secs < 60) return `${secs}s remaining`;
return `${Math.floor(secs / 60)}m ${secs % 60}s remaining`;
}
async function withdrawChallenge(challengeId: string) {
const res = await apiFetch(`${API_URL}/challenges/${challengeId}/decline`, { method: 'POST' });
if (res.ok) challenges = challenges.filter((c: any) => c.id !== challengeId);
}
async function removeFriend(friendshipId: string) {
await apiFetch(`${API_URL}/friendships/${friendshipId}`, { method: 'DELETE' });
friends = friends.filter((f: any) => f.friendship_id !== friendshipId);
}
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('refresh_token');
@@ -69,7 +155,7 @@
<span class="shards-icon"></span>
<span class="shards-value">{profile.shards}</span>
<span class="shards-label">Shards</span>
<a href="/shards" class="shards-link">shatter cards</a>
<a href="/shatter" class="shards-link">shatter cards</a>
</div>
<div class="section-divider"></div>
@@ -98,6 +184,28 @@
<div class="section-divider"></div>
<div class="wishlist-group">
<h2 class="section-title">Trade Wishlist</h2>
<div class="wishlist-section">
<p class="wishlist-hint">Cards or types you're looking to trade for. Visible on your public profile.</p>
<textarea
class="wishlist-textarea"
bind:value={wishlistText}
placeholder="e.g. Looking for legendary locations, rare scientists..."
rows="3"
></textarea>
<div class="wishlist-actions">
<button class="save-btn" onclick={saveWishlist} disabled={wishlistSaving}>
{wishlistSaving ? 'Saving...' : 'Save'}
</button>
{#if wishlistSaved}<span class="wishlist-ok">Saved ✓</span>{/if}
<p class="wishlist-error" style="min-height: 1.2em">{wishlistError}</p>
</div>
</div>
</div>
<div class="section-divider"></div>
<h2 class="section-title">Highlights</h2>
<div class="highlights">
<div class="highlight-card">
@@ -129,17 +237,161 @@
</div>
</div>
<div class="section-divider"></div>
<h2 class="section-title">Friends</h2>
{#if friends.length === 0}
<p class="no-friends">No friends yet.</p>
{:else}
<ul class="friends-list">
{#each friends as f (f.friendship_id)}
<li class="friend-item">
<a href="/profile/{f.username}" class="friend-name">{f.username}</a>
{#if confirmingRemove.has(f.friendship_id)}
<span class="confirm-label">Remove friend?</span>
<button class="confirm-yes-btn" onclick={() => removeFriend(f.friendship_id)}>Confirm</button>
<button class="confirm-no-btn" onclick={() => { confirmingRemove.delete(f.friendship_id); confirmingRemove = confirmingRemove; }}>Cancel</button>
{:else}
<button class="unfriend-btn" onclick={() => { confirmingRemove.add(f.friendship_id); confirmingRemove = confirmingRemove; }}>Remove</button>
{/if}
</li>
{/each}
</ul>
{/if}
<div class="section-divider"></div>
<h2 class="section-title">Trade Proposals</h2>
{#if proposals.length === 0}
<p class="no-friends">No trade proposals.</p>
{:else}
{@const incoming = proposals.filter((p: any) => p.direction === 'incoming' && p.status === 'pending')}
{@const outgoing = proposals.filter((p: any) => p.direction === 'outgoing' && p.status === 'pending')}
{@const resolved = proposals.filter((p: any) => p.status !== 'pending')}
{#if incoming.length > 0}
<p class="proposal-subhead">Incoming</p>
{#each incoming as p (p.id)}
<div class="proposal-card">
<div class="proposal-meta">
<a href="/profile/{p.proposer_username}" target="_blank" class="friend-name">{p.proposer_username}</a>
<span class="proposal-status pending">Pending</span>
<span class="proposal-expires">{formatExpiry(p.expires_at)}</span>
</div>
<p class="proposal-desc">
{#if p.requested_cards.length > 0}Wants: <strong>{p.requested_cards.map((c: any) => c.name).join(', ')}</strong><br/>{/if}
{#if p.offered_cards.length > 0}Offering: {p.offered_cards.map((c: any) => c.name).join(', ')}{/if}
</p>
<a href="/trade/proposal/{p.id}" class="see-proposal-btn">See Proposal</a>
</div>
{/each}
{/if}
{#if outgoing.length > 0}
<p class="proposal-subhead">Outgoing</p>
{#each outgoing as p (p.id)}
<div class="proposal-card">
<div class="proposal-meta">
<span class="proposal-to">To: <a href="/profile/{p.recipient_username}" target="_blank" class="friend-name">{p.recipient_username}</a></span>
<span class="proposal-status pending">Pending</span>
<span class="proposal-expires">{formatExpiry(p.expires_at)}</span>
</div>
<p class="proposal-desc">
{#if p.requested_cards.length > 0}Requesting: <strong>{p.requested_cards.map((c: any) => c.name).join(', ')}</strong><br/>{/if}
{#if p.offered_cards.length > 0}Offering: {p.offered_cards.map((c: any) => c.name).join(', ')}{/if}
</p>
<a href="/trade/proposal/{p.id}" class="see-proposal-btn">See Proposal</a>
</div>
{/each}
{/if}
{#if resolved.length > 0}
<p class="proposal-subhead">History</p>
{#each resolved as p (p.id)}
<div class="proposal-card resolved">
<div class="proposal-meta">
<span class="proposal-to">{p.direction === 'incoming' ? `From: ${p.proposer_username}` : `To: ${p.recipient_username}`}</span>
<span class="proposal-status {p.status}">{p.status}</span>
</div>
<p class="proposal-desc">{p.requested_cards.length} requested · {p.offered_cards.length} offered</p>
<a href="/trade/proposal/{p.id}" class="see-proposal-btn">See Proposal</a>
</div>
{/each}
{/if}
{/if}
{#if challenges.length > 0}
<div class="section-divider"></div>
<h2 class="section-title">Game Challenges</h2>
{@const pendingOut = challenges.filter((c: any) => c.direction === 'outgoing' && c.status === 'pending')}
{@const pendingIn = challenges.filter((c: any) => c.direction === 'incoming' && c.status === 'pending')}
{@const resolvedC = challenges.filter((c: any) => c.status !== 'pending')}
{#if pendingOut.length > 0}
<p class="proposal-subhead">Sent</p>
{#each pendingOut as c (c.id)}
<div class="proposal-card">
<div class="proposal-meta">
<span class="proposal-to">To: <a href="/profile/{c.challenged_username}" target="_blank" class="friend-name">{c.challenged_username}</a></span>
<span class="proposal-status pending">Awaiting response</span>
<span class="proposal-expires">{formatChallengeExpiry(c.expires_at, tick)}</span>
</div>
<p class="proposal-desc">Deck: <strong>{c.deck_name}</strong></p>
{#if confirmingWithdraw.has(c.id)}
<span class="confirm-label">Withdraw challenge?</span>
<button class="confirm-yes-btn" onclick={() => withdrawChallenge(c.id)}>Confirm</button>
<button class="confirm-no-btn" onclick={() => { confirmingWithdraw.delete(c.id); confirmingWithdraw = confirmingWithdraw; }}>Cancel</button>
{:else}
<button class="withdraw-btn" onclick={() => { confirmingWithdraw.add(c.id); confirmingWithdraw = confirmingWithdraw; }}>Withdraw</button>
{/if}
</div>
{/each}
{/if}
{#if pendingIn.length > 0}
<p class="proposal-subhead">Incoming</p>
{#each pendingIn as c (c.id)}
<div class="proposal-card">
<div class="proposal-meta">
<a href="/profile/{c.challenger_username}" target="_blank" class="friend-name">{c.challenger_username}</a>
<span class="proposal-status pending">Pending</span>
<span class="proposal-expires">{formatChallengeExpiry(c.expires_at, tick)}</span>
</div>
<p class="proposal-desc">Their deck: <strong>{c.deck_name}</strong></p>
<p class="proposal-desc">Check your notification bell to accept.</p>
</div>
{/each}
{/if}
{#if resolvedC.length > 0}
<p class="proposal-subhead">History</p>
{#each resolvedC as c (c.id)}
<div class="proposal-card resolved">
<div class="proposal-meta">
<span class="proposal-to">
{c.direction === 'outgoing' ? `To: ${c.challenged_username}` : `From: ${c.challenger_username}`}
</span>
<span class="proposal-status {c.status}">{c.status}</span>
</div>
</div>
{/each}
{/if}
{/if}
</div>
{/if}
</main>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap');
main {
height: 100vh;
overflow-y: auto;
background: #0d0a04;
background: var(--color-bg);
padding: 2rem;
}
@@ -149,6 +401,7 @@
display: flex;
flex-direction: column;
gap: 1.5rem;
padding-bottom: 5rem;
}
.profile-header {
@@ -160,16 +413,16 @@
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
background: #3d2507;
border: 2px solid #c8861a;
border-radius: var(--radius-full);
background: var(--color-surface-raised);
border: 2px solid var(--color-bronze);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Cinzel', serif;
font-size: 28px;
font-size: var(--text-2xl);
font-weight: 700;
color: #f0d080;
color: var(--color-gold);
flex-shrink: 0;
}
@@ -182,22 +435,22 @@
.username {
font-family: 'Cinzel', serif;
font-size: 24px;
font-size: var(--text-xl);
font-weight: 700;
color: #f0d080;
color: var(--color-gold);
margin: 0;
}
.email {
font-family: 'Crimson Text', serif;
font-size: 15px;
font-size: var(--text-md);
color: rgba(240, 180, 80, 0.5);
margin: 0;
}
.unverified-badge {
font-family: 'Cinzel', serif;
font-size: 9px;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
@@ -211,7 +464,7 @@
.resend-btn {
font-family: 'Crimson Text', serif;
font-size: 13px;
font-size: var(--text-base);
font-style: italic;
color: rgba(240, 180, 80, 0.5);
background: none;
@@ -228,7 +481,7 @@
.joined {
font-family: 'Crimson Text', serif;
font-size: 13px;
font-size: var(--text-base);
font-style: italic;
color: rgba(240, 180, 80, 0.35);
margin: 0;
@@ -236,29 +489,29 @@
.logout-btn {
font-family: 'Cinzel', serif;
font-size: 11px;
font-size: var(--btn-font-md);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: none;
border: 1px solid rgba(180, 60, 60, 0.4);
border-radius: 4px;
border-radius: var(--radius-sm);
color: rgba(200, 80, 80, 0.7);
padding: 8px 16px;
padding: var(--btn-padding-md);
cursor: pointer;
transition: all 0.15s;
align-self: flex-start;
}
.logout-btn:hover {
border-color: #c84040;
border-color: var(--color-error);
color: #e05050;
background: rgba(180, 40, 40, 0.1);
}
.reset-link {
font-family: 'Crimson Text', serif;
font-size: 14px;
font-size: var(--text-base);
font-style: italic;
color: rgba(240, 180, 80, 0.4);
text-decoration: underline;
@@ -276,38 +529,39 @@
.shards-link {
font-family: 'Cinzel', serif;
font-size: 10px;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(126, 207, 207, 0.6);
border: 1px solid rgba(126, 207, 207, 0.3);
border-radius: 4px;
border-radius: var(--radius-sm);
padding: 3px 8px;
text-decoration: none;
margin-top: 4px;
margin-left: 0.5rem;
transition: color 0.15s, border-color 0.15s;
}
.shards-link:hover { color: #7ecfcf; border-color: rgba(126, 207, 207, 0.7); }
.shards-link:hover { color: var(--color-cyan); border-color: rgba(126, 207, 207, 0.7); }
.shards-icon {
font-size: 22px;
color: #7ecfcf;
font-size: var(--text-xl);
color: var(--color-cyan);
position: relative;
top: -0.1em;
animation: shard-pulse 3s ease-in-out infinite;
}
.shards-value {
font-family: 'Cinzel', serif;
font-size: 28px;
font-size: var(--text-2xl);
font-weight: 700;
color: #7ecfcf;
color: var(--color-cyan);
}
.shards-label {
font-family: 'Cinzel', serif;
font-size: 11px;
font-size: var(--text-sm);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
@@ -317,16 +571,16 @@
.section-divider {
height: 1px;
background: rgba(107, 76, 30, 0.3);
background: var(--color-border-dim);
}
.section-title {
font-family: 'Cinzel', serif;
font-size: 13px;
font-size: var(--text-base);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(240, 180, 80, 0.4);
color: var(--color-gold-faint);
margin: 0;
}
@@ -337,9 +591,9 @@
}
.stat-card {
background: #1a1008;
border: 1px solid rgba(107, 76, 30, 0.3);
border-radius: 8px;
background: var(--color-surface);
border: 1px solid var(--color-border-dim);
border-radius: var(--radius-lg);
padding: 1rem;
display: flex;
flex-direction: column;
@@ -348,24 +602,24 @@
.stat-label {
font-family: 'Cinzel', serif;
font-size: 9px;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(240, 180, 80, 0.4);
color: var(--color-gold-faint);
}
.stat-value {
font-family: 'Cinzel', serif;
font-size: 28px;
font-size: var(--text-2xl);
font-weight: 700;
color: #f0d080;
color: var(--color-gold);
}
.wins { color: #6aaa6a; }
.losses { color: #c85050; }
.good-wr { color: #6aaa6a; }
.bad-wr { color: #c85050; }
.wins { color: var(--color-success); }
.losses { color: var(--color-error); }
.good-wr { color: var(--color-success); }
.bad-wr { color: var(--color-error); }
.highlights {
display: grid;
@@ -374,9 +628,9 @@
}
.highlight-card {
background: #1a1008;
border: 1px solid rgba(107, 76, 30, 0.3);
border-radius: 8px;
background: var(--color-surface);
border: 1px solid var(--color-border-dim);
border-radius: var(--radius-lg);
padding: 1rem;
display: flex;
flex-direction: column;
@@ -385,23 +639,23 @@
.highlight-label {
font-family: 'Cinzel', serif;
font-size: 9px;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(240, 180, 80, 0.4);
color: var(--color-gold-faint);
}
.highlight-value {
font-family: 'Cinzel', serif;
font-size: 16px;
font-size: var(--text-md);
font-weight: 700;
color: #f0d080;
color: var(--color-gold);
}
.highlight-sub {
font-family: 'Crimson Text', serif;
font-size: 13px;
font-size: var(--text-base);
font-style: italic;
color: rgba(240, 180, 80, 0.45);
}
@@ -418,8 +672,8 @@
height: 48px;
object-fit: cover;
object-position: top;
border-radius: 4px;
border: 1px solid rgba(107, 76, 30, 0.4);
border-radius: var(--radius-sm);
border: 1px solid var(--color-border-subtle);
flex-shrink: 0;
}
@@ -429,22 +683,303 @@
gap: 0.2rem;
}
.wishlist-group {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.wishlist-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.wishlist-hint {
font-family: 'Crimson Text', serif;
font-size: var(--text-base);
font-style: italic;
color: rgba(240, 180, 80, 0.35);
margin: 0;
}
.wishlist-textarea {
width: 100%;
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
color: var(--color-gold);
font-family: 'Crimson Text', serif;
font-size: var(--text-md);
padding: 0.6rem 0.75rem;
resize: vertical;
box-sizing: border-box;
transition: border-color 0.15s;
outline: none;
}
.wishlist-textarea:focus { border-color: var(--color-bronze); }
.wishlist-textarea::placeholder { color: rgba(240, 180, 80, 0.25); }
.wishlist-actions {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.save-btn {
font-family: 'Cinzel', serif;
font-size: var(--btn-font-md);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: var(--color-surface-raised);
border: 1px solid var(--color-bronze);
border-radius: var(--radius-sm);
color: var(--color-btn-text);
padding: var(--btn-padding-md);
cursor: pointer;
transition: all 0.15s;
}
.save-btn:hover:not(:disabled) { background: #4d3010; }
.save-btn:disabled { opacity: 0.5; cursor: default; }
.wishlist-ok {
font-family: 'Crimson Text', serif;
font-size: var(--text-base);
color: var(--color-success);
}
.wishlist-error {
font-family: 'Crimson Text', serif;
font-size: var(--text-base);
color: var(--color-error);
margin: 0;
}
.no-friends {
font-family: 'Crimson Text', serif;
font-size: var(--text-base);
font-style: italic;
color: rgba(240, 180, 80, 0.25);
margin: 0;
}
.friends-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.friend-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem 0.75rem;
background: var(--color-surface);
border: 1px solid var(--color-border-dim);
border-radius: var(--radius-md);
}
.friend-name {
font-family: 'Cinzel', serif;
font-size: var(--text-base);
font-weight: 700;
color: var(--color-gold);
text-decoration: none;
flex: 1;
transition: color 0.15s;
}
.friend-name:hover { color: var(--color-bronze); }
.unfriend-btn {
font-family: 'Cinzel', serif;
font-size: var(--btn-font-md);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: rgba(180, 40, 40, 0.8);
border: none;
border-radius: var(--radius-md);
color: #fff;
padding: var(--btn-padding-md);
cursor: pointer;
transition: background 0.15s;
}
.unfriend-btn:hover { background: rgba(210, 50, 50, 0.9); }
.no-data {
font-family: 'Crimson Text', serif;
font-size: 14px;
font-size: var(--text-base);
font-style: italic;
color: rgba(240, 180, 80, 0.25);
}
/* ── Trade Proposals ── */
.proposal-subhead {
font-family: 'Cinzel', serif;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(240, 180, 80, 0.3);
margin: 0.5rem 0 0;
}
.proposal-card {
background: var(--color-surface);
border: 1px solid var(--color-border-dim);
border-radius: var(--radius-lg);
padding: 0.85rem 1rem;
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.proposal-card.resolved { opacity: 0.5; }
.proposal-meta {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
.proposal-to {
font-family: 'Crimson Text', serif;
font-size: var(--text-base);
color: rgba(240, 180, 80, 0.5);
}
.proposal-status {
font-family: 'Cinzel', serif;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
border-radius: 3px;
padding: 1px 5px;
border: 1px solid;
}
.proposal-status.pending { color: var(--color-bronze); border-color: rgba(200, 134, 26, 0.4); }
.proposal-status.accepted { color: var(--color-success); border-color: rgba(106, 170, 106, 0.4); }
.proposal-status.declined { color: var(--color-error); border-color: rgba(200, 64, 64, 0.4); }
.proposal-status.expired { color: rgba(240, 180, 80, 0.3); border-color: var(--color-border-dim); }
.proposal-status.withdrawn { color: rgba(240, 180, 80, 0.3); border-color: var(--color-border-dim); }
.proposal-expires {
font-family: 'Crimson Text', serif;
font-size: var(--text-sm);
font-style: italic;
color: rgba(240, 180, 80, 0.3);
margin-left: auto;
}
.proposal-desc {
font-family: 'Crimson Text', serif;
font-size: var(--text-base);
color: rgba(240, 180, 80, 0.65);
margin: 0;
line-height: 1.5;
}
.proposal-desc strong { color: var(--color-gold); font-style: normal; font-weight: 600; }
.see-proposal-btn {
font-family: 'Cinzel', serif;
font-size: var(--btn-font-md);
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
color: var(--color-gold);
padding: var(--btn-padding-md);
text-decoration: none;
display: inline-block;
transition: all 0.15s;
margin-top: 0.25rem;
align-self: flex-start;
}
.see-proposal-btn:hover { border-color: var(--color-bronze); background: #4d3010; }
.withdraw-btn {
font-family: 'Cinzel', serif;
font-size: var(--btn-font-sm);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: none;
border: 1px solid rgba(180, 60, 60, 0.4);
border-radius: var(--radius-sm);
color: rgba(200, 80, 80, 0.6);
padding: var(--btn-padding-sm);
cursor: pointer;
transition: all 0.15s;
align-self: flex-start;
}
.withdraw-btn:hover { border-color: var(--color-error); color: #e05050; }
.status {
font-family: 'Crimson Text', serif;
font-size: 16px;
font-size: var(--text-md);
font-style: italic;
color: rgba(240, 180, 80, 0.5);
text-align: center;
margin-top: 4rem;
}
.confirm-label {
font-family: 'Crimson Text', serif;
font-size: var(--text-base);
font-style: italic;
color: rgba(200, 80, 80, 0.8);
}
.confirm-yes-btn {
font-family: 'Cinzel', serif;
font-size: var(--btn-font-sm);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: rgba(180, 40, 40, 0.8);
border: none;
border-radius: var(--radius-sm);
color: #fff;
padding: var(--btn-padding-sm);
cursor: pointer;
transition: background 0.15s;
}
.confirm-yes-btn:hover { background: rgba(210, 50, 50, 0.9); }
.confirm-no-btn {
font-family: 'Cinzel', serif;
font-size: var(--btn-font-sm);
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: none;
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
color: var(--color-gold-faint);
padding: var(--btn-padding-sm);
cursor: pointer;
transition: all 0.15s;
}
.confirm-no-btn:hover { border-color: rgba(107, 76, 30, 0.7); color: rgba(240, 180, 80, 0.7); }
@media (max-width: 640px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.highlights { grid-template-columns: 1fr; }