@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&display=swap'); /* Astryx component styles, pre-compiled by the package (`@layer astryx-base`). Only the component layer is imported: Tailwind's preflight is already the app's reset, and Astryx's own reset would be a second, global one. The theme package is likewise left out — Astryx components in KROW wear KROW's tokens, which the `.krow-astryx` scope below supplies. */ @import '@astryxdesign/core/astryx.css'; @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { /* ── KROW ambient theme ───────────────────────────────────────────── The named tokens the theme is specified in, kept as literal hex so the source values are readable at a glance. Everything below them is the same palette expressed as HSL triplets, which is the form Tailwind consumes — so components keep using `bg-surface`, `text-ink-1`, `border-border` and pick the theme up without a single component being edited. Two vocabularies, one palette. Nothing in the app should reach for a raw hex; these exist so the theme has a stated source, not so components can bypass the semantic tokens. */ --krow-bg: #F8F8F8; --krow-bg-blue: #E8F0F8; --krow-bg-warm: #F8F8E8; --krow-primary: #0838E0; --krow-text: #101820; --krow-text-secondary: #687386; --krow-text-muted: #8B94A5; --krow-border: #E0E4EA; --krow-border-strong: #D8DDE5; --krow-success: #16A765; --krow-warning: #E7B900; --krow-error: #D64545; /* KROW Official Brand Colors */ /* Primary: KROW Blue #0838E0 */ /* Accent: KROW Yellow #F9E547 (Pantone 106CP) */ /* Pale Yellow: #F8E08E (Pantone 1205CP) */ /* Dark Navy: #333F48 (Pantone 432CP) */ /* Mint: #D1E0D7 (Pantone 621CP) */ --background: 0 0% 97%; /* #F8F8F8 — the neutral base */ --foreground: 210 33% 9%; /* #101820 */ --card: 0 0% 100%; --card-foreground: 210 33% 9%; --popover: 0 0% 100%; --popover-foreground: 210 33% 9%; --primary: 227 93% 45%; /* #0838E0 */ --primary-foreground: 0 0% 100%; --secondary: 227 89% 96%; --secondary-foreground: 227 89% 30%; --muted: 210 16% 95%; --muted-foreground: 218 13% 47%; /* #687386 */ --accent: 227 89% 94%; --accent-foreground: 227 89% 25%; --destructive: 0 64% 55%; /* #D64545 */ --destructive-foreground: 0 0% 100%; --border: 216 19% 90%; /* #E0E4EA */ --input: 215 21% 89%; /* #DDE2E9 */ --ring: 227 93% 45%; --chart-1: 227 93% 45%; --chart-2: 52 91% 63%; --chart-3: 210 17% 24%; --chart-4: 42 73% 74%; --chart-5: 150 16% 84%; --radius: 0.75rem; --font-heading: 'Sora', ui-sans-serif, system-ui, sans-serif; --font-body: 'Inter', ui-sans-serif, system-ui, sans-serif; --font-display: 'Sora', ui-sans-serif, system-ui, sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --sidebar-background: 0 0% 98%; --sidebar-foreground: 240 5.3% 26.1%; --sidebar-primary: 240 5.9% 10%; --sidebar-primary-foreground: 0 0% 98%; --sidebar-accent: 240 4.8% 95.9%; --sidebar-accent-foreground: 240 5.9% 10%; --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; /* ── Design system tokens ─────────────────────────────────────────── Everything below is consumed through Tailwind (see tailwind.config.js) rather than referenced as raw CSS in components. */ /* Semantic status — used by StatusBadge, Alert, KpiCard trends */ --success: 153 77% 37%; /* #16A765 */ --success-foreground: 0 0% 100%; --success-muted: 146 50% 95%; /* #EAF8F0 */ --warning: 48 100% 40%; /* #E7B900, darkened for text contrast */ --warning-foreground: 0 0% 100%; --warning-muted: 48 90% 94%; --info: 227 93% 45%; --info-foreground: 0 0% 100%; --info-muted: 227 89% 96%; --neutral: 210 33% 9%; --neutral-foreground: 0 0% 100%; --neutral-muted: 216 14% 94%; --destructive-muted: 0 70% 97%; /* Ink scale — the text hierarchy every component uses */ --ink-1: 210 33% 9%; /* #101820 — headings */ --ink-2: 216 17% 30%; /* body, one step above secondary */ --ink-3: 218 13% 47%; /* #687386 — secondary */ --ink-4: 219 13% 60%; /* #8B94A5 — placeholder / meta */ /* Surfaces. Cards stay opaque white by default: the ambient tint is supplied by the canvas behind them, and making every card translucent would cost readability for a difference nobody can name. The `.surface-translucent` utility below exists for the few places a surface genuinely wants the tint to read through it. */ --surface: 0 0% 100%; --surface-subtle: 210 20% 98%; --surface-sunken: 216 16% 96%; /* Elevation — a 4-step scale, tinted with brand blue not neutral black */ /* Softened for the ambient canvas: with a tinted background providing the separation, a card only needs to lift off it — not cast a shadow that announces itself. `sm` is the reference's own `0 2px 10px / 0.04`. */ --shadow-xs: 0 1px 2px 0 rgb(16 24 32 / 0.03); --shadow-sm: 0 2px 10px 0 rgb(16 24 32 / 0.04); --shadow-md: 0 4px 16px -2px rgb(16 24 32 / 0.06), 0 2px 6px -3px rgb(16 24 32 / 0.04); --shadow-lg: 0 12px 28px -6px rgb(16 24 40 / 0.12), 0 4px 10px -4px rgb(16 24 40 / 0.06); --shadow-xl: 0 24px 56px -12px rgb(16 24 40 / 0.18), 0 8px 20px -8px rgb(16 24 40 / 0.08); --shadow-brand: 0 10px 30px -6px rgb(10 57 223 / 0.24), 0 2px 6px -2px rgb(51 63 72 / 0.08); --shadow-panel: 0 24px 64px -12px rgb(9 12 24 / 0.45), 0 8px 24px -8px rgb(9 12 24 / 0.30); /* Motion — one easing vocabulary across the app */ --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); --duration-fast: 120ms; --duration-base: 200ms; --duration-slow: 320ms; /* Layout — the single container width every page shares */ --container-max: 80rem; /* 1280px — the dashboard's own measure */ --container-wide: 108rem; /* 1728px — the shell, sized to fit the 380px assistant column beside a full-width dashboard */ --container-admin: 102rem; /* 1632px — the Admin console. Fills the viewport up to a cap rather than leaving wide gutters. */ --page-gutter: 1.5rem; /* Owliver assistant panel — the one dark surface in the system */ --panel: 222 47% 9%; --panel-raised: 222 40% 14%; --panel-border: 222 30% 22%; --panel-foreground: 210 20% 96%; --panel-muted: 217 15% 62%; } .dark { --background: 0 0% 3.9%; --foreground: 0 0% 98%; --card: 0 0% 3.9%; --card-foreground: 0 0% 98%; --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%; --primary: 0 0% 98%; --primary-foreground: 0 0% 9%; --secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%; --muted: 0 0% 14.9%; --muted-foreground: 0 0% 63.9%; --accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; --border: 0 0% 14.9%; --input: 0 0% 14.9%; --ring: 0 0% 83.1%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; --sidebar-background: 240 5.9% 10%; --sidebar-foreground: 240 4.8% 95.9%; --sidebar-primary: 224.3 76.3% 48%; --sidebar-primary-foreground: 0 0% 100%; --sidebar-accent: 240 3.7% 15.9%; --sidebar-accent-foreground: 240 4.8% 95.9%; --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; } } @layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-foreground font-body; } /** * The ambient canvas. * * Cool blue on the left, neutral through the middle, the faintest warm cream * on the right — the atmosphere the whole product sits in. Three decisions * worth stating: * * A fixed pseudo-element rather than a background on `body`. The gradient runs * left to right, so it must be sized to the *viewport*, not to the document — * on a long page a body background stretches and the tint stops landing where * it was drawn. Fixed also means it paints once and never repaints on scroll. * * `z-index: -1` puts it behind everything without entering the layout at all, * so no shell had to grow a wrapper to hold it. * * The stops are close together in lightness on purpose. The reference reads as * "softer", not as "there is a gradient" — at this spread the eye registers * warmth and coolness without resolving a band. */ body::before { content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none; background: linear-gradient( 90deg, var(--krow-bg-blue) 0%, #F1F4F7 28%, #F5F6F7 45%, var(--krow-bg) 65%, #F8F8F1 86%, var(--krow-bg-warm) 100% ); } /* The dark palette is configured (`darkMode: 'class'`) but nothing in the app applies the class today. This keeps the canvas from being a trap the day something does: a fixed light gradient behind a dark theme would wash out every page at once. */ .dark body::before { background: hsl(var(--background)); } } @layer utilities { /** * A surface that lets the ambient canvas tint read through it. * * Opt-in rather than the default for cards: at 0.82 the tint is present * without body text losing contrast, but applied everywhere it turns the page * into glassmorphism, which is the thing the theme is explicitly not. */ .surface-translucent { background-color: rgb(255 255 255 / 0.82); } .glass { background: rgba(255, 255, 255, 0.72); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%); } .glass-card { background: rgba(255, 255, 255, 0.65); backdrop-filter: blur(20px) saturate(160%); -webkit-backdrop-filter: blur(20px) saturate(160%); } .gradient-mesh { background-image: radial-gradient(at 15% 15%, rgba(10, 57, 223, 0.07) 0px, transparent 50%), radial-gradient(at 85% 5%, rgba(249, 229, 71, 0.12) 0px, transparent 50%), radial-gradient(at 75% 85%, rgba(10, 57, 223, 0.05) 0px, transparent 50%), radial-gradient(at 10% 90%, rgba(209, 224, 215, 0.20) 0px, transparent 50%); } .gradient-text { color: #0838E0; } .glow-blue { box-shadow: 0 0 40px rgba(10, 57, 223, 0.15); } .glow-yellow { box-shadow: 0 0 40px rgba(249, 229, 71, 0.30); } .no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } /* The dark Owliver panel surface. Kept as a utility so the one AI component can never drift from the token. */ .panel-surface { background: linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0) 40%), hsl(var(--panel)); backdrop-filter: blur(24px) saturate(140%); -webkit-backdrop-filter: blur(24px) saturate(140%); } /* Scrollbar for dark surfaces — the default is invisible against the panel. */ .panel-scroll::-webkit-scrollbar { width: 6px; } .panel-scroll::-webkit-scrollbar-track { background: transparent; } .panel-scroll::-webkit-scrollbar-thumb { background: hsl(var(--panel-border)); border-radius: 9999px; } .panel-scroll::-webkit-scrollbar-thumb:hover { background: hsl(var(--panel-muted)); } /* Standard focus treatment. Applied by DS components so keyboard focus is identical everywhere and never relies on the browser default. */ .focus-ring { @apply outline-none ring-offset-2 ring-offset-background focus-visible:ring-2 focus-visible:ring-[#0838E0]/60; } .focus-ring-inset { @apply outline-none focus-visible:ring-2 focus-visible:ring-[#0838E0]/60; } /* Skeleton shimmer — a moving highlight reads as loading far better than a pulsing block, especially on the glass surfaces. */ .shimmer { background-image: linear-gradient( 90deg, transparent 0%, rgba(255, 255, 255, 0.65) 50%, transparent 100% ); background-size: 200% 100%; animation: shimmer 1.6s var(--ease-in-out) infinite; } /* Text truncation past one line — Tailwind has line-clamp, this keeps the DS API consistent with `truncate`. */ .truncate-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } } /** * Astryx components, wearing KROW. * * Astryx reads its own CSS custom properties, so dressing it in the KROW * language is a matter of re-pointing those tokens at the ones the rest of the * app already uses — not of overriding component rules. Scoped to `.krow-astryx` * so nothing outside an Astryx island is touched, and unlayered so it wins over * the package's `@layer astryx-base` defaults. * * Only the tokens Tab and TabList actually read are declared. Anything else * keeps the package default, which is the point of restating as little as * possible. */ .krow-astryx { /* The selected indicator rule. KROW blue is the active state. */ --color-accent: #0838E0; /* Tab labels: selected picks up brand, the rest the app's secondary ink. */ --color-text-primary: #0838E0; --color-text-secondary: hsl(var(--ink-3)); --color-border: hsl(var(--border)); /* The hover wash sits *over* the label, so it has to stay translucent — an opaque surface token here paints the tab's own text out. */ --color-overlay-hover: rgba(10, 57, 223, 0.06); --color-overlay-pressed: rgba(10, 57, 223, 0.11); --font-family-body: var(--font-body); } .dark .krow-astryx { --color-accent: #5B8DEF; --color-text-primary: #93B4FF; --color-overlay-hover: rgba(147, 180, 255, 0.10); --color-overlay-pressed: rgba(147, 180, 255, 0.16); } /** * Give Astryx's own button styling back. * * Astryx ships its component CSS inside `@layer astryx-base`, while Tailwind * emits its preflight unlayered — and unlayered always beats a layer, whatever * the selectors. So Tailwind's `button { font-size: 100%; padding: 0; color: * inherit }` reset silently flattened every Astryx tab: no size, no padding, no * label colour, leaving five words run together under a stray indicator. * * `revert-layer` rolls each property back to the previous layer's value, which * is Astryx's own. Nothing is re-specified here — the design system keeps * authorship of its sizing, and this only stops the reset from reaching it. */ /* Same story for `hasDivider`: preflight's `*{border-width:0}` zeroed the rail under the strip, so the tabs floated over the content with nothing separating them from it. */ .krow-astryx .astryx-tab-list { border-bottom-width: revert-layer; } .krow-astryx .astryx-tab { font-family: revert-layer; font-size: revert-layer; font-weight: revert-layer; line-height: revert-layer; color: revert-layer; padding-inline: revert-layer; } /** * Tab strips scroll rather than wrap. * * A tab row that wraps to a second line stops reading as one control, and on a * phone there is never room for five section names. The strip takes the * overflow so the page itself never gains a horizontal scrollbar. */ .krow-astryx-scroll { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; } .krow-astryx-scroll::-webkit-scrollbar { display: none; } .krow-astryx-scroll > nav { flex-wrap: nowrap; min-width: max-content; } @layer base { /* Respect reduced-motion globally rather than per component. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } }