Ran the official @tailwindcss/upgrade tool: - tailwind.config.ts → @theme directive in globals.css - @tailwind base/components/utilities → @import 'tailwindcss' - postcss.config switched from tailwindcss + autoprefixer to @tailwindcss/postcss (autoprefixer baked in) - focus-visible:outline-none → focus-visible:outline-hidden (the v3 utility was a footgun — outline still showed in forced-colors mode) Reverted the migration tool's over-zealous variant="outline" → variant="outline-solid" rename on CVA prop values; that rename was meant for the Tailwind `outline:` utility, not our Button/Badge component variants. Swapped tailwindcss-animate (v3-style JS plugin) for tw-animate-css (v4-native @import). Same utility surface (animate-spin, animate-in, etc.), one fewer JS plugin in the bundle. Fixed the upgrade tool's malformed dark variant (@custom-variant dark (&:is(class *)) — `class` was being parsed as a tag) to canonical &:where(.dark, .dark *). Verified: tsc 0 errors, eslint 0 errors (16 pre-existing warnings), vitest 1315/1315, next build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
449 lines
14 KiB
CSS
449 lines
14 KiB
CSS
@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;
|
|
}
|