Add flair badge gamification system with custom SVG badges

- Create 12 Office Space themed SVG badges (extraction, basement, printer,
  coffee, tps, bobs, memo, oface, stapler, conclusions, spreadsheet, flair-badge)
- Implement FlairBadge.astro component with localStorage persistence
- Add 15-second timer per page to earn flair
- Create floating counter, modal flair board, toast notifications
- Override Starlight Footer to inject flair system globally
- Add name capture dialog on first flair earned

Features:
- Badges glow golden when earned, grayscale when locked
- Progress persists across browser sessions
- Stan quote: "We need to talk about your document processing..."
This commit is contained in:
Ryan Malloy 2026-01-11 14:24:39 -07:00
parent 32b41f79d9
commit 5ae7040496
28 changed files with 1410 additions and 11 deletions

View file

@ -0,0 +1,979 @@
---
/**
* FlairBadge - Collect pieces of flair by spending time on key pages
* Adapted for Starlight documentation site
*
* "We need to talk about your flair..." - Stan, Manager
*
* Features:
* - Floating counter in bottom-right corner
* - Opens modal "flair board" showing collection
* - 15-second timer per page to earn flair
* - localStorage persistence across sessions
* - Astro view transition support
*/
import flairConfig from '../data/flair-config.json';
interface Props {
currentPath: string;
}
const { currentPath } = Astro.props;
// Normalize path for matching (handle trailing slashes)
const normalizedPath = currentPath === '/' ? '/' : currentPath.replace(/\/$/, '') + '/';
const altPath = currentPath === '/' ? '/' : currentPath.replace(/\/$/, '');
// Check if current page has flair (try both with and without trailing slash)
const currentFlair = flairConfig.flairs.find(f =>
f.path === normalizedPath || f.path === altPath || f.path === currentPath
);
---
<!-- Floating Flair Counter -->
<button
id="flair-counter"
type="button"
class="flair-counter-btn"
aria-label="Open flair collection"
>
<span class="flair-icon">🎖️</span>
<span class="flair-count">0</span>
<span class="flair-divider">/</span>
<span class="flair-total">{flairConfig.flairs.length}</span>
<!-- Progress ring indicator -->
<span id="flair-progress-indicator" class="progress-indicator hidden"></span>
</button>
<!-- Flair Earned Toast -->
<div id="flair-toast" class="flair-toast hidden-right">
<div class="toast-icon-container">
<img id="toast-image" src="" alt="" class="toast-image hidden" />
<span class="toast-emoji" id="toast-icon">🎉</span>
</div>
<div class="toast-content">
<p class="toast-title">Flair Earned!</p>
<p class="toast-name" id="toast-name">Badge Name</p>
</div>
</div>
<!-- Name Capture Dialog -->
<dialog id="name-dialog" class="name-dialog">
<div class="dialog-content">
<div class="dialog-header">
<span id="name-dialog-icon" class="dialog-icon">🎖️</span>
<h2 id="name-dialog-title" class="dialog-title">You earned your first flair!</h2>
<p id="name-dialog-subtitle" class="dialog-subtitle">What should we call you?</p>
</div>
<form id="name-form" class="name-form">
<input
type="text"
id="visitor-name-input"
placeholder="Your name"
maxlength="30"
class="name-input"
/>
<div class="dialog-buttons">
<button type="button" id="skip-name-btn" class="btn-secondary">Skip</button>
<button type="submit" class="btn-primary">Save</button>
</div>
</form>
</div>
</dialog>
<!-- Flair Board Modal - Stan's Chotchkie's Vest -->
<dialog id="flair-modal" class="flair-modal">
<div class="modal-container">
<!-- Close button -->
<button
type="button"
id="flair-modal-close"
class="modal-close"
aria-label="Close flair board"
>
<svg class="close-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Vest Container -->
<div class="vest-container">
<!-- Header -->
<div class="vest-header">
<h2 class="vest-title" id="modal-title">Your Flair Collection</h2>
<div class="name-tag-wrapper">
<div class="name-tag">
<span id="vest-name-tag">VISITOR</span>
<button type="button" id="edit-name-btn" class="edit-name-btn" aria-label="Edit your name" title="Edit your name">
<svg class="edit-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z" />
</svg>
</button>
</div>
</div>
</div>
<!-- Flair Grid -->
<div class="flair-grid">
{flairConfig.flairs.map((flair, index) => {
const rotations = [-5, 3, -2, 6, -4, 2, 5, -3, 4, -6, 3, -2];
const rotation = rotations[index % rotations.length];
return (
<div
class="flair-item"
data-flair-id={flair.id}
data-flair-image={flair.image || ''}
data-flair-placeholder={flair.placeholder}
data-earned="false"
style={`--rotation: ${rotation}deg`}
>
<div class="flair-badge">
{flair.image ? (
<img src={flair.image} alt={flair.name} class="badge-image" />
) : (
<span class="badge-emoji">{flair.placeholder}</span>
)}
</div>
<div class="flair-tooltip">
<span class="tooltip-name">{flair.name}</span>
<span class="tooltip-desc">{flair.description}</span>
</div>
</div>
);
})}
</div>
<!-- Progress Footer -->
<div class="vest-footer">
<div class="progress-counter">
<span class="earned-count" id="modal-earned-count">0</span>
<span class="count-divider">/</span>
<span class="total-count">{flairConfig.flairs.length}</span>
<span class="count-label">pieces of flair</span>
</div>
<div class="stan-quote">
<p class="quote-text">"{flairConfig.stanQuote}"</p>
<p class="quote-attribution">— Stan, Manager</p>
</div>
<!-- Completion message -->
<div id="completion-message" class="completion-message hidden">
<p>{flairConfig.completionMessage}</p>
</div>
<!-- Current page hint -->
{currentFlair && (
<div class="current-page-hint">
<span class="hint-label">Current page:</span>
<span class="hint-name">{currentFlair.name}</span>
<span id="timer-display" class="timer-display"></span>
</div>
)}
</div>
</div>
</div>
</dialog>
<script define:vars={{ flairConfig, currentFlair }}>
// Flair Collection System
const STORAGE_KEY = flairConfig.storageKey;
const REQUIRED_TIME = flairConfig.requiredTime;
let pageTimer = null;
let timeSpent = 0;
let isTimerRunning = false;
// Get/set flair data from localStorage
function getFlairData() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : { earned: [], timestamps: {}, visitorName: null, namePrompted: false };
} catch {
return { earned: [], timestamps: {}, visitorName: null, namePrompted: false };
}
}
function saveFlairData(data) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (e) {
console.warn('Could not save flair data:', e);
}
}
// Check if flair is earned
function hasEarnedFlair(flairId) {
const data = getFlairData();
return data.earned.includes(flairId);
}
// Award flair
function awardFlair(flairId) {
const data = getFlairData();
if (!data.earned.includes(flairId)) {
const isFirstFlair = data.earned.length === 0;
data.earned.push(flairId);
data.timestamps[flairId] = Date.now();
saveFlairData(data);
updateUI();
showToast(flairId);
// Prompt for name on first flair
if (isFirstFlair && !data.namePrompted) {
setTimeout(() => showNamePrompt(), 3500);
}
return true;
}
return false;
}
// Show name prompt dialog
function showNamePrompt(mode = 'first-flair') {
const nameDialog = document.getElementById('name-dialog');
const iconEl = document.getElementById('name-dialog-icon');
const titleEl = document.getElementById('name-dialog-title');
const subtitleEl = document.getElementById('name-dialog-subtitle');
const input = document.getElementById('visitor-name-input');
const skipBtn = document.getElementById('skip-name-btn');
if (!nameDialog) return;
if (mode === 'edit') {
if (iconEl) iconEl.textContent = '✏️';
if (titleEl) titleEl.textContent = 'Update Your Name';
if (subtitleEl) subtitleEl.textContent = 'Change how your flair collection is personalized';
if (skipBtn) skipBtn.textContent = 'Cancel';
const data = getFlairData();
if (input && data.visitorName) {
input.value = data.visitorName;
}
} else {
if (iconEl) iconEl.textContent = '🎖️';
if (titleEl) titleEl.textContent = 'You earned your first flair!';
if (subtitleEl) subtitleEl.textContent = 'What should we call you?';
if (skipBtn) skipBtn.textContent = 'Skip';
if (input) input.value = '';
}
nameDialog.showModal();
if (input) {
input.focus();
if (mode === 'edit') input.select();
}
}
// Save visitor name
function saveVisitorName(name) {
const data = getFlairData();
data.visitorName = name || null;
data.namePrompted = true;
saveFlairData(data);
updateModalTitle();
}
// Update modal title and vest name tag
function updateModalTitle() {
const data = getFlairData();
const titleEl = document.getElementById('modal-title');
const vestNameTag = document.getElementById('vest-name-tag');
if (titleEl && data.visitorName) {
titleEl.textContent = `${data.visitorName}'s Flair Collection`;
} else if (titleEl) {
titleEl.textContent = 'Your Flair Collection';
}
if (vestNameTag) {
vestNameTag.textContent = data.visitorName || 'VISITOR';
}
}
// Show toast notification
function showToast(flairId) {
const flair = flairConfig.flairs.find(f => f.id === flairId);
if (!flair) return;
const toast = document.getElementById('flair-toast');
const toastIcon = document.getElementById('toast-icon');
const toastImage = document.getElementById('toast-image');
const toastName = document.getElementById('toast-name');
if (toast && toastIcon && toastImage && toastName) {
if (flair.image) {
toastImage.src = flair.image;
toastImage.alt = flair.name;
toastImage.classList.remove('hidden');
toastIcon.classList.add('hidden');
} else {
toastIcon.textContent = flair.placeholder;
toastIcon.classList.remove('hidden');
toastImage.classList.add('hidden');
}
toastName.textContent = flair.name;
toast.classList.remove('hidden-right');
setTimeout(() => {
toast.classList.add('hidden-right');
}, 3000);
}
}
// Update all UI elements
function updateUI() {
const data = getFlairData();
const earnedCount = data.earned.length;
const totalCount = flairConfig.flairs.length;
// Update counter
const countEl = document.querySelector('.flair-count');
if (countEl) countEl.textContent = earnedCount.toString();
// Update modal
const modalCount = document.getElementById('modal-earned-count');
if (modalCount) modalCount.textContent = earnedCount.toString();
// Update flair grid
document.querySelectorAll('.flair-item').forEach((item) => {
const flairId = item.getAttribute('data-flair-id');
const isEarned = data.earned.includes(flairId);
item.setAttribute('data-earned', isEarned.toString());
});
// Show completion message if all earned
const completionMsg = document.getElementById('completion-message');
if (completionMsg) {
completionMsg.classList.toggle('hidden', earnedCount < totalCount);
}
}
// Timer display update
function updateTimerDisplay() {
const timerEl = document.getElementById('timer-display');
if (!timerEl || !currentFlair) return;
if (hasEarnedFlair(currentFlair.id)) {
timerEl.textContent = '✓ Earned!';
timerEl.classList.add('earned');
} else if (isTimerRunning) {
const remaining = Math.max(0, REQUIRED_TIME - timeSpent);
timerEl.textContent = `(${remaining}s remaining)`;
timerEl.classList.remove('earned');
}
}
// Start page timer
function startTimer() {
if (!currentFlair || hasEarnedFlair(currentFlair.id)) {
updateTimerDisplay();
return;
}
isTimerRunning = true;
timeSpent = 0;
const indicator = document.getElementById('flair-progress-indicator');
if (indicator) indicator.classList.remove('hidden');
pageTimer = setInterval(() => {
timeSpent++;
updateTimerDisplay();
if (timeSpent >= REQUIRED_TIME) {
stopTimer();
awardFlair(currentFlair.id);
if (indicator) indicator.classList.add('hidden');
}
}, 1000);
updateTimerDisplay();
}
// Stop timer
function stopTimer() {
isTimerRunning = false;
if (pageTimer) {
clearInterval(pageTimer);
pageTimer = null;
}
}
// Initialize
function init() {
updateUI();
updateModalTitle();
// Counter click opens modal
const counter = document.getElementById('flair-counter');
const modal = document.getElementById('flair-modal');
const closeBtn = document.getElementById('flair-modal-close');
if (counter && modal) {
counter.addEventListener('click', () => modal.showModal());
}
if (closeBtn && modal) {
closeBtn.addEventListener('click', () => modal.close());
}
// Edit name button
const editNameBtn = document.getElementById('edit-name-btn');
if (editNameBtn && modal) {
editNameBtn.addEventListener('click', () => {
modal.close();
showNamePrompt('edit');
});
}
// Close modal on backdrop click
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.close();
});
}
// Name form handlers
const nameDialog = document.getElementById('name-dialog');
const nameForm = document.getElementById('name-form');
const skipBtn = document.getElementById('skip-name-btn');
const nameInput = document.getElementById('visitor-name-input');
if (nameForm && nameDialog) {
nameForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = nameInput?.value.trim();
saveVisitorName(name);
nameDialog.close();
});
}
if (skipBtn && nameDialog) {
skipBtn.addEventListener('click', () => {
saveVisitorName(null);
nameDialog.close();
});
}
if (nameDialog) {
nameDialog.addEventListener('click', (e) => {
if (e.target === nameDialog) {
saveVisitorName(null);
nameDialog.close();
}
});
}
// Start timer for current page
startTimer();
}
// Cleanup on page transition
function cleanup() {
stopTimer();
}
// Handle Astro view transitions
document.addEventListener('astro:before-swap', cleanup);
document.addEventListener('astro:page-load', init);
// Initial load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script>
<style>
/* Reset and base styles */
.hidden { display: none !important; }
.hidden-right { transform: translateX(150%); }
/* Floating Counter Button */
.flair-counter-btn {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 100;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(8px);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 9999px;
color: white;
font-family: system-ui, sans-serif;
font-size: 0.875rem;
cursor: pointer;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
}
.flair-counter-btn:hover {
background: rgba(30, 41, 59, 0.95);
border-color: rgba(245, 158, 11, 0.5);
transform: scale(1.02);
}
.flair-icon { font-size: 1.125rem; }
.flair-count { font-weight: 600; color: #fbbf24; }
.flair-divider { color: #64748b; }
.flair-total { color: #cbd5e1; }
.progress-indicator {
position: absolute;
top: -0.25rem;
right: -0.25rem;
width: 0.75rem;
height: 0.75rem;
background: #f59e0b;
border-radius: 9999px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Toast Notification */
.flair-toast {
position: fixed;
bottom: 5rem;
right: 1.5rem;
z-index: 100;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: rgba(6, 78, 59, 0.95);
backdrop-filter: blur(8px);
border: 1px solid rgba(16, 185, 129, 0.5);
border-radius: 0.75rem;
color: white;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease;
}
.toast-icon-container {
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
}
.toast-image {
width: 3rem;
height: 3rem;
object-fit: contain;
}
.toast-emoji { font-size: 1.5rem; }
.toast-title { font-weight: 600; color: #6ee7b7; margin: 0; }
.toast-name { font-size: 0.875rem; color: #d1fae5; margin: 0; }
/* Dialogs */
.name-dialog,
.flair-modal {
padding: 0;
border: none;
border-radius: 1rem;
background: transparent;
max-width: 90vw;
}
.name-dialog::backdrop,
.flair-modal::backdrop {
background: rgba(0, 0, 0, 0.85);
}
.name-dialog[open],
.flair-modal[open] {
animation: modal-appear 0.3s ease-out;
}
@keyframes modal-appear {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* Name Dialog */
.name-dialog {
max-width: 24rem;
width: 100%;
background: #0f172a;
border: 1px solid rgba(245, 158, 11, 0.5);
}
.dialog-content { padding: 1.5rem; }
.dialog-header {
text-align: center;
margin-bottom: 1.5rem;
}
.dialog-icon {
font-size: 2.5rem;
display: block;
margin-bottom: 1rem;
}
.dialog-title {
font-size: 1.25rem;
font-weight: 700;
color: white;
margin: 0 0 0.5rem;
}
.dialog-subtitle {
font-size: 0.875rem;
color: #94a3b8;
margin: 0;
}
.name-form { display: flex; flex-direction: column; gap: 1rem; }
.name-input {
width: 100%;
padding: 0.75rem 1rem;
background: #1e293b;
border: 1px solid #475569;
border-radius: 0.75rem;
color: white;
font-size: 1rem;
transition: all 0.2s;
}
.name-input:focus {
outline: none;
border-color: #f59e0b;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
}
.name-input::placeholder { color: #64748b; }
.dialog-buttons {
display: flex;
gap: 0.75rem;
}
.btn-secondary,
.btn-primary {
flex: 1;
padding: 0.625rem 1rem;
border-radius: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-secondary {
background: #334155;
color: #cbd5e1;
}
.btn-secondary:hover { background: #475569; }
.btn-primary {
background: #f59e0b;
color: white;
}
.btn-primary:hover { background: #d97706; }
/* Flair Modal */
.flair-modal {
max-width: 32rem;
width: 100%;
}
.modal-container { position: relative; }
.modal-close {
position: absolute;
top: -0.5rem;
right: -0.5rem;
z-index: 10;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
background: #1e293b;
border: 1px solid #475569;
border-radius: 9999px;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
transition: all 0.2s;
}
.modal-close:hover {
background: #dc2626;
color: white;
}
.close-icon { width: 1rem; height: 1rem; }
/* Vest Container */
.vest-container {
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.vest-header {
padding: 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.vest-title {
font-size: 1.125rem;
font-weight: 700;
color: white;
margin: 0;
}
.name-tag-wrapper { display: flex; align-items: center; }
.name-tag {
background: #16a34a;
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 700;
color: white;
letter-spacing: 0.05em;
position: relative;
display: flex;
align-items: center;
gap: 0.5rem;
}
.edit-name-btn {
width: 1.25rem;
height: 1.25rem;
background: rgba(0, 0, 0, 0.3);
border: none;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.edit-name-btn:hover { background: #f59e0b; }
.edit-icon { width: 0.625rem; height: 0.625rem; color: white; }
/* Flair Grid */
.flair-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
padding: 1.5rem;
background: linear-gradient(180deg, #334155 0%, #1e293b 100%);
}
.flair-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
transform: rotate(var(--rotation, 0deg));
transition: transform 0.2s;
}
.flair-item:hover {
transform: rotate(var(--rotation, 0deg)) scale(1.1);
z-index: 5;
}
.flair-badge {
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
background: #475569;
border: 2px solid #64748b;
border-radius: 9999px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
}
.flair-item[data-earned="false"] .flair-badge {
filter: grayscale(100%);
opacity: 0.4;
}
.flair-item[data-earned="true"] .flair-badge {
filter: none;
opacity: 1;
background: linear-gradient(145deg, #fcd34d 0%, #f59e0b 50%, #d97706 100%);
border-color: #b45309;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.3),
0 4px 8px rgba(251, 191, 36, 0.3),
inset 0 1px 2px rgba(255, 255, 255, 0.4);
}
.badge-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.badge-emoji { font-size: 1.25rem; }
/* Flair Tooltip */
.flair-tooltip {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(0, 0, 0, 0.95);
border-radius: 0.375rem;
opacity: 0;
visibility: hidden;
transition: all 0.2s;
pointer-events: none;
white-space: nowrap;
z-index: 10;
text-align: center;
}
.flair-item:hover .flair-tooltip {
opacity: 1;
visibility: visible;
}
.tooltip-name {
display: block;
font-size: 0.6875rem;
font-weight: 600;
color: white;
}
.tooltip-desc {
display: block;
font-size: 0.625rem;
color: #94a3b8;
margin-top: 0.125rem;
}
/* Vest Footer */
.vest-footer {
padding: 1.25rem 1.5rem;
text-align: center;
background: #0f172a;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.progress-counter {
display: inline-flex;
align-items: baseline;
gap: 0.25rem;
background: rgba(0, 0, 0, 0.3);
padding: 0.5rem 1rem;
border-radius: 9999px;
margin-bottom: 0.75rem;
}
.earned-count {
font-size: 1.5rem;
font-weight: 700;
color: #fbbf24;
}
.count-divider { color: #64748b; }
.total-count { font-size: 1.125rem; color: #cbd5e1; }
.count-label { font-size: 0.75rem; color: #64748b; margin-left: 0.25rem; }
.stan-quote {
max-width: 16rem;
margin: 0 auto 0.75rem;
}
.quote-text {
font-size: 0.6875rem;
font-style: italic;
color: rgba(251, 191, 36, 0.9);
margin: 0;
line-height: 1.4;
}
.quote-attribution {
font-size: 0.625rem;
color: #64748b;
margin: 0.25rem 0 0;
}
.completion-message {
margin-top: 0.75rem;
padding: 0.5rem 1rem;
background: rgba(16, 185, 129, 0.9);
border: 1px solid #10b981;
border-radius: 0.5rem;
display: inline-block;
}
.completion-message p {
color: white;
font-weight: 600;
font-size: 0.875rem;
margin: 0;
}
.current-page-hint {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.8125rem;
color: #94a3b8;
}
.hint-label { color: #fbbf24; }
.hint-name { color: white; margin-left: 0.25rem; }
.timer-display { margin-left: 0.5rem; color: #64748b; }
.timer-display.earned { color: #10b981; }
/* Responsive */
@media (max-width: 480px) {
.flair-grid {
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
padding: 1rem;
}
.flair-badge {
width: 2.5rem;
height: 2.5rem;
}
.vest-header { padding: 1rem; }
.vest-footer { padding: 1rem; }
}
</style>

View file

@ -0,0 +1,15 @@
---
/**
* Custom Footer component that includes the FlairBadge system
* Wraps Starlight's default Footer
*/
import type { Props } from '@astrojs/starlight/props';
import Default from '@astrojs/starlight/components/Footer.astro';
import FlairBadge from './FlairBadge.astro';
// Get current path for flair matching
const currentPath = Astro.url.pathname;
---
<Default {...Astro.props}><slot /></Default>
<FlairBadge currentPath={currentPath} />