mirror of
https://github.com/anand34577/ferrum.git
synced 2026-09-24 11:26:19 +00:00
29f0c18dac
- Add glassmorphism, neumorphism, and brutalist looks (backend validation, theme.tsx types, AppearanceCard picker) alongside the existing six. - Give looks real structural knobs, not just color: sidebar width/border, header height/blur are now CSS vars AppShell reads, and card shadow/hover states are fully token-driven instead of a hardcoded dark-mode override. - Sidebar nav active state drops its hard box border on glass/neumorphic looks (glow pill / inset shadow) instead of always drawing a box. - Fix RankedBarChart's value label wrapping onto two lines on the longest bar (recharts auto-wraps LabelList when space gets tight) by rendering it as a plain unconstrained <text> with a wider right margin.
976 lines
34 KiB
CSS
976 lines
34 KiB
CSS
@import "tailwindcss";
|
|
|
|
/* The app toggles a .dark class on <html> (lib/theme.tsx) — point Tailwind's
|
|
dark: variant at the class instead of the OS media query so every dark:
|
|
utility follows the in-app theme toggle. */
|
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
|
|
/* Self-hosted (not CDN) so the UI renders correctly with zero network access. */
|
|
@font-face {
|
|
font-family: "Inter";
|
|
font-style: normal;
|
|
font-weight: 100 900;
|
|
font-display: swap;
|
|
src: url("/fonts/inter-variable.woff2") format("woff2");
|
|
}
|
|
@font-face {
|
|
font-family: "Space Grotesk";
|
|
font-style: normal;
|
|
font-weight: 300 700;
|
|
font-display: swap;
|
|
src: url("/fonts/space-grotesk-variable.woff2") format("woff2");
|
|
}
|
|
@font-face {
|
|
font-family: "JetBrains Mono";
|
|
font-style: normal;
|
|
font-weight: 100 800;
|
|
font-display: swap;
|
|
src: url("/fonts/jetbrains-mono-variable.woff2") format("woff2");
|
|
}
|
|
/* Used only by the Glass Flight Deck look's panel-label register — declared
|
|
here so it loads once regardless of which look is active. */
|
|
@font-face {
|
|
font-family: "Oswald";
|
|
font-style: normal;
|
|
font-weight: 200 700;
|
|
font-display: swap;
|
|
src: url("/fonts/oswald-variable.woff2") format("woff2");
|
|
}
|
|
|
|
@theme {
|
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
--font-display: "Space Grotesk", "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
--font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace;
|
|
|
|
/* Ferrum's default brand color is oxide — the color of weathered iron, and
|
|
the namesake reaction (ferrum + oxygen = rust). The ramp itself lives in
|
|
--accent-* below (a runtime token, like --elev-* for shadows) so a user's
|
|
accent preference can repaint every brand-* utility by swapping those
|
|
variables via [data-accent] — Tailwind v4 inlines @theme color values
|
|
directly into utility CSS, so referencing them here (rather than putting
|
|
hex literals straight in @theme) is what keeps them swappable at runtime. */
|
|
--color-brand-50: var(--accent-50);
|
|
--color-brand-100: var(--accent-100);
|
|
--color-brand-200: var(--accent-200);
|
|
--color-brand-300: var(--accent-300);
|
|
--color-brand-400: var(--accent-400);
|
|
--color-brand-500: var(--accent-500);
|
|
--color-brand-600: var(--accent-600);
|
|
--color-brand-700: var(--accent-700);
|
|
--color-brand-800: var(--accent-800);
|
|
|
|
/* Slightly tighter than the generic "bubbly SaaS" 8/12/16px scale —
|
|
reads more like a technical instrument panel than a consumer app. Every
|
|
[data-look] block below overrides this radius scale (and --elev-*,
|
|
--font-*, --bg/--border/--text) as a whole, so switching looks never
|
|
needs a component-level conditional. */
|
|
--radius-sm: 0.25rem;
|
|
--radius-md: 0.375rem;
|
|
--radius-lg: 0.5rem;
|
|
--radius-xl: 0.625rem;
|
|
|
|
/* Elevation utilities reference runtime tokens (defined in :root/.dark
|
|
below) so dark mode can swap to darker, tighter shadows — Tailwind v4
|
|
inlines literal values, so the indirection is what keeps them themable. */
|
|
--shadow-xs: var(--elev-xs);
|
|
--shadow-sm: var(--elev-sm);
|
|
--shadow-md: var(--elev-md);
|
|
--shadow-lg: var(--elev-lg);
|
|
--shadow-card: var(--elev-card);
|
|
}
|
|
|
|
/* ==========================================================================
|
|
* Enterprise SaaS — the default look. Restrained neutrals, one accent,
|
|
* Inter + Space Grotesk, moderate radius, soft shadows: the clean B2B
|
|
* dashboard register (Linear/Vercel/Stripe-adjacent). Every other look
|
|
* (see the "Look-and-feel presets" section further down) overrides this
|
|
* :root/.dark baseline — this block is never itself gated by [data-look],
|
|
* so it can never again silently inherit another look's leftovers.
|
|
* ========================================================================== */
|
|
:root {
|
|
color-scheme: light;
|
|
|
|
/* Cool steel neutrals, not the warm-cream default — this is a fleet of
|
|
iron and silicon, not a stationery brand. */
|
|
--bg: #eef0f3;
|
|
--bg-surface: #ffffff;
|
|
--bg-surface-hover: #f4f6f8;
|
|
--bg-elevated: #ffffff;
|
|
--bg-muted: #e4e7ec;
|
|
--border: #d7dbe2;
|
|
--border-strong: #c2c8d2;
|
|
--text: #14171c;
|
|
--text-muted: #5b6270;
|
|
--text-faint: #6e7480; /* 4.6:1 on white — WCAG AA for the 10-11px text that uses it */
|
|
--ring: var(--accent-600);
|
|
|
|
/* Default accent ramp — oxide, the color of weathered iron (see the brand
|
|
comment in @theme above). A user's accent preference is persisted server-
|
|
side (PUT /auth/me/preferences) and applied as [data-accent] on <html>;
|
|
the presets below override this ramp, and [data-accent] wins over :root
|
|
by specificity alone (equal ordering, one more selector). */
|
|
--accent-50: #fbeee6;
|
|
--accent-100: #f5dac6;
|
|
--accent-200: #eec0a0;
|
|
--accent-300: #e39e72;
|
|
--accent-400: #d97a49;
|
|
--accent-500: #bd5a2c;
|
|
--accent-600: #9c481f;
|
|
--accent-700: #7c3a19;
|
|
--accent-800: #5e2c13;
|
|
|
|
/* Status hues are deliberately spaced away from the brand oxide (~18°)
|
|
so "this needs attention" never gets mistaken for "this is Ferrum's
|
|
accent": ok is green (~150°), warn is gold (~42°), error is crimson
|
|
(~4°). */
|
|
--status-ok: #1f8a4c;
|
|
--status-warn: #8a6208; /* 5.1:1 on white */
|
|
--status-error: #c62828;
|
|
--status-info: #3667b6;
|
|
|
|
/* Chart/gauge/bar "remaining capacity" tracks. --bg-muted fails here in
|
|
dark mode: #20242a on the #1a1d22 surface reads as transparent, which
|
|
made unfilled donuts and bars look broken. This sits between border and
|
|
muted so it reads on every surface token in its theme. */
|
|
--track: #d5dae1;
|
|
|
|
/* Elevation. Kept as runtime tokens so dark mode can swap to darker,
|
|
tighter shadows (shadow color reads differently on near-black surfaces). */
|
|
--elev-xs: 0 1px 2px rgb(16 20 26 / 0.05);
|
|
--elev-sm: 0 1px 2px rgb(16 20 26 / 0.06), 0 1px 3px rgb(16 20 26 / 0.05);
|
|
--elev-md: 0 2px 4px rgb(16 20 26 / 0.05), 0 4px 12px rgb(16 20 26 / 0.08);
|
|
--elev-lg: 0 4px 8px rgb(16 20 26 / 0.06), 0 12px 32px rgb(16 20 26 / 0.12);
|
|
--elev-card: 0 1px 3px rgb(16 20 26 / 0.05);
|
|
/* Card hover state as its own token (not just shadow-md) so a look can
|
|
give hover a completely different treatment (glow, lift, none) instead
|
|
of only scaling the same shadow up. */
|
|
--card-shadow-hover: var(--elev-md);
|
|
|
|
/* A small categorical palette for charts with more than one series,
|
|
kept separate from the semantic status colors above. */
|
|
--chart-1: #bd5a2c;
|
|
--chart-2: #1f7a78;
|
|
--chart-3: #5b6cad;
|
|
--chart-4: #8a7226;
|
|
--chart-5: #a14f74;
|
|
--chart-6: #4f7a5a;
|
|
|
|
/* The sidebar is a fixed dark "control panel" regardless of the content
|
|
theme — like an instrument rail bolted onto whichever room it's in,
|
|
rather than a surface that repaints itself with the rest of the app. */
|
|
--sidebar-bg: #15171b;
|
|
--sidebar-border: #262930;
|
|
--sidebar-text: #e7e9ec;
|
|
--sidebar-text-muted: #8b919c;
|
|
--sidebar-active-bg: #21242b;
|
|
--sidebar-active-text: #ffffff;
|
|
|
|
/* Body-background texture, off by default (plain flat --bg) — only the
|
|
Glass Flight Deck look below turns this into a blueprint grid. */
|
|
--page-grid: none;
|
|
|
|
/* Structural knobs — AppShell reads these via inline style (not Tailwind
|
|
classes) so a look can genuinely change the chrome's proportions, not
|
|
just its colors. Values below match the pre-existing hardcoded classes
|
|
(w-60/w-16/h-14) so "enterprise" (the baseline) is pixel-identical to
|
|
before this existed. */
|
|
--sidebar-width: 15rem;
|
|
--sidebar-width-collapsed: 4rem;
|
|
--header-height: 3.5rem;
|
|
--sidebar-border-width: 0px;
|
|
--header-blur: blur(12px);
|
|
}
|
|
|
|
.dark {
|
|
color-scheme: dark;
|
|
|
|
--bg: #121417;
|
|
--bg-surface: #1a1d22;
|
|
--bg-surface-hover: #20242a;
|
|
--bg-elevated: #1f2329;
|
|
--bg-muted: #20242a;
|
|
--border: #2b3038;
|
|
--border-strong: #3a414c;
|
|
--text: #e7e9ec;
|
|
--text-muted: #969ea9;
|
|
--text-faint: #9aa3b0; /* 5.5:1 on the dark surface */
|
|
--ring: var(--accent-400);
|
|
|
|
--status-ok: #3ecb7d;
|
|
--status-warn: #dfab3c;
|
|
--status-error: #ef5b52;
|
|
--status-info: #6d9ee8;
|
|
|
|
--track: #3a414c;
|
|
|
|
--elev-xs: 0 1px 2px rgb(0 0 0 / 0.4);
|
|
--elev-sm: 0 1px 2px rgb(0 0 0 / 0.45), 0 1px 3px rgb(0 0 0 / 0.35);
|
|
--elev-md: 0 2px 4px rgb(0 0 0 / 0.4), 0 4px 12px rgb(0 0 0 / 0.45);
|
|
--elev-lg: 0 4px 8px rgb(0 0 0 / 0.4), 0 12px 32px rgb(0 0 0 / 0.55);
|
|
--elev-card: inset 0 1px 0 0 rgb(255 255 255 / 0.05), 0 1px 3px rgb(0 0 0 / 0.4);
|
|
--card-shadow-hover: inset 0 1px 0 0 rgb(255 255 255 / 0.08), 0 8px 24px rgb(0 0 0 / 0.6);
|
|
|
|
--chart-1: #d97a49;
|
|
--chart-2: #3fa19e;
|
|
--chart-3: #8b98d6;
|
|
--chart-4: #c2a544;
|
|
--chart-5: #c97ba0;
|
|
--chart-6: #7aa885;
|
|
}
|
|
|
|
/* Accent presets — same 9-step ramp shape as the default oxide, just hue-
|
|
shifted, so contrast and the light/dark split stay correct for every
|
|
choice. Applied via [data-accent] on <html> alongside the .dark class;
|
|
"oxide" needs no rule since it's already :root's default. Every look
|
|
above and below shares this same accent system. */
|
|
[data-accent="azure"] {
|
|
--accent-50: #eaf1fb;
|
|
--accent-100: #cfe0f6;
|
|
--accent-200: #a3c6ed;
|
|
--accent-300: #70a7e0;
|
|
--accent-400: #4a8ad2;
|
|
--accent-500: #3568b8;
|
|
--accent-600: #2a5595;
|
|
--accent-700: #234674;
|
|
--accent-800: #1c3759;
|
|
}
|
|
|
|
[data-accent="verdant"] {
|
|
--accent-50: #e8f6ef;
|
|
--accent-100: #c7ecdc;
|
|
--accent-200: #98dabd;
|
|
--accent-300: #63c299;
|
|
--accent-400: #3aa87c;
|
|
--accent-500: #268a64;
|
|
--accent-600: #1d6f51;
|
|
--accent-700: #185940;
|
|
--accent-800: #134632;
|
|
}
|
|
|
|
[data-accent="violet"] {
|
|
--accent-50: #f1edfb;
|
|
--accent-100: #ddd2f4;
|
|
--accent-200: #c0abe9;
|
|
--accent-300: #9d7ddb;
|
|
--accent-400: #8058c9;
|
|
--accent-500: #6a3fb0;
|
|
--accent-600: #54318f;
|
|
--accent-700: #432772;
|
|
--accent-800: #341e59;
|
|
}
|
|
|
|
[data-accent="slate"] {
|
|
--accent-50: #eef1f4;
|
|
--accent-100: #d9e0e7;
|
|
--accent-200: #bcc7d1;
|
|
--accent-300: #98a7b6;
|
|
--accent-400: #76889b;
|
|
--accent-500: #5c6e82;
|
|
--accent-600: #495868;
|
|
--accent-700: #3b4756;
|
|
--accent-800: #2e3844;
|
|
}
|
|
|
|
/* ==========================================================================
|
|
* Look-and-feel presets — a whole-app visual register, orthogonal to the
|
|
* light/dark toggle and the accent color. Applied as [data-look] on <html>
|
|
* (lib/theme.tsx). Every component reads --radius-*, --elev-*, --font-*,
|
|
* and the --bg/--border/--text ramp, so a look is a token repaint only —
|
|
* it never needs a component-level conditional, with two narrow, disclosed
|
|
* exceptions: MasterCautionBar's persistent-vs-reactive behavior, and the
|
|
* .panel-label / .corner-frame utilities' look-gated visual payload just
|
|
* below the nine blocks. "enterprise" needs no block: it's the :root/.dark
|
|
* baseline above, so it can never drift when a new look is added.
|
|
* ========================================================================== */
|
|
|
|
/* Proxmox-native — utilitarian and dense, closer to traditional sysadmin
|
|
tools (Proxmox's own UI, PRTG, pfSense): plain system font at a smaller
|
|
root size (Tailwind's rem-based scale shrinks with it, so density is
|
|
real, not just thinner borders), zero radius, no shadows — bordered grid
|
|
panels, not floating cards, with the card surface folded into the page
|
|
background so regions read as one dense sheet split by rules. */
|
|
[data-look="proxmox"] {
|
|
font-size: 92%;
|
|
--font-sans: ui-sans-serif, system-ui, "Segoe UI", Roboto, Arial, sans-serif;
|
|
--font-display: var(--font-sans);
|
|
--radius-sm: 0px;
|
|
--radius-md: 0px;
|
|
--radius-lg: 0px;
|
|
--radius-xl: 0px;
|
|
--elev-xs: none;
|
|
--elev-sm: none;
|
|
--elev-md: none;
|
|
--elev-lg: none;
|
|
--elev-card: none;
|
|
--page-grid: none;
|
|
--sidebar-border-width: 1px;
|
|
}
|
|
[data-look="proxmox"]:not(.dark) {
|
|
--bg: #dde1e6;
|
|
--bg-surface: #eef0f3;
|
|
--bg-surface-hover: #e4e8ec;
|
|
--bg-elevated: #ffffff;
|
|
--bg-muted: #d2d7dd;
|
|
--border: #aeb5bf;
|
|
--border-strong: #8a919d;
|
|
--text: #1a1d21;
|
|
}
|
|
[data-look="proxmox"].dark {
|
|
--bg: #101214;
|
|
--bg-surface: #17191c;
|
|
--bg-surface-hover: #1d2023;
|
|
--bg-elevated: #1d2023;
|
|
--bg-muted: #202327;
|
|
--border: #34393f;
|
|
--border-strong: #4a5058;
|
|
}
|
|
|
|
/* Terminal — still technical, but quiet: the *entire* UI (not just data)
|
|
sets in monospace, no display face at all, flat near-zero-radius panels,
|
|
no shadows. Precision over metaphor — the unmistakable signature is the
|
|
face itself, not caution stripes or condensed caps. */
|
|
[data-look="terminal"] {
|
|
--font-sans: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace;
|
|
--font-display: var(--font-sans);
|
|
--radius-sm: 0.125rem;
|
|
--radius-md: 0.125rem;
|
|
--radius-lg: 0.1875rem;
|
|
--radius-xl: 0.25rem;
|
|
--elev-xs: none;
|
|
--elev-sm: none;
|
|
--elev-md: none;
|
|
--elev-lg: none;
|
|
--elev-card: none;
|
|
--page-grid: none;
|
|
--sidebar-border-width: 1px;
|
|
--header-blur: none;
|
|
}
|
|
[data-look="terminal"]:not(.dark) {
|
|
--bg: #f3f4f6;
|
|
--bg-surface: #ffffff;
|
|
--bg-surface-hover: #ececf0;
|
|
--bg-elevated: #ffffff;
|
|
--bg-muted: #e7e8ec;
|
|
--border: #d4d7dc;
|
|
--border-strong: #b7bcc4;
|
|
}
|
|
[data-look="terminal"].dark {
|
|
--bg: #0a0d10;
|
|
--bg-surface: #12151b;
|
|
--bg-surface-hover: #171b22;
|
|
--bg-elevated: #161a22;
|
|
--bg-muted: #181c24;
|
|
--border: #202632;
|
|
--border-strong: #2e3644;
|
|
--status-info: #22c1dd;
|
|
}
|
|
|
|
/* Glass Flight Deck — an aviation cockpit/EICAS instrument panel: near-black
|
|
ground, a strict amber/red/green/cyan caution hierarchy, condensed Oswald
|
|
caps for every label, targeting-reticle corner brackets on instrument
|
|
tiles, and a persistent master-caution nameplate (see the JS-level
|
|
exception in MasterCautionBar.tsx). The most maximalist of the nine —
|
|
opt-in, not a leftover baseline. */
|
|
[data-look="glassFlightDeck"] {
|
|
--font-display: "Oswald", "Arial Narrow", sans-serif;
|
|
--radius-sm: 0.125rem;
|
|
--radius-md: 0.1875rem;
|
|
--radius-lg: 0.25rem;
|
|
--radius-xl: 0.375rem;
|
|
--elev-xs: none;
|
|
--elev-sm: none;
|
|
--elev-md: none;
|
|
--elev-lg: none;
|
|
--elev-card: none;
|
|
--page-grid:
|
|
linear-gradient(color-mix(in oklab, var(--border-strong) 30%, transparent) 1px, transparent 1px),
|
|
linear-gradient(90deg, color-mix(in oklab, var(--border-strong) 30%, transparent) 1px, transparent 1px),
|
|
linear-gradient(color-mix(in oklab, var(--border-strong) 45%, transparent) 1px, transparent 1px),
|
|
linear-gradient(90deg, color-mix(in oklab, var(--border-strong) 45%, transparent) 1px, transparent 1px);
|
|
}
|
|
[data-look="glassFlightDeck"]:not(.dark) {
|
|
--bg: #eef0f3;
|
|
--bg-surface: #ffffff;
|
|
--bg-elevated: #ffffff;
|
|
--status-ok: #15883f;
|
|
--status-warn: #b45f06;
|
|
--status-error: #c22525;
|
|
--status-info: #0e7f95;
|
|
}
|
|
[data-look="glassFlightDeck"].dark {
|
|
--bg: #090b0e;
|
|
--bg-surface: #12151b;
|
|
--bg-surface-hover: #181c24;
|
|
--bg-elevated: #161a22;
|
|
--bg-muted: #1a1e28;
|
|
--border: #202632;
|
|
--border-strong: #313a4b;
|
|
--text: #f1f5f9;
|
|
--text-muted: #94a3b8;
|
|
--status-ok: #2fbf62;
|
|
--status-warn: #f0a020;
|
|
--status-error: #ff4d4d;
|
|
--status-info: #22c1dd;
|
|
--caution-glow-warn: 0 0 10px rgb(240 160 32 / 0.35);
|
|
--caution-glow-error: 0 0 12px rgb(255 77 77 / 0.4);
|
|
--track: #1e2430;
|
|
--sidebar-bg: #090b0e;
|
|
--sidebar-border: #1a1e26;
|
|
}
|
|
|
|
/* Midnight — a modern dev-tool dark register (Nord/GitHub-dark-adjacent):
|
|
cool blue-slate ground, comfortable rounded surfaces, a soft glow instead
|
|
of a hard shadow. Meant to be lived in dark; the light variant below
|
|
keeps it coherent without pretending to be its own thing. */
|
|
[data-look="midnight"] {
|
|
--font-display: "Space Grotesk", "Inter", sans-serif;
|
|
--radius-sm: 0.3125rem;
|
|
--radius-md: 0.5rem;
|
|
--radius-lg: 0.75rem;
|
|
--radius-xl: 1rem;
|
|
}
|
|
[data-look="midnight"]:not(.dark) {
|
|
--bg: #eef1f6;
|
|
--bg-surface: #ffffff;
|
|
--bg-surface-hover: #f3f5f9;
|
|
--bg-elevated: #ffffff;
|
|
--bg-muted: #e6e9f0;
|
|
--border: #d7dce6;
|
|
--border-strong: #b9c1d0;
|
|
--elev-xs: 0 1px 2px rgb(30 41 70 / 0.05);
|
|
--elev-sm: 0 1px 2px rgb(30 41 70 / 0.06), 0 1px 3px rgb(30 41 70 / 0.06);
|
|
--elev-md: 0 2px 4px rgb(30 41 70 / 0.05), 0 6px 16px rgb(30 41 70 / 0.09);
|
|
--elev-lg: 0 4px 10px rgb(30 41 70 / 0.08), 0 14px 32px rgb(30 41 70 / 0.12);
|
|
--elev-card: 0 1px 3px rgb(30 41 70 / 0.06);
|
|
}
|
|
[data-look="midnight"].dark {
|
|
/* Distinctly blue-slate, not near-black-neutral like the other five dark
|
|
variants — the temperature shift is the point, so it has to read at a
|
|
glance, not just survive a color picker. */
|
|
--bg: #0a0e1a;
|
|
--bg-surface: #141b30;
|
|
--bg-surface-hover: #1a2340;
|
|
--bg-elevated: #1c2544;
|
|
--bg-muted: #171f38;
|
|
--border: #2c3860;
|
|
--border-strong: #3f4f80;
|
|
--text: #e8ecfb;
|
|
--text-muted: #93a0c4;
|
|
--text-faint: #93a0c4;
|
|
--status-ok: #4fd087;
|
|
--status-warn: #e8b048;
|
|
--status-error: #f16d68;
|
|
--status-info: #6fb2f5;
|
|
--track: #2c3860;
|
|
/* A visible cool glow, not a hairline — every card gets a faint blue
|
|
rim-light plus a soft ambient bloom underneath it. */
|
|
--elev-xs: 0 0 0 1px rgb(90 130 255 / 0.12), 0 1px 3px rgb(0 0 15 / 0.5);
|
|
--elev-sm: 0 0 0 1px rgb(90 130 255 / 0.14), 0 2px 12px rgb(50 90 255 / 0.16), 0 2px 8px rgb(0 0 15 / 0.5);
|
|
--elev-md: 0 0 0 1px rgb(90 130 255 / 0.16), 0 4px 24px rgb(50 90 255 / 0.2), 0 6px 20px rgb(0 0 15 / 0.55);
|
|
--elev-lg: 0 0 0 1px rgb(90 130 255 / 0.18), 0 8px 40px rgb(50 90 255 / 0.24), 0 16px 40px rgb(0 0 15 / 0.65);
|
|
--elev-card: 0 0 0 1px rgb(90 130 255 / 0.14), 0 2px 16px rgb(50 90 255 / 0.14), 0 2px 10px rgb(0 0 15 / 0.45);
|
|
}
|
|
|
|
/* Paper — ultra-minimal, warm and quiet (Notion/plain-docs-adjacent): warm
|
|
off-white ground, no display face at all, zero shadow, hairline borders
|
|
doing all the separating. Its dark variant stays warm (charcoal, not
|
|
blue-black) — every other dark look here leans cool, so Paper is the one
|
|
genuine warm-toned dark surface in the set. */
|
|
[data-look="paper"] {
|
|
--font-display: var(--font-sans);
|
|
--radius-sm: 0.1875rem;
|
|
--radius-md: 0.25rem;
|
|
--radius-lg: 0.375rem;
|
|
--radius-xl: 0.5rem;
|
|
--elev-xs: none;
|
|
--elev-sm: none;
|
|
--elev-md: none;
|
|
--elev-lg: none;
|
|
--elev-card: none;
|
|
}
|
|
[data-look="paper"]:not(.dark) {
|
|
--bg: #f6f4ef;
|
|
--bg-surface: #fffefb;
|
|
--bg-surface-hover: #f0ede5;
|
|
--bg-elevated: #fffefb;
|
|
--bg-muted: #ece8de;
|
|
--border: #e3ddce;
|
|
--border-strong: #cfc7b0;
|
|
--text: #221f19;
|
|
--text-muted: #6b6455;
|
|
--text-faint: #7a7364;
|
|
--track: #e3ddce;
|
|
}
|
|
[data-look="paper"].dark {
|
|
--bg: #18140f;
|
|
--bg-surface: #211c15;
|
|
--bg-surface-hover: #27221a;
|
|
--bg-elevated: #27221a;
|
|
--bg-muted: #241f18;
|
|
--border: #362f24;
|
|
--border-strong: #4a4131;
|
|
--text: #f1ece1;
|
|
--text-muted: #ac9f89;
|
|
--text-faint: #ac9f89;
|
|
--track: #362f24;
|
|
}
|
|
|
|
/* Glassmorphism — frosted, layered, airy: soft blurred color blobs behind
|
|
translucent panels (repurposing --page-grid, normally a hard blueprint
|
|
grid, as a gradient wash instead — same hook, opposite payload). Cards and
|
|
the sidebar itself pick up backdrop-filter blur via .card-surface /
|
|
.sidebar-shell, so "glass" isn't just a color, it visibly blurs whatever
|
|
scrolls underneath. */
|
|
[data-look="glassmorphism"] {
|
|
--font-display: "Space Grotesk", "Inter", sans-serif;
|
|
--radius-sm: 0.5rem;
|
|
--radius-md: 0.75rem;
|
|
--radius-lg: 1.125rem;
|
|
--radius-xl: 1.5rem;
|
|
--header-blur: blur(20px);
|
|
--sidebar-border-width: 1px;
|
|
}
|
|
[data-look="glassmorphism"]:not(.dark) {
|
|
--bg: #eef1fb;
|
|
--bg-surface: #ffffff;
|
|
--bg-surface-hover: #f4f6fd;
|
|
--bg-elevated: #ffffff;
|
|
--bg-muted: #e6eaf7;
|
|
--border: color-mix(in oklab, #ffffff 55%, #b9c2e6);
|
|
--border-strong: color-mix(in oklab, #ffffff 25%, #a6b0dc);
|
|
--sidebar-bg: color-mix(in oklab, #dfe4f8 55%, transparent);
|
|
--sidebar-border: color-mix(in oklab, #ffffff 50%, #b9c2e6);
|
|
--page-grid: radial-gradient(circle at 12% 8%, color-mix(in oklab, var(--color-brand-400) 30%, transparent), transparent 42%),
|
|
radial-gradient(circle at 88% 18%, color-mix(in oklab, #7aa2ff 32%, transparent), transparent 45%),
|
|
radial-gradient(circle at 30% 92%, color-mix(in oklab, #b98af0 28%, transparent), transparent 45%);
|
|
--elev-xs: 0 1px 2px rgb(60 70 120 / 0.06);
|
|
--elev-sm: 0 1px 3px rgb(60 70 120 / 0.08), 0 1px 2px rgb(60 70 120 / 0.06);
|
|
--elev-md: 0 8px 24px rgb(60 70 120 / 0.12);
|
|
--elev-lg: 0 16px 40px rgb(60 70 120 / 0.16);
|
|
--elev-card: 0 1px 0 0 rgb(255 255 255 / 0.6) inset, 0 8px 24px rgb(60 70 120 / 0.1);
|
|
--card-shadow-hover: 0 1px 0 0 rgb(255 255 255 / 0.7) inset, 0 16px 40px rgb(60 70 120 / 0.16);
|
|
}
|
|
[data-look="glassmorphism"].dark {
|
|
--bg: #0c0f1c;
|
|
--bg-surface: #141a2e;
|
|
--bg-surface-hover: #1a2138;
|
|
--bg-elevated: #171d33;
|
|
--bg-muted: #161c30;
|
|
--border: color-mix(in oklab, #6d7ecf 30%, transparent);
|
|
--border-strong: color-mix(in oklab, #6d7ecf 45%, transparent);
|
|
--text: #eef0fb;
|
|
--text-muted: #9aa3cc;
|
|
--text-faint: #9aa3cc;
|
|
--track: #262e4d;
|
|
--sidebar-bg: color-mix(in oklab, #171d33 60%, transparent);
|
|
--sidebar-border: color-mix(in oklab, #6d7ecf 25%, transparent);
|
|
--page-grid: radial-gradient(circle at 10% 10%, color-mix(in oklab, var(--color-brand-500) 22%, transparent), transparent 42%),
|
|
radial-gradient(circle at 90% 15%, color-mix(in oklab, #5468d6 26%, transparent), transparent 45%),
|
|
radial-gradient(circle at 25% 95%, color-mix(in oklab, #8f5fd6 20%, transparent), transparent 45%);
|
|
--elev-card: 0 1px 0 0 rgb(255 255 255 / 0.05) inset, 0 8px 28px rgb(0 0 10 / 0.55);
|
|
--card-shadow-hover: 0 1px 0 0 rgb(255 255 255 / 0.08) inset, 0 16px 44px rgb(0 0 10 / 0.65);
|
|
}
|
|
|
|
/* Neumorphism — soft, tactile, monochrome: surfaces are the *same* color as
|
|
the ground and separate purely through light/dark shadow pairs, never a
|
|
border. Cards, and the sidebar (via .card-surface / .sidebar-shell), drop
|
|
their border entirely — --border below matches --bg-surface so any
|
|
hairline that does render (inputs, dividers) stays nearly invisible
|
|
rather than fighting the shadow-only language. */
|
|
[data-look="neumorphism"] {
|
|
--font-display: "Inter", sans-serif;
|
|
--radius-sm: 0.625rem;
|
|
--radius-md: 0.875rem;
|
|
--radius-lg: 1.25rem;
|
|
--radius-xl: 1.75rem;
|
|
--page-grid: none;
|
|
}
|
|
[data-look="neumorphism"]:not(.dark) {
|
|
--bg: #e6e9ef;
|
|
--bg-surface: #e6e9ef;
|
|
--bg-surface-hover: #eaedf3;
|
|
--bg-elevated: #e6e9ef;
|
|
--bg-muted: #dde1e9;
|
|
--border: #e6e9ef;
|
|
--border-strong: #cfd4de;
|
|
--sidebar-bg: #e6e9ef;
|
|
--sidebar-border: #e6e9ef;
|
|
--sidebar-text: #33384a;
|
|
--sidebar-text-muted: #6b7284;
|
|
--sidebar-active-bg: #dde1e9;
|
|
--sidebar-active-text: #14171c;
|
|
--track: #d3d8e1;
|
|
--elev-xs: 2px 2px 5px rgb(163 172 196 / 0.35), -2px -2px 5px rgb(255 255 255 / 0.75);
|
|
--elev-sm: 3px 3px 8px rgb(163 172 196 / 0.4), -3px -3px 8px rgb(255 255 255 / 0.8);
|
|
--elev-md: 6px 6px 14px rgb(163 172 196 / 0.45), -6px -6px 14px rgb(255 255 255 / 0.85);
|
|
--elev-lg: 9px 9px 22px rgb(163 172 196 / 0.5), -9px -9px 22px rgb(255 255 255 / 0.9);
|
|
--elev-card: 6px 6px 14px rgb(163 172 196 / 0.4), -6px -6px 14px rgb(255 255 255 / 0.85);
|
|
--card-shadow-hover: 8px 8px 18px rgb(163 172 196 / 0.45), -8px -8px 18px rgb(255 255 255 / 0.9);
|
|
}
|
|
[data-look="neumorphism"].dark {
|
|
--bg: #262a33;
|
|
--bg-surface: #262a33;
|
|
--bg-surface-hover: #2b2f39;
|
|
--bg-elevated: #262a33;
|
|
--bg-muted: #21252d;
|
|
--border: #262a33;
|
|
--border-strong: #363c48;
|
|
--text: #e9ebf0;
|
|
--text-muted: #9297a3;
|
|
--text-faint: #9297a3;
|
|
--sidebar-bg: #262a33;
|
|
--sidebar-border: #262a33;
|
|
--sidebar-text: #e9ebf0;
|
|
--sidebar-text-muted: #9297a3;
|
|
--sidebar-active-bg: #2b2f39;
|
|
--sidebar-active-text: #ffffff;
|
|
--track: #1f232a;
|
|
--elev-xs: 2px 2px 5px rgb(0 0 0 / 0.45), -2px -2px 5px rgb(255 255 255 / 0.04);
|
|
--elev-sm: 3px 3px 8px rgb(0 0 0 / 0.5), -3px -3px 8px rgb(255 255 255 / 0.05);
|
|
--elev-md: 6px 6px 14px rgb(0 0 0 / 0.55), -6px -6px 14px rgb(255 255 255 / 0.05);
|
|
--elev-lg: 9px 9px 22px rgb(0 0 0 / 0.6), -9px -9px 22px rgb(255 255 255 / 0.06);
|
|
--elev-card: 6px 6px 14px rgb(0 0 0 / 0.5), -6px -6px 14px rgb(255 255 255 / 0.05);
|
|
--card-shadow-hover: 8px 8px 18px rgb(0 0 0 / 0.55), -8px -8px 18px rgb(255 255 255 / 0.06);
|
|
}
|
|
|
|
/* Brutalist — raw and high-contrast: pure black hairlines everywhere (thick,
|
|
not subtle), zero radius, hard offset shadows with no blur at all (a flat
|
|
drop, not a soft glow), no backdrop blur on the header. The one look
|
|
that's deliberately loud rather than quiet. */
|
|
[data-look="brutalist"] {
|
|
--font-display: "Space Grotesk", "Inter", sans-serif;
|
|
--radius-sm: 0px;
|
|
--radius-md: 0px;
|
|
--radius-lg: 0px;
|
|
--radius-xl: 0px;
|
|
--header-blur: none;
|
|
--sidebar-border-width: 3px;
|
|
--page-grid: none;
|
|
}
|
|
[data-look="brutalist"]:not(.dark) {
|
|
--bg: #f2f1ec;
|
|
--bg-surface: #ffffff;
|
|
--bg-surface-hover: #ececE4;
|
|
--bg-elevated: #ffffff;
|
|
--bg-muted: #e6e4da;
|
|
--border: #14171c;
|
|
--border-strong: #000000;
|
|
--text: #0a0a0a;
|
|
--text-muted: #33342f;
|
|
--text-faint: #46473f;
|
|
--track: #d8d6c9;
|
|
--status-ok: #0f7a2e;
|
|
--status-warn: #a85b00;
|
|
--status-error: #b3121f;
|
|
--status-info: #164a9c;
|
|
--sidebar-bg: #0a0a0a;
|
|
--sidebar-border: #000000;
|
|
--elev-xs: 2px 2px 0 0 #14171c;
|
|
--elev-sm: 3px 3px 0 0 #14171c;
|
|
--elev-md: 5px 5px 0 0 #14171c;
|
|
--elev-lg: 8px 8px 0 0 #14171c;
|
|
--elev-card: 4px 4px 0 0 #14171c;
|
|
--card-shadow-hover: 6px 6px 0 0 #14171c;
|
|
}
|
|
[data-look="brutalist"].dark {
|
|
--bg: #121212;
|
|
--bg-surface: #1a1a1a;
|
|
--bg-surface-hover: #212121;
|
|
--bg-elevated: #1a1a1a;
|
|
--bg-muted: #1f1f1f;
|
|
--border: #f2f1ec;
|
|
--border-strong: #ffffff;
|
|
--text: #f7f6f0;
|
|
--text-muted: #c9c8bf;
|
|
--text-faint: #b3b2a8;
|
|
--track: #2c2c2c;
|
|
--status-ok: #4fdc7e;
|
|
--status-warn: #ffb020;
|
|
--status-error: #ff4d4d;
|
|
--status-info: #5aa6ff;
|
|
--sidebar-bg: #000000;
|
|
--sidebar-border: #f2f1ec;
|
|
--elev-xs: 2px 2px 0 0 #f2f1ec;
|
|
--elev-sm: 3px 3px 0 0 #f2f1ec;
|
|
--elev-md: 5px 5px 0 0 #f2f1ec;
|
|
--elev-lg: 8px 8px 0 0 #f2f1ec;
|
|
--elev-card: 4px 4px 0 0 #f2f1ec;
|
|
--card-shadow-hover: 6px 6px 0 0 #f2f1ec;
|
|
}
|
|
|
|
/* Glassmorphism's actual "glass": cards and the sidebar are the only two
|
|
surfaces given translucency + blur — everywhere else (inputs, dropdowns,
|
|
dense tables) stays opaque on purpose, so frosted panels never fight
|
|
legibility. */
|
|
[data-look="glassmorphism"] .card-surface {
|
|
background: color-mix(in oklab, var(--bg-surface) 70%, transparent);
|
|
backdrop-filter: blur(16px);
|
|
-webkit-backdrop-filter: blur(16px);
|
|
}
|
|
[data-look="glassmorphism"] .sidebar-shell {
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
}
|
|
|
|
/* Brutalist's cards get a visibly thick hairline, not just a dark one — a
|
|
1px border reads as "restrained" no matter the color. */
|
|
[data-look="brutalist"] .card-surface {
|
|
border-width: 2px;
|
|
}
|
|
|
|
/* The active/hover sidebar nav pill: a hard 1px box border is Enterprise's
|
|
signature, not universal. Glass and neumorphic surfaces never draw boxes
|
|
around anything — they separate with translucency or shadow instead — so
|
|
both drop the border and pick up a look-native indicator. Brutalist goes
|
|
the other way and thickens it, since a box is exactly its idiom. */
|
|
[data-look="glassmorphism"] .nav-item-active {
|
|
border-color: transparent;
|
|
background: color-mix(in oklab, var(--color-brand-400) 22%, transparent);
|
|
backdrop-filter: blur(6px);
|
|
}
|
|
[data-look="neumorphism"] .nav-item-active {
|
|
border-color: transparent;
|
|
box-shadow: inset 2px 2px 5px rgb(0 0 0 / 0.35), inset -2px -2px 5px rgb(255 255 255 / 0.06);
|
|
}
|
|
[data-look="neumorphism"]:not(.dark) .nav-item-active {
|
|
box-shadow: inset 2px 2px 5px rgb(0 0 0 / 0.45), inset -2px -2px 5px rgb(255 255 255 / 0.08);
|
|
}
|
|
[data-look="brutalist"] .nav-item-active {
|
|
border-width: 2px;
|
|
}
|
|
|
|
* {
|
|
border-color: var(--border);
|
|
}
|
|
|
|
html,
|
|
body,
|
|
#root {
|
|
height: 100%;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
background-color: var(--bg);
|
|
background-image: var(--page-grid);
|
|
background-size:
|
|
8px 8px,
|
|
8px 8px,
|
|
32px 32px,
|
|
32px 32px;
|
|
background-attachment: fixed;
|
|
color: var(--text);
|
|
font-family: var(--font-sans);
|
|
font-feature-settings: "cv05" 1; /* Inter: open numerals pair well with mono metrics */
|
|
-webkit-font-smoothing: antialiased;
|
|
text-rendering: optimizeLegibility;
|
|
}
|
|
|
|
::selection {
|
|
background: var(--color-brand-500);
|
|
color: white;
|
|
}
|
|
|
|
/* Keyboard focus is always visible, always the same shape: a 2px outline
|
|
with a hairline offset so it reads on both light and dark surfaces.
|
|
Components with their own focus-visible rings override this freely — the
|
|
outline respects the element's real border-radius (no re-shaping).
|
|
|
|
Must live in @layer base: Tailwind v4 emits utilities inside cascade
|
|
layers, and unlayered CSS beats every layer — unlayered, this rule won
|
|
over `outline-none` and stacked a square outline on top of components'
|
|
rounded focus rings. */
|
|
@layer base {
|
|
:focus-visible {
|
|
outline: 2px solid var(--ring);
|
|
outline-offset: 2px;
|
|
}
|
|
}
|
|
|
|
/* thin, unobtrusive scrollbars for the dense inventory/log panes */
|
|
* {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--border-strong) transparent;
|
|
}
|
|
::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--border-strong);
|
|
border-radius: 9999px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: var(--text-muted);
|
|
}
|
|
|
|
/* React Flow chrome (Topology page) — the stock Controls/MiniMap/selection
|
|
styles hardcode light-theme whites and near-black strokes, which read
|
|
wrong in dark mode (and vice versa). Re-point them at the app tokens so
|
|
both themes follow the toggle.
|
|
|
|
The Controls' background/color/border are themed by defining xyflow's
|
|
own `--xy-controls-button-*` prop variables (the values its stylesheet
|
|
consumes) rather than overriding its rules: @xyflow's style.css is
|
|
bundled AFTER this file (it's imported inside TopologyPage), so plain
|
|
overrides lose the cascade — but CSS variables resolve regardless of
|
|
order. xyflow's dark defaults only activate under `.react-flow.dark`,
|
|
which never matches this app (we toggle `.dark` on <html>), so without
|
|
these the buttons stay white-on-white in dark mode. */
|
|
:root {
|
|
--xy-controls-button-background-color: var(--bg-elevated);
|
|
--xy-controls-button-background-color-hover: var(--bg-surface-hover);
|
|
--xy-controls-button-color: var(--text-muted);
|
|
--xy-controls-button-color-hover: var(--text);
|
|
--xy-controls-button-border-color: var(--border);
|
|
}
|
|
|
|
.dark {
|
|
--xy-controls-button-background-color: var(--bg-elevated);
|
|
--xy-controls-button-background-color-hover: var(--bg-surface-hover);
|
|
--xy-controls-button-color: var(--text-muted);
|
|
--xy-controls-button-color-hover: var(--text);
|
|
--xy-controls-button-border-color: var(--border);
|
|
}
|
|
|
|
.react-flow__controls {
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
box-shadow: var(--elev-sm);
|
|
}
|
|
|
|
.react-flow__controls-button svg {
|
|
fill: currentColor;
|
|
}
|
|
|
|
.react-flow__minimap {
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.react-flow__node.selected,
|
|
.react-flow__node:focus-visible {
|
|
box-shadow: 0 0 0 2px var(--ring);
|
|
}
|
|
|
|
/* Metric digits align in columns — every number in tables, stats and charts
|
|
should sit inside this utility. */
|
|
@utility tabular {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* The panel-label register: every page title, nav label, badge, tab, and
|
|
panel heading picks up the active look's display face and a little extra
|
|
weight. Tracked ALL-CAPS is Glass Flight Deck's signature specifically
|
|
(an engraved instrument placard), not a universal default — every other
|
|
look renders this in plain mixed case, so switching away from Glass
|
|
Flight Deck can never leave a shouty-caps residue behind. */
|
|
@utility panel-label {
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
}
|
|
[data-look="glassFlightDeck"] .panel-label {
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* Targeting-reticle corner brackets — Glass Flight Deck's one recurring
|
|
signature frame (instrument tiles, the page nameplate). The utility only
|
|
reserves the positioning; every other look leaves the pseudo-elements
|
|
borderless, so it costs nothing and paints nothing outside that look. */
|
|
@utility corner-frame {
|
|
position: relative;
|
|
&::before,
|
|
&::after {
|
|
content: "";
|
|
position: absolute;
|
|
width: 9px;
|
|
height: 9px;
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
[data-look="glassFlightDeck"] .corner-frame::before {
|
|
top: -1px;
|
|
left: -1px;
|
|
border-top: 2px solid var(--border-strong);
|
|
border-left: 2px solid var(--border-strong);
|
|
}
|
|
[data-look="glassFlightDeck"] .corner-frame::after {
|
|
bottom: -1px;
|
|
right: -1px;
|
|
border-bottom: 2px solid var(--border-strong);
|
|
border-right: 2px solid var(--border-strong);
|
|
}
|
|
|
|
/* The master-caution bar (AppShell, Glass Flight Deck only — see the
|
|
look-aware branch in MasterCautionBar.tsx) — quiet and collapsed when
|
|
nothing needs attention, a banded amber/red strip when it does. Bands run
|
|
at a shallow angle like hazard tape, not a flat fill, so the caution
|
|
state reads as a physical strip rather than a colored toast. */
|
|
@utility caution-band {
|
|
background-image: repeating-linear-gradient(
|
|
-45deg,
|
|
color-mix(in oklab, currentColor 16%, transparent) 0 10px,
|
|
transparent 10px 20px
|
|
);
|
|
}
|
|
|
|
/* Recharts portals its tooltip into the chart wrapper as an absolutely
|
|
positioned box that overflows the wrapper near the plot edges. Inside a
|
|
scroll container (widget bodies, the page) that overflow extends the
|
|
scrollable area on hover — a scrollbar flashes in and the chart visibly
|
|
resizes. Clipping at the wrapper bounds stops that; the tooltip placement
|
|
math already flips it to stay inside the plot area, so content is cut
|
|
only in the degenerate "tooltip bigger than the chart" case. */
|
|
.recharts-wrapper {
|
|
overflow: clip;
|
|
}
|
|
|
|
/* Hides the scrollbar while keeping the element scrollable (wheel, touch,
|
|
keyboard) — used on the collapsed sidebar rail so the scrollbar can't
|
|
push the centered icons off the rail's midline. */
|
|
@utility no-scrollbar {
|
|
scrollbar-width: none;
|
|
&::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% {
|
|
transform: translateX(-100%);
|
|
}
|
|
100% {
|
|
transform: translateX(100%);
|
|
}
|
|
}
|
|
|
|
@utility animate-shimmer {
|
|
animation: shimmer 1.8s infinite linear;
|
|
}
|
|
|
|
@utility glass {
|
|
background-color: color-mix(in oklab, var(--bg-surface) 80%, transparent);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
}
|
|
|
|
/* Respect the OS "reduce motion" setting: kill decorative movement but keep
|
|
opacity fades (they don't cause vestibular issues and state changes still
|
|
need to be perceivable). */
|
|
@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;
|
|
}
|
|
}
|