snipe/web/src/assets/theme.css
pyr0ball b993f6f4a9 feat(ux): active search indicator + Candycore easter egg theme
Search indicator:
- SearchProgress.vue: indeterminate amber progress bar + status line
  + 4 staggered skeleton cards shown while loading=true and no results yet
  (fills the previously-blank results area during initial scrape phase)
- Re-search badge: blue "Re-searching…" pill in toolbar when loading=true
  over existing stale results (distinct from the amber enrichment badge)

Candycore theme:
- New [data-candycore="active"] CSS block; palette sourced from
  snipe_v0_Neon_IPad_Paint.jpeg — purple-black sky, lavender primary,
  cyan glow, yellow crown, bubblegum pink text
- useCandycoreMode.ts: word trigger ("neon", typed outside form fields),
  ascending arpeggio audio, localStorage persistence, restore on reload
- Mutually exclusive with Snipe Mode (each deactivates the other)
- Added :not([data-candycore="active"]) guards to existing dark/light
  theme override selectors so they don't stomp on Candycore
2026-05-01 23:11:36 -07:00

384 lines
12 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* assets/theme.css — CENTRAL THEME FILE for Snipe
Dark tactical theme: near-black surfaces, amber accent, trust-signal colours.
ALL color/font/spacing tokens live here — nowhere else.
Snipe Mode easter egg: activated by Konami code (cf-snipe-mode in localStorage).
Planned theme variants (add as [data-theme="<name>"] blocks using the same token set):
solarized-dark — Ethan Schoonover's Solarized dark palette, amber accent
solarized-light — Solarized light palette, amber accent
high-contrast — WCAG AAA minimum contrast ratios, no mid-grey text
colorblind — Deuteranopia-safe trust signal colours (blue/orange instead of green/red)
*/
/* ── Snipe — dark tactical (default) ───────────────
Light variant is defined below via prefers-color-scheme.
Snipe Mode easter egg always overrides both.
*/
:root {
/* Brand — amber target reticle */
--app-primary: #f59e0b;
--app-primary-hover: #d97706;
--app-primary-light: rgba(245, 158, 11, 0.12);
/* Surfaces — near-black GitHub-dark inspired */
--color-surface: #0d1117;
--color-surface-2: #161b22;
--color-surface-raised: #1c2129;
/* Borders */
--color-border: #30363d;
--color-border-light: #21262d;
/* Text */
--color-text: #e6edf3;
--color-text-muted: #8b949e;
--color-text-inverse: #0d1117;
/* Trust signal colours */
--trust-high: #3fb950; /* composite_score >= 80 — green */
--trust-mid: #d29922; /* composite_score 5079 — amber */
--trust-low: #f85149; /* composite_score < 50 — red */
/* Semantic */
--color-success: #3fb950;
--color-error: #f85149;
--color-warning: #d29922;
--color-info: #58a6ff;
--color-accent: #a478ff; /* purple — csv import badge, secondary accent */
/* Typography */
--font-display: 'Fraunces', Georgia, serif;
--font-body: 'Atkinson Hyperlegible', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
/* Spacing scale */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-12: 3rem;
--space-16: 4rem;
--space-24: 6rem;
/* Radii */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 1rem;
--radius-full: 9999px;
/* Shadows — dark base */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.6), 0 4px 8px rgba(0, 0, 0, 0.3);
/* Transitions */
--transition: 200ms ease;
--transition-slow: 400ms ease;
/* Layout */
--sidebar-width: 220px;
}
/* ── Light mode — field notebook / tactical map ─────
Warm cream surfaces with the same amber accent.
Snipe Mode data attribute overrides this via higher specificity.
*/
/* Explicit dark override — beats OS preference when user forces dark in Settings */
[data-theme="dark"]:not([data-snipe-mode="active"]):not([data-candycore="active"]) {
--color-surface: #0d1117;
--color-surface-2: #161b22;
--color-surface-raised: #1c2129;
--color-border: #30363d;
--color-border-light: #21262d;
--color-text: #e6edf3;
--color-text-muted: #8b949e;
--color-text-inverse: #0d1117;
--app-primary: #f59e0b;
--app-primary-hover: #d97706;
--app-primary-light: rgba(245, 158, 11, 0.12);
--trust-high: #3fb950;
--trust-mid: #d29922;
--trust-low: #f85149;
--color-success: #3fb950;
--color-error: #f85149;
--color-warning: #d29922;
--color-info: #58a6ff;
--color-accent: #a478ff;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.3);
--shadow-md: 0 4px 12px rgba(0,0,0,0.5), 0 2px 4px rgba(0,0,0,0.3);
--shadow-lg: 0 10px 30px rgba(0,0,0,0.6), 0 4px 8px rgba(0,0,0,0.3);
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]):not([data-snipe-mode="active"]):not([data-candycore="active"]) {
/* Surfaces — warm cream, like a tactical field notebook */
--color-surface: #f8f5ee;
--color-surface-2: #f0ece3;
--color-surface-raised: #e8e3d8;
/* Borders — warm khaki */
--color-border: #c8bfae;
--color-border-light: #dbd3c4;
/* Text — warm near-black ink */
--color-text: #1c1a16;
--color-text-muted: #6b6357;
--color-text-inverse: #f8f5ee;
/* Brand — amber stays identical (works great on light too) */
--app-primary: #d97706; /* slightly deeper for contrast on light */
--app-primary-hover: #b45309;
--app-primary-light: rgba(217, 119, 6, 0.12);
/* Trust signals — same hues, adjusted for legibility on cream */
--trust-high: #16a34a;
--trust-mid: #b45309;
--trust-low: #dc2626;
/* Semantic */
--color-success: #16a34a;
--color-error: #dc2626;
--color-warning: #b45309;
--color-info: #2563eb;
--color-accent: #7c3aed; /* purple — deeper for contrast on cream */
/* Shadows — lighter, warm tint */
--shadow-sm: 0 1px 3px rgba(60, 45, 20, 0.12), 0 1px 2px rgba(60, 45, 20, 0.08);
--shadow-md: 0 4px 12px rgba(60, 45, 20, 0.15), 0 2px 4px rgba(60, 45, 20, 0.1);
--shadow-lg: 0 10px 30px rgba(60, 45, 20, 0.2), 0 4px 8px rgba(60, 45, 20, 0.1);
}
}
/* Explicit light override — beats OS preference when user forces light in Settings */
[data-theme="light"]:not([data-snipe-mode="active"]):not([data-candycore="active"]) {
--color-surface: #f8f5ee;
--color-surface-2: #f0ece3;
--color-surface-raised: #e8e3d8;
--color-border: #c8bfae;
--color-border-light: #dbd3c4;
--color-text: #1c1a16;
--color-text-muted: #6b6357;
--color-text-inverse: #f8f5ee;
--app-primary: #d97706;
--app-primary-hover: #b45309;
--app-primary-light: rgba(217, 119, 6, 0.12);
--trust-high: #16a34a;
--trust-mid: #b45309;
--trust-low: #dc2626;
--color-success: #16a34a;
--color-error: #dc2626;
--color-warning: #b45309;
--color-info: #2563eb;
--color-accent: #7c3aed;
--shadow-sm: 0 1px 3px rgba(60,45,20,0.12), 0 1px 2px rgba(60,45,20,0.08);
--shadow-md: 0 4px 12px rgba(60,45,20,0.15), 0 2px 4px rgba(60,45,20,0.1);
--shadow-lg: 0 10px 30px rgba(60,45,20,0.2), 0 4px 8px rgba(60,45,20,0.1);
}
/* ── Candycore easter egg theme ─────────────────────
Activated by typing "neon" outside a form field (tribute to artist Neon).
Palette sourced from snipe_v0_Neon_IPad_Paint.jpeg:
purple-black sky + lavender primary + cyan glow + yellow crown + pink text.
Stored as 'cf-candycore' in localStorage.
Applied: document.documentElement.dataset.candycore = 'active'
NOTE: Snipe Mode is declared last and overrides this when both are active.
*/
[data-candycore="active"] {
--app-primary: #c77dff;
--app-primary-hover: #a855f7;
--app-primary-light: rgba(199, 125, 255, 0.15);
/* Purple-black night sky */
--color-surface: #08051a;
--color-surface-2: #100d28;
--color-surface-raised: #1a1248;
/* Purple glow borders */
--color-border: rgba(199, 125, 255, 0.20);
--color-border-light: rgba(199, 125, 255, 0.10);
/* Candy-floss text — pink-white, muted bubblegum */
--color-text: #ffd6f5;
--color-text-muted: #f09099;
--color-text-inverse: #08051a;
/* Trust signals — straight from the painting */
--trust-high: #00c8e0; /* cyan (outline glow) = good */
--trust-mid: #ffe520; /* yellow (crown stripe) = caution */
--trust-low: #ff6eb4; /* hot pink = danger */
/* Semantic */
--color-success: #00c8e0;
--color-error: #ff6eb4;
--color-warning: #ffe520;
--color-info: #c77dff;
--color-accent: #00c8e0; /* cyan accent */
/* Purple glow shadows */
--shadow-sm: 0 1px 3px rgba(199, 125, 255, 0.12);
--shadow-md: 0 4px 12px rgba(199, 125, 255, 0.20);
--shadow-lg: 0 10px 30px rgba(199, 125, 255, 0.28);
/* Glow helpers (used in scoped styles if needed) */
--candy-glow-xs: rgba(199, 125, 255, 0.08);
--candy-glow-sm: rgba(199, 125, 255, 0.18);
--candy-glow-md: rgba(199, 125, 255, 0.45);
}
/* ── Snipe Mode easter egg theme ─────────────────── */
/* Activated by Konami code; stored as 'cf-snipe-mode' in localStorage */
/* Applied: document.documentElement.dataset.snipeMode = 'active' */
[data-snipe-mode="active"] {
--app-primary: #ff6b35;
--app-primary-hover: #ff4500;
--app-primary-light: rgba(255, 107, 53, 0.15);
--color-surface: #050505;
--color-surface-2: #0a0a0a;
--color-surface-raised: #0f0f0f;
--color-border: #ff6b3530;
--color-border-light: #ff6b3518;
--color-text: #ff9970;
--color-text-muted: #ff6b3580;
/* Glow variants for snipe mode UI */
--snipe-glow-xs: rgba(255, 107, 53, 0.08);
--snipe-glow-sm: rgba(255, 107, 53, 0.15);
--snipe-glow-md: rgba(255, 107, 53, 0.4);
--shadow-sm: 0 1px 3px rgba(255, 107, 53, 0.08);
--shadow-md: 0 4px 12px rgba(255, 107, 53, 0.12);
--shadow-lg: 0 10px 30px rgba(255, 107, 53, 0.18);
}
/* ── Base resets ─────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html {
font-family: var(--font-body);
color: var(--color-text);
background: var(--color-surface);
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body { margin: 0; min-height: 100vh; background: var(--color-surface); }
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
color: var(--app-primary);
line-height: 1.2;
margin: 0;
}
/* Focus visible — keyboard nav — accessibility requirement */
:focus-visible {
outline: 2px solid var(--app-primary);
outline-offset: 3px;
border-radius: var(--radius-sm);
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* ── Utility: screen reader only ────────────────── */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ── Steal shimmer animation ─────────────────────
Applied to ListingCard when listing qualifies as a steal:
composite_score >= 80 AND price < marketPrice * 0.8
The shimmer sweeps left-to-right across the card border.
*/
@keyframes steal-shimmer {
0% { background-position: -200% center; }
100% { background-position: 200% center; }
}
.steal-card {
border: 1.5px solid transparent;
background-clip: padding-box;
position: relative;
}
.steal-card::before {
content: '';
position: absolute;
inset: -1.5px;
border-radius: inherit;
background: linear-gradient(
90deg,
var(--trust-high) 0%,
#7ee787 40%,
var(--app-primary) 60%,
var(--trust-high) 100%
);
background-size: 200% auto;
animation: steal-shimmer 2.4s linear infinite;
z-index: -1;
}
/* ── Auction de-emphasis ─────────────────────────
Auctions with >1h remaining have fluid prices — de-emphasise
the card and current price to avoid anchoring on a misleading figure.
*/
.listing-card--auction {
opacity: 0.72;
border-color: var(--color-border-light);
}
.listing-card--auction:hover {
opacity: 1;
}
.auction-price--live {
opacity: 0.55;
font-style: italic;
}
.auction-badge {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 2px var(--space-2);
border-radius: var(--radius-full);
background: var(--color-warning);
color: var(--color-text-inverse);
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.04em;
}
.fixed-price-badge {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 2px var(--space-2);
border-radius: var(--radius-full);
background: var(--color-surface-raised);
color: var(--color-text-muted);
border: 1px solid var(--color-border);
font-size: 0.7rem;
font-weight: 600;
}