@import 'tailwindcss'; @import 'tw-animate-css'; @custom-variant dark (&:where(.dark, .dark *)); @theme { --color-border: hsl(var(--border)); --color-input: hsl(var(--input)); --color-ring: hsl(var(--ring)); --color-background: hsl(var(--background)); --color-foreground: hsl(var(--foreground)); --color-primary: hsl(var(--primary)); --color-primary-foreground: hsl(var(--primary-foreground)); --color-secondary: hsl(var(--secondary)); --color-secondary-foreground: hsl(var(--secondary-foreground)); --color-destructive: hsl(var(--destructive)); --color-destructive-foreground: hsl(var(--destructive-foreground)); --color-muted: hsl(var(--muted)); --color-muted-foreground: hsl(var(--muted-foreground)); --color-accent: hsl(var(--accent)); --color-accent-foreground: hsl(var(--accent-foreground)); --color-popover: hsl(var(--popover)); --color-popover-foreground: hsl(var(--popover-foreground)); --color-card: hsl(var(--card)); --color-card-foreground: hsl(var(--card-foreground)); --color-brand-50: #d8e5f4; --color-brand-100: #b1cbe9; --color-brand-200: #89b0de; --color-brand-300: #6196d3; --color-brand-400: #3a7bc8; --color-brand-500: #2f6ab5; --color-brand-600: #255a9e; --color-brand-700: #1c4a87; --color-brand: #3a7bc8; --color-brand-dark: #1e2844; --color-navy-50: #cdcfd6; --color-navy-100: #9ea1af; --color-navy-200: #71768a; --color-navy-300: #474e66; --color-navy-400: #1e2844; --color-navy-500: #171f35; --color-navy-600: #101625; --color-navy: #1e2844; --color-sage: #dae3c1; --color-sage-light: #edf1e2; --color-sage-dark: #b8c49e; --color-mint: #add5b3; --color-mint-light: #d6ead9; --color-mint-dark: #7dba85; --color-teal: #83aab1; --color-teal-light: #b1cdd2; --color-teal-dark: #5a8a92; --color-purple: #685aa3; --color-purple-light: #a49ac6; --color-purple-dark: #4d4280; --color-success: #2d8a4e; --color-success-bg: #e8f5e9; --color-success-border: #a5d6a7; --color-warning: #e6a817; --color-warning-bg: #fff8e1; --color-warning-border: #ffe082; --color-error: #d32f2f; --color-error-bg: #ffebee; --color-error-border: #ef9a9a; --color-sidebar: #1e2844; --color-sidebar-text: #cdcfd6; --color-sidebar-hover: #171f35; --color-sidebar-active: #3a7bc8; --color-sidebar-divider: #474e66; --font-sans: Inter, system-ui, -apple-system, Arial, sans-serif; --font-mono: JetBrains Mono, ui-monospace, monospace; --font-serif: Georgia, Times New Roman, serif; --shadow-xs: 0 1px 2px 0 rgb(15 23 42 / 0.04); --shadow-sm: 0 2px 4px -1px rgb(15 23 42 / 0.06); --shadow: 0 1px 3px rgba(30, 40, 68, 0.1), 0 1px 2px rgba(30, 40, 68, 0.06); --shadow-md: 0 4px 12px -2px rgb(15 23 42 / 0.08); --shadow-lg: 0 12px 32px -8px rgb(15 23 42 / 0.12); --shadow-xl: 0 20px 25px rgba(30, 40, 68, 0.1), 0 8px 10px rgba(30, 40, 68, 0.04); --shadow-glow: 0 0 0 4px rgb(58 123 200 / 0.12); --radius-sm: 0.375rem; --radius: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.625rem; --radius-xl: 0.875rem; --background-image-gradient-brand: linear-gradient(135deg, #3a7bc8 0%, #2f6ab5 100%); --background-image-gradient-brand-soft: linear-gradient(135deg, #d8e5f4 0%, #ffffff 100%); --background-image-gradient-success: linear-gradient(135deg, #e8f5e9 0%, #ffffff 100%); --background-image-gradient-warning: linear-gradient(135deg, #fef3c7 0%, #ffffff 100%); --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1); --width-sidebar: 256px; --width-sidebar-collapsed: 64px; --transition-duration-sidebar: 200ms; --transition-duration-fast: 150ms; --transition-duration-base: 200ms; --transition-duration-slow: 300ms; --spacing-safe: env(safe-area-inset-bottom); --spacing-safe-top: env(safe-area-inset-top); --spacing-safe-bottom: env(safe-area-inset-bottom); --spacing-safe-left: env(safe-area-inset-left); --spacing-safe-right: env(safe-area-inset-right); --animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; --animate-badge-pop: badge-pop 0.32s cubic-bezier(0.34, 1.56, 0.64, 1); @keyframes accordion-down { from { height: 0; } to { height: var(--radix-accordion-content-height); } } @keyframes accordion-up { from { height: var(--radix-accordion-content-height); } to { height: 0; } } @keyframes badge-pop { 0% { transform: scale(0.5); opacity: 0; } 60% { transform: scale(1.18); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } } /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentcolor); } } @layer base { :root { /* shadcn/ui variable format: H S% L% */ --background: 0 0% 100%; /* #ffffff */ --foreground: 224 39% 19%; /* #1e2844 */ --card: 0 0% 100%; --card-foreground: 224 39% 19%; --popover: 0 0% 100%; --popover-foreground: 224 39% 19%; --primary: 213 55% 56%; /* #3a7bc8 */ --primary-foreground: 0 0% 100%; --secondary: 224 39% 19%; /* #1e2844 */ --secondary-foreground: 0 0% 100%; --muted: 210 11% 96%; /* #f1f3f5 */ --muted-foreground: 228 10% 49%; /* #71768a */ --accent: 213 60% 95%; /* #eef3fb — soft brand-blue tint for hover/focus */ --accent-foreground: 224 39% 19%; /* dark navy text for contrast on light bg */ --destructive: 0 65% 51%; /* #d32f2f */ --destructive-foreground: 0 0% 100%; --border: 227 10% 82%; /* #cdcfd6 */ --input: 227 10% 82%; --ring: 213 55% 56%; /* #3a7bc8 focus ring */ --radius: 0.375rem; /* Sidebar (using dark navy) */ --sidebar-background: 224 39% 19%; --sidebar-foreground: 227 10% 82%; --sidebar-primary: 213 55% 56%; --sidebar-primary-foreground: 0 0% 100%; --sidebar-accent: 224 39% 15%; --sidebar-accent-foreground: 227 10% 82%; --sidebar-border: 226 18% 34%; --sidebar-ring: 213 55% 56%; /* Chart colors for Recharts */ --chart-1: 213 55% 56%; /* Brand blue */ --chart-2: 224 39% 19%; /* Dark navy */ --chart-3: 190 18% 60%; /* Teal */ --chart-4: 254 29% 50%; /* Purple */ --chart-5: 130 30% 76%; /* Mint */ --chart-6: 75 30% 82%; /* Sage */ } .dark { --background: 224 40% 12%; --foreground: 227 10% 91%; --card: 224 39% 19%; --card-foreground: 227 10% 91%; --popover: 224 39% 19%; --popover-foreground: 227 10% 91%; --primary: 213 52% 62%; --primary-foreground: 0 0% 100%; --secondary: 224 39% 22%; --secondary-foreground: 227 10% 82%; --muted: 224 39% 18%; --muted-foreground: 228 10% 49%; --accent: 224 39% 24%; /* subtle elevation above card for hover/focus */ --accent-foreground: 227 10% 91%; /* light text on dark accent */ --destructive: 0 72% 63%; --destructive-foreground: 0 0% 100%; --border: 224 35% 28%; --input: 224 35% 28%; --ring: 213 52% 62%; /* Sidebar stays dark navy in both modes */ --sidebar-background: 224 40% 10%; --sidebar-foreground: 227 10% 82%; --sidebar-primary: 213 52% 62%; --sidebar-primary-foreground: 0 0% 100%; --sidebar-accent: 224 40% 14%; --sidebar-accent-foreground: 227 10% 82%; --sidebar-border: 226 18% 28%; --sidebar-ring: 213 52% 62%; /* Chart colors (brightened for dark mode) */ --chart-1: 213 52% 62%; --chart-2: 227 10% 82%; --chart-3: 190 18% 55%; --chart-4: 254 29% 55%; --chart-5: 130 30% 70%; --chart-6: 75 30% 78%; } * { @apply border-border; } body { @apply bg-background text-foreground font-sans antialiased; /* Suppress iOS Safari's default black tap-highlight overlay so our * explicit `active:bg-accent` styles are the only press effect. * Without this, every tap on a mobile button/link flashes a muddy * dark rectangle on top of whatever active style we set. */ -webkit-tap-highlight-color: transparent; } /* Wave watermark - subtle background texture for auth pages */ .wave-watermark { background-image: repeating-linear-gradient( 135deg, transparent, transparent 10px, rgba(58, 123, 200, 0.03) 10px, rgba(58, 123, 200, 0.03) 20px ); } /* * No global focus ring. shadcn components opt in individually * (Button uses `focus-visible:ring-1`, DropdownMenuItem uses * `focus:bg-accent`, etc.) — that gives us a quiet, per-component * indicator without the chunky `ring-2 + ring-offset-2` artifact * the global rule was creating on every rounded element. * * Components that need a custom focus indicator (e.g. the global * search bar's wrapper-border swap) provide their own. Bare * focusable elements without explicit styles fall back to the * browser's native focus indicator, which keeps keyboard navigation * accessible without painting blue rings everywhere. */ /* Scrollbar styling */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { @apply bg-transparent; } ::-webkit-scrollbar-thumb { @apply bg-border rounded-full; } ::-webkit-scrollbar-thumb:hover { @apply bg-muted-foreground/30; } } /* ─── Form-factor shell visibility ────────────────────────────────────────── * Two shells (desktop + mobile) render to the DOM on every page; CSS hides * the inactive one. The data-form-factor body attribute is set server-side * from User-Agent (see src/lib/form-factor.ts). The media-query fallback * handles desktop browsers resized below lg (1024px), or stripped UAs. * * IMPORTANT: only `display: none` rules are emitted - we never set a positive * display, because the desktop shell uses Tailwind's `flex` class which would * be overridden by `display: block` (same specificity, later cascade). */ [data-shell='mobile'] { display: none; } @media (max-width: 1023.98px) { [data-shell='desktop'] { display: none; } [data-shell='mobile'] { display: block; } } body[data-form-factor='mobile'] [data-shell='desktop'] { display: none; } body[data-form-factor='mobile'] [data-shell='mobile'] { display: block; } /* * React Query Devtools floating button collides with the bottom tab bar's * "More" tab on mobile. The devtools panel itself remains accessible from * desktop where the toggle is positioned out of the way of any UI. */ @media (max-width: 1023.98px) { .tsqd-open-btn-container, .tsqd-parent-container { display: none !important; } } /* * Recharts focus-ring suppression. * * Recharts SVG surfaces become keyboard-focusable when a user clicks into * them (the library adds tabindex on chart sectors / paths). The global * `*:focus-visible` rule above paints a 4px brand-blue box-shadow ring, * which on a chart surface reads as a stray rectangle around the plot * area. Hover/tooltip already handles chart interactivity, so suppress * the ring entirely here. * * Lives OUTSIDE `@layer base` so Tailwind's PostCSS pipeline can't drop * it during purge (an earlier copy inside `@layer base` was being * silently removed at build time, leaving the ring intact). */ div.recharts-wrapper:focus, div.recharts-wrapper:focus-visible, svg.recharts-surface:focus, svg.recharts-surface:focus-visible, div.recharts-responsive-container:focus, div.recharts-responsive-container:focus-visible, .recharts-wrapper *:focus, .recharts-wrapper *:focus-visible { outline: none !important; box-shadow: none !important; --tw-ring-shadow: 0 0 #0000 !important; --tw-ring-offset-shadow: 0 0 #0000 !important; --tw-ring-color: transparent !important; --tw-ring-offset-color: transparent !important; } /* * Vaul drawer (bottom-direction) animation timing override. * * Vaul's defaults feel slightly snappy when the drawer is full-screen * (mobile search overlay) — the snap-on / snap-off reads as janky at * scale. We slow it down and use a softer easing curve (ease-out-quint) * which decelerates smoothly without the elastic kick. * * Scoped to mobile drawers via the data-vaul-drawer-direction attr so * the smaller MoreSheet drawer keeps its punchier default. * * The overlay's opacity transition is matched to the same duration so * the backdrop and drawer stay in sync. */ [data-vaul-drawer][data-vaul-drawer-direction='bottom'] { transition: transform 480ms cubic-bezier(0.22, 1, 0.36, 1) !important; } [data-vaul-drawer][data-vaul-drawer-direction='bottom'][data-state='closed'] { transition: transform 380ms cubic-bezier(0.4, 0, 0.2, 1) !important; } [data-vaul-overlay] { transition: opacity 480ms cubic-bezier(0.22, 1, 0.36, 1) !important; } [data-vaul-overlay][data-state='closed'] { transition: opacity 380ms cubic-bezier(0.4, 0, 0.2, 1) !important; } /* * GPU compositing hints for Vaul drawers. * * `will-change: transform` tells the browser to promote the drawer to * its own composite layer ahead of the animation, so the swipe-drag * transforms run on the GPU instead of triggering re-paints. Without * this, Safari sometimes defers layer creation until the first frame * of the drag, producing the visible "jump" reps were seeing when * flicking the drawer down to close. * * `backface-visibility: hidden` keeps the layer flat and prevents * sub-pixel jitter during the spring-physics close animation. * * `contain: layout style paint` isolates the drawer's render tree * from the rest of the document — repaints inside the drawer (e.g. * focus-state changes on a button) don't invalidate the parent. */ [data-vaul-drawer] { will-change: transform; backface-visibility: hidden; contain: layout style paint; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } [data-vaul-overlay] { will-change: opacity; backface-visibility: hidden; }