435 lines
16 KiB
CSS
435 lines
16 KiB
CSS
@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;
|
|
}
|
|
}
|
|
} |