vestige/apps/dashboard/src/app.css

575 lines
17 KiB
CSS
Raw Normal View History

@import 'tailwindcss';
@theme {
/* Vestige cosmic dark palette */
--color-void: #050510;
--color-abyss: #0a0a1a;
--color-deep: #10102a;
--color-surface: #161638;
--color-elevated: #1e1e4a;
--color-subtle: #2a2a5e;
--color-muted: #4a4a7a;
--color-dim: #7a7aaa;
--color-text: #e0e0ff;
--color-bright: #ffffff;
/* Accent colors */
--color-synapse: #6366f1;
--color-synapse-glow: #818cf8;
--color-dream: #a855f7;
--color-dream-glow: #c084fc;
--color-memory: #3b82f6;
--color-recall: #10b981;
--color-decay: #ef4444;
--color-warning: #f59e0b;
/* Node type colors */
--color-node-fact: #3b82f6;
--color-node-concept: #8b5cf6;
--color-node-event: #f59e0b;
--color-node-person: #10b981;
--color-node-place: #06b6d4;
--color-node-note: #6b7280;
--color-node-pattern: #ec4899;
--color-node-decision: #ef4444;
--font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', monospace;
}
/* Base styles */
html {
background: var(--color-void);
color: var(--color-text);
font-family: var(--font-mono);
}
/*
OKLCH / DISPLAY-P3 ACCENT PALETTE (PROGRESSIVE ENHANCEMENT)
The @theme block above keeps the original sRGB hex values, which
Tailwind reads at build time and which serve as the fallback for
sRGB monitors and browsers without OKLCH support.
When the browser understands oklch(), we redefine the SAME vivid
accents + node-type colors using their OKLCH equivalents. These are
faithful conversions of the hex values (same hue/chroma identity);
on a wide-gamut display-p3 monitor they render more saturated while
reading as the same color. The void/abyss/surface neutrals are left
untouched only the vivid accents benefit from the wider gamut. */
@supports (color: oklch(0 0 0)) {
:root {
/* Accent colors */
--color-synapse: oklch(0.585 0.222 277);
--color-synapse-glow: oklch(0.685 0.169 277);
--color-dream: oklch(0.627 0.265 304);
--color-dream-glow: oklch(0.714 0.203 305);
--color-memory: oklch(0.623 0.214 259);
--color-recall: oklch(0.696 0.17 162);
--color-decay: oklch(0.637 0.237 25);
--color-warning: oklch(0.769 0.188 70);
/* Node type colors */
--color-node-fact: oklch(0.623 0.214 259);
--color-node-concept: oklch(0.606 0.25 292);
--color-node-event: oklch(0.769 0.188 70);
--color-node-person: oklch(0.696 0.17 162);
--color-node-place: oklch(0.715 0.143 215);
--color-node-note: oklch(0.551 0.027 264);
--color-node-pattern: oklch(0.656 0.241 354);
--color-node-decision: oklch(0.637 0.237 25);
}
}
body {
margin: 0;
min-height: 100vh;
overflow: hidden;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-subtle);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-muted);
}
/*
GLASSMORPHISM SYSTEM
*/
.glass {
background: rgba(22, 22, 56, 0.45);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(99, 102, 241, 0.08);
box-shadow:
inset 0 1px 0 0 rgba(255, 255, 255, 0.03),
0 4px 24px rgba(0, 0, 0, 0.3);
}
.glass-subtle {
background: rgba(16, 16, 42, 0.4);
backdrop-filter: blur(12px) saturate(150%);
-webkit-backdrop-filter: blur(12px) saturate(150%);
border: 1px solid rgba(99, 102, 241, 0.06);
box-shadow:
inset 0 1px 0 0 rgba(255, 255, 255, 0.02),
0 2px 12px rgba(0, 0, 0, 0.2);
}
.glass-sidebar {
background: rgba(10, 10, 26, 0.6);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border-right: 1px solid rgba(99, 102, 241, 0.1);
box-shadow:
inset -1px 0 0 0 rgba(255, 255, 255, 0.02),
4px 0 24px rgba(0, 0, 0, 0.3);
}
.glass-panel {
background: rgba(10, 10, 26, 0.8);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border: 1px solid rgba(99, 102, 241, 0.1);
box-shadow:
inset 0 1px 0 0 rgba(255, 255, 255, 0.03),
0 8px 32px rgba(0, 0, 0, 0.4);
}
/*
GLOW EFFECTS
*/
.glow-synapse {
box-shadow: 0 0 20px rgba(99, 102, 241, 0.3), 0 0 60px rgba(99, 102, 241, 0.1);
}
.glow-dream {
box-shadow: 0 0 20px rgba(168, 85, 247, 0.3), 0 0 60px rgba(168, 85, 247, 0.1);
}
.glow-memory {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3), 0 0 60px rgba(59, 130, 246, 0.1);
}
/*
ANIMATIONS
*/
/* Pulse animation for live indicators */
@keyframes pulse-glow {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
/* Ambient background orbs */
@keyframes orb-float-1 {
0%, 100% { transform: translate(0, 0) scale(1); }
25% { transform: translate(60px, -40px) scale(1.1); }
50% { transform: translate(-30px, -80px) scale(0.95); }
75% { transform: translate(-60px, -20px) scale(1.05); }
}
@keyframes orb-float-2 {
0%, 100% { transform: translate(0, 0) scale(1); }
25% { transform: translate(-50px, 30px) scale(1.08); }
50% { transform: translate(40px, 60px) scale(0.92); }
75% { transform: translate(20px, -40px) scale(1.03); }
}
@keyframes orb-float-3 {
0%, 100% { transform: translate(0, 0) scale(1); }
25% { transform: translate(30px, 50px) scale(1.05); }
50% { transform: translate(-60px, 20px) scale(0.98); }
75% { transform: translate(40px, -30px) scale(1.1); }
}
.ambient-orb {
position: fixed;
border-radius: 50%;
filter: blur(80px);
pointer-events: none;
z-index: 0;
opacity: 0.35;
}
.ambient-orb-1 {
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(168, 85, 247, 0.4), transparent 70%);
top: -10%;
right: -5%;
animation: orb-float-1 20s ease-in-out infinite;
}
.ambient-orb-2 {
width: 350px;
height: 350px;
background: radial-gradient(circle, rgba(99, 102, 241, 0.35), transparent 70%);
bottom: -15%;
left: -5%;
animation: orb-float-2 25s ease-in-out infinite;
}
.ambient-orb-3 {
width: 300px;
height: 300px;
background: radial-gradient(circle, rgba(245, 158, 11, 0.2), transparent 70%);
top: 40%;
left: 40%;
animation: orb-float-3 22s ease-in-out infinite;
}
/* Active nav indicator with animated gradient border */
.nav-active-border {
position: relative;
}
.nav-active-border::before {
content: '';
position: absolute;
left: 0;
top: 4px;
bottom: 4px;
width: 2px;
border-radius: 1px;
background: linear-gradient(
180deg,
var(--color-synapse),
var(--color-dream),
var(--color-synapse)
);
background-size: 100% 200%;
animation: gradient-shift 3s ease-in-out infinite;
}
@keyframes gradient-shift {
0%, 100% { background-position: 0% 0%; }
50% { background-position: 0% 100%; }
}
/* Neural particle animation */
@keyframes float {
0%, 100% { transform: translateY(0) translateX(0); }
25% { transform: translateY(-10px) translateX(5px); }
50% { transform: translateY(-5px) translateX(-5px); }
75% { transform: translateY(-15px) translateX(3px); }
}
/* Retention bar colors */
.retention-critical { color: var(--color-decay); }
.retention-low { color: var(--color-warning); }
.retention-good { color: var(--color-recall); }
.retention-strong { color: var(--color-synapse); }
/*
VIEW TRANSITIONS (CROSSFADE)
Native View Transitions API crossfade between routes. Pairs with the
onNavigate hook in +layout.svelte that calls document.startViewTransition.
Reduced-motion users get an instant cut (the @media guard disables the
animation entirely, so the default browser cross-fade does not run). */
@media not (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 180ms;
animation-timing-function: ease;
}
::view-transition-old(root) {
animation-name: vt-fade-out;
}
::view-transition-new(root) {
animation-name: vt-fade-in;
}
@keyframes vt-fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes vt-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
}
feat(dashboard): alive overhaul — unique icons, dropdowns, motion on every page Make the dashboard feel alive every second, with clear controls, for the July 14 HN relaunch. Memory Cinema is left fully untouched (zero changes to MemoryCinema.svelte / graph/cinema/*; its tests still pass). Foundation (lifts every page): - Icon.svelte: inline-SVG icon system, zero runtime dep. A UNIQUE semantic silhouette per nav item — kills the old duplicated Unicode glyphs (◎◈◉◷ were each reused across multiple items). Wired into sidebar, mobile nav, command palette, logo. - Dropdown.svelte: accessible, keyboard-nav, type-ahead, animated select replacement with color dots / badges. Replaces dead native <select>s. - AnimatedNumber.svelte: rAF count-up/tween, reduced-motion safe. - PageHeader.svelte: shared masthead (drawn route icon + aurora title). - actions/reveal.ts + actions/interactions.ts: scroll-reveal, magnetic, tilt(+glare), spotlight — all no-op under reduced-motion. - app.css "alive layer": @property animatable gradients, conic live-border, breathe/ping, shimmer skeletons, @starting-style entry, aurora text, lift. Per-page: every route (graph non-cinema controls, reasoning, memories, timeline, feed, explore, activation, dreams, schedule, importance, duplicates, contradictions, patterns, intentions, stats, settings) now uses PageHeader, real Icons, count-ups, staggered reveals, shimmer loaders, spotlight cards, and warm empty states. Native selects and button-row filters became clear Dropdowns where it improves clarity. Gates: svelte-check 0 errors/0 warnings, 937/937 tests pass, build green, verified live in the browser preview. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:58:26 -05:00
/*
THE "ALIVE" LAYER (bleeding-edge, progressively enhanced)
Shared primitives that make every page breathe, respond, and feel seen.
Everything here degrades gracefully and is disabled under reduced-motion.
*/
/* Registered animatable custom props (@property)
Registering a property as <angle>/<percentage> lets the browser TWEEN it,
which plain CSS vars can't do. Powers the rotating conic borders below. */
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
@property --shine {
syntax: '<percentage>';
initial-value: 0%;
inherits: false;
}
/* Scroll-reveal (paired with use:reveal)
Elements start translated + transparent; .reveal-in lands them. The
action toggles the class via IntersectionObserver, with per-item --reveal-delay
for staggered list entrances. */
.reveal {
opacity: 0;
transform: translateY(var(--reveal-y, 16px));
transition:
opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
transition-delay: var(--reveal-delay, 0ms);
will-change: opacity, transform;
}
.reveal-in {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
transform: none;
transition: none;
}
}
/* @starting-style page/section entry
Native entry animation with zero JS: the element animates FROM the
starting-style the first time it's rendered. Used on route content. */
@media not (prefers-reduced-motion: reduce) {
.enter {
transition:
opacity 0.4s ease,
transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
@starting-style {
.enter {
opacity: 0;
transform: translateY(10px);
}
}
}
/* Conic-gradient animated border (premium "live" frame)
A slowly rotating iridescent ring around a panel. The @property --angle
makes the conic gradient's start angle tweenable. Use class .live-border. */
.live-border {
position: relative;
isolation: isolate;
}
.live-border::before {
content: '';
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 1px;
background: conic-gradient(
from var(--angle),
transparent 0%,
var(--color-synapse) 18%,
var(--color-dream) 33%,
transparent 50%,
transparent 100%
);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
opacity: 0.6;
z-index: -1;
}
@media not (prefers-reduced-motion: reduce) {
.live-border::before {
animation: border-rotate 6s linear infinite;
}
@keyframes border-rotate {
to {
--angle: 360deg;
}
}
}
/* Cursor spotlight surface (paired with use:spotlight)
A soft radial glow follows the pointer across a panel. --spot-x/y/o are
set by the action; default opacity 0 so it's invisible until hovered. */
.spotlight-surface {
position: relative;
overflow: hidden;
}
.spotlight-surface::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(
340px circle at var(--spot-x, 50%) var(--spot-y, 50%),
rgba(129, 140, 248, 0.12),
transparent 60%
);
opacity: var(--spot-o, 0);
transition: opacity 0.3s ease;
z-index: 0;
}
/* ── Tilt glare (paired with use:tilt glare) ────────────────────────────── */
.tilt-glare {
position: relative;
overflow: hidden;
}
.tilt-glare::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(
circle at var(--glare-x, 50%) var(--glare-y, 50%),
rgba(255, 255, 255, 0.14),
transparent 45%
);
opacity: var(--glare-o, 0);
transition: opacity 0.3s ease;
}
/* Breathing live indicator (calmer than a hard blink)
A dot that gently inhales/exhales scale + glow reads as "alive,
listening" rather than "error blinking". */
.breathe {
animation: breathe 3.2s ease-in-out infinite;
}
@keyframes breathe {
0%, 100% {
transform: scale(1);
filter: drop-shadow(0 0 2px currentColor);
opacity: 0.85;
}
50% {
transform: scale(1.18);
filter: drop-shadow(0 0 7px currentColor);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.breathe { animation: none; }
}
/* A live "radar ping" ring that expands and fades — wrap a dot in .ping-host. */
.ping-host {
position: relative;
}
.ping-host::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: currentColor;
opacity: 0.6;
z-index: -1;
}
@media not (prefers-reduced-motion: reduce) {
.ping-host::before {
animation: ping 2.4s cubic-bezier(0, 0, 0.2, 1) infinite;
}
@keyframes ping {
0% { transform: scale(1); opacity: 0.5; }
80%, 100% { transform: scale(2.6); opacity: 0; }
}
}
/* Shimmer skeleton (loading that feels intentional, not frozen)
A diagonal light sweep over skeleton blocks. Use .shimmer on the
placeholder; it replaces a plain pulse with a directional sheen. */
.shimmer {
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.03);
}
.shimmer::after {
content: '';
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(
90deg,
transparent,
rgba(129, 140, 248, 0.12),
transparent
);
}
@media not (prefers-reduced-motion: reduce) {
.shimmer::after {
animation: shimmer 1.6s ease-in-out infinite;
}
@keyframes shimmer {
100% { transform: translateX(100%); }
}
}
/* ── Gradient text that slowly drifts (alive headings) ──────────────────── */
.text-aurora {
background: linear-gradient(
100deg,
var(--color-synapse-glow),
var(--color-dream-glow),
var(--color-recall),
var(--color-synapse-glow)
);
background-size: 250% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
@media not (prefers-reduced-motion: reduce) {
.text-aurora {
animation: aurora-drift 8s ease-in-out infinite;
}
@keyframes aurora-drift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
}
/* ── Hover lift utility — cards rise + glow when pointed at ───────────────*/
.lift {
transition:
transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.28s ease,
border-color 0.28s ease;
}
.lift:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(99, 102, 241, 0.18);
}
@media (prefers-reduced-motion: reduce) {
.lift:hover { transform: none; }
}
/* ── Tabular numerals helper (count-ups don't jitter) ──────────────────── */
.tabular-nums {
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum';
}
/* ── Focus ring consistency (accessible + on-brand) ─────────────────────── */
:where(button, a, input, select, [role='button'], [tabindex]):focus-visible {
outline: 2px solid var(--color-synapse-glow);
outline-offset: 2px;
border-radius: 0.4rem;
}