feat: initial — fork skyrise for finance product

- Bakes in --warning / --warning-foreground (light + dark).
- Adds --text-hero size step (3.5rem) above display, for KPI heroes.
- Ships a .num utility (tabular-nums + slashed-zero + zero tracking).
- Redesigned dark-mode background: corner glows pushed to L≈0.30 with
  lower chroma over a clean near-black base — reads as soft brand-accent
  light instead of skyrise's muddy gamut-fringe blobs.
- Drops 11 background atmospheres + 4 surface tints — focused product theme.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-18 22:02:38 +10:00
commit 8ba09b5db3
4 changed files with 709 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
name: validate theme
on:
push:
branches: [main]
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Fetch validator + reference
run: |
mkdir -p .ci-scripts
curl -fsSL -o .ci-scripts/check-theme.mjs \
https://git.sky-ai.com/CremaUIStudio/core-interface/raw/branch/main/scripts/check-theme.mjs
git clone --depth 1 \
https://git.sky-ai.com/CremaUIStudio/lib-theme-pristine.git \
.ci-scripts/lib-theme-pristine
- name: Validate against pristine contract
run: |
node .ci-scripts/check-theme.mjs \
--target=theme.css \
--reference=.ci-scripts/lib-theme-pristine/theme.css \
--name=skyai-finance

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.ci-scripts/
.DS_Store

69
README.md Normal file
View File

@@ -0,0 +1,69 @@
# lib-theme-skyai-finance
Finance-focused premium AI glass theme. Forked from [`lib-theme-skyrise`](../lib-theme-skyrise/) with four product-driven changes:
1. **Bakes in `--warning` + `--warning-foreground`** — finance KPIs need warning semantics out of the box. Pristine had `--success` but no warning; skyrise had both. This theme guarantees them in light and dark.
2. **Adds a `--text-hero` size step** (`3.5rem`) above `display`, for KPI hero numbers. Exposed as `text-hero` once the consuming app maps it into Tailwind's `@theme inline`.
3. **Ships a `.num` utility** that sets `font-feature-settings: "tnum","ss01","cv01"`, `font-variant-numeric: tabular-nums`, and `letter-spacing: 0`. Apply to any element that displays money or counts — never inherit body's negative tracking on digits, never tolerate the ambiguous round-zero glyph in a finance context.
4. **Redesigned dark-mode background.** Skyrise's dark blobs sat at `L≈0.18` with chroma `≈0.12`, which clipped to muddy gray near OKLCH gamut edges. Here the four corner glows are pushed to `L≈0.30` with lower chroma (`0.060.09`), paired with a cleaner near-black base. Reads as soft brand-accent light rather than tinted fog.
Also **dropped**: the 11 background atmospheres and 4 surface tints skyrise shipped for white-label flexibility. A focused product picks one brand surface and ships it. Re-import them from skyrise if needs change.
## Install
Sibling-clone like the other Crema themes:
```
your-workspace/
skyai-finance/
lib-theme-skyai-finance/ ← this dir
lib-action-bus/
...
```
In the consuming app's `app/app.css`, swap the active theme import:
```css
@import "../../lib-theme-skyai-finance/theme.css"; /* CREMA:THEME */
```
Then load the fonts at the very top of `app.css` (Google Fonts URL):
```css
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=Instrument+Sans:wght@400..700&family=Geist+Mono:wght@400..600&display=swap");
```
And expose the hero size step + `--font-heading` to Tailwind in your `@theme inline` block:
```css
@theme inline {
--font-heading: "Instrument Sans", "Inter", sans-serif;
--text-hero: var(--text-hero-size);
--text-hero--line-height: var(--text-hero-lh);
/* … plus the usual --color-* mappings the lib expects */
}
```
## What's in the box
- Iridescent lavender→peach→cyan→mint light surface (the brand)
- Clean deep-violet dark surface with four soft corner glows
- Glass surfaces (blur + saturate + brightness stack) on every shadcn `[data-slot]`
- Apple-style spring physics motion tokens
- Premium primary button gradient + wet-sheen highlight
- AI orb avatar (rainbow conic) — opt in via `<AvatarFallback data-ai>`
- Heading face: **Instrument Sans 600 / -0.02em**
- Body face: **Inter** 17px (1.0625rem), `letter-spacing: -0.011em`
- Mono: **Geist Mono**
- `.num` utility — slashed zero, tabular nums, zero tracking
- `--text-hero` / `--text-display` / `--text-headline` / `--text-title` / `--text-body` / `--text-label` / `--text-caption`
- Full semantic colour set: primary, secondary, accent, muted, destructive, **success, warning** + foregrounds
- 5-step `--elevation-{1..5}` shadow scale (primary-tinted at e4/e5)
## What's not
- Background atmosphere variants (`pearl`, `linen`, `mist`, …) — strip from `DisplayMenu` too.
- Surface tints (`snow`, `stone`, `sage`, `slate`) — same.
- Multiple syntax themes.
If you need either, fork back to `lib-theme-skyrise`.

607
theme.css Normal file
View File

@@ -0,0 +1,607 @@
/*
* skyai-finance — finance-focused premium AI glass theme.
* Forked from skyrise (lib-theme-skyrise) with four product-driven changes:
*
* 1. Bakes in --warning + --warning-foreground (skyrise had them; pristine
* did not — finance KPIs need warning semantics out of the box).
* 2. Adds a --text-hero size step (3.5rem) above display, for KPI heroes.
* 3. Ships a `.num` utility that sets tabular-nums + slashed-zero +
* letter-spacing:0 — non-negotiable on money numbers.
* 4. Redesigns the dark-mode background. Skyrise's dark blobs sat at
* L≈0.18 with chroma≈0.12, near OKLCH gamut edges, which clipped to
* muddy gray. Here the corner glows are pushed to L≈0.30 with lower
* chroma, paired with a cleaner near-black base — they read as soft
* brand-accent light instead of tinted fog.
*
* Also dropped: the 11 background atmospheres and 4 surface tints skyrise
* shipped for white-label flexibility. A focused product picks one brand
* surface and ships it; if those return, re-import from skyrise.
*
* Fonts loaded by the consuming app via Google Fonts URL @import at the
* very top of its entry CSS — see README. Do NOT @import them here.
*/
:root {
/* ── Colors ──
* Light mode rests on an iridescent lavender→peach→mint→rose gradient
* (defined in @layer base below). Glass surfaces sit at low alpha so the
* gradient reads through; foreground is pushed to L=0.16 for vivid,
* never-faint text against translucent surfaces. */
--background: oklch(0.97 0.015 295);
--foreground: oklch(0.16 0.025 285);
--card: oklch(1 0 0 / 0.58);
--card-foreground: oklch(0.16 0.025 285);
--popover: oklch(1 0 0 / 0.74);
--popover-foreground: oklch(0.16 0.025 285);
--primary: oklch(0.55 0.22 295);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.96 0.012 290);
--secondary-foreground: oklch(0.22 0.025 285);
--muted: oklch(0.95 0.012 290);
--muted-foreground: oklch(0.42 0.02 285);
--accent: oklch(0.93 0.04 295);
--accent-foreground: oklch(0.25 0.05 295);
--destructive: oklch(0.6 0.24 25);
--success: oklch(0.5 0.18 150);
--success-foreground: oklch(1 0 0);
--warning: oklch(0.7 0.18 75);
--warning-foreground: oklch(0.18 0.04 75);
--border: oklch(0 0 0 / 0.09);
--input: oklch(0 0 0 / 0.09);
--ring: oklch(0.55 0.22 295 / 0.55);
/* Charts: violet → blue-cyan → amber → rose → emerald. Hues spread ≥60°
* apart and lightness is varied between adjacent indices so adjacent
* series stay perceptually distinct in multi-line / stacked charts. */
--chart-1: oklch(0.55 0.24 295);
--chart-2: oklch(0.62 0.2 230);
--chart-3: oklch(0.82 0.18 80);
--chart-4: oklch(0.72 0.2 350);
--chart-5: oklch(0.5 0.18 150);
--sidebar: oklch(1 0 0 / 0.5);
--sidebar-foreground: oklch(0.16 0.025 285);
--sidebar-primary: oklch(0.55 0.22 295);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.93 0.04 295);
--sidebar-accent-foreground: oklch(0.25 0.05 295);
--sidebar-border: oklch(0 0 0 / 0.07);
--sidebar-ring: oklch(0.55 0.22 295 / 0.55);
--syntax-keyword: oklch(0.5 0.22 295);
--syntax-string: oklch(0.5 0.16 150);
--syntax-number: oklch(0.55 0.18 35);
--syntax-comment: oklch(0.52 0.02 285);
--syntax-function: oklch(0.48 0.18 235);
--syntax-type: oklch(0.48 0.14 195);
/* ── Radius ── Generous, premium feel. */
--radius: 1rem;
/* ── Motion ── Apple-style spring physics. */
--duration-fast: 180ms;
--duration-base: 320ms;
--duration-slow: 450ms;
--duration-slower: 600ms;
--duration-spring: 520ms;
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
--ease-decelerate: cubic-bezier(0, 0, 0.2, 1);
--ease-accelerate: cubic-bezier(0.4, 0, 1, 1);
--ease-spring-gentle: linear(0, 0.04 8%, 0.12 15%, 0.23 22%, 0.37 30%, 0.51 38%, 0.63 46%, 0.74 54%, 0.83 62%, 0.89 70%, 0.94 78%, 0.97 86%, 0.99 94%, 1);
--ease-spring-snappy: linear(0, 0.06 6%, 0.18 12%, 0.33 18%, 0.5 25%, 0.65 32%, 0.8 40%, 0.9 48%, 0.98 56%, 1.015 64%, 1.02 72%, 1.015 80%, 1.005 88%, 1);
--ease-spring-bouncy: linear(0, 0.08 8%, 0.22 16%, 0.4 24%, 0.58 32%, 0.75 40%, 0.88 48%, 0.98 56%, 1.04 64%, 1.06 72%, 1.04 80%, 1.01 86%, 0.995 92%, 1);
--ease-emphasized: var(--ease-spring-snappy);
/* ── Typography ── Bumped one notch from typical sizes for readability.
* Body sits at 17px (1.0625rem) — never-faint text. All scales ride
* along proportionally. Hero is the finance KPI step above display. */
--text-hero-size: 3.5rem;
--text-hero-lh: 3.75rem;
--text-display-size: 3rem;
--text-display-lh: 3.25rem;
--text-headline-size: 2rem;
--text-headline-lh: 2.375rem;
--text-title-size: 1.5rem;
--text-title-lh: 1.875rem;
--text-body-size: 1.0625rem;
--text-body-lh: 1.625rem;
--text-label-size: 0.9375rem;
--text-label-lh: 1.25rem;
--text-caption-size: 0.8125rem;
--text-caption-lh: 1.125rem;
/* ── Elevation ── Soft layered shadows in line with glass philosophy. */
--elevation-0: none;
--elevation-1: 0 1px 2px oklch(0 0 0 / 0.06);
--elevation-2: 0 2px 8px oklch(0 0 0 / 0.07), 0 1px 2px oklch(0 0 0 / 0.04);
--elevation-3: 0 4px 16px oklch(0 0 0 / 0.08), 0 2px 4px oklch(0 0 0 / 0.04);
--elevation-4: 0 12px 36px oklch(0.5 0.18 295 / 0.14), 0 4px 12px oklch(0 0 0 / 0.06);
--elevation-5: 0 20px 56px oklch(0.5 0.18 295 / 0.18), 0 8px 20px oklch(0 0 0 / 0.08);
--spacing: 0.25rem;
--border-width-thin: 0.5px;
--border-width-base: 1px;
--border-width-thick: 2px;
--border-width-heavy: 4px;
/* ── Glass vibrancy ── Heavy frost, like the skyrise reference. */
--glass-blur: 32px;
--glass-saturate: 220%;
--glass-brightness: 1.06;
--glass-contrast: 1.02;
/* ── Font stacks ── Loaded by the consuming app. */
--font-sans: "Inter", "Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-heading: "Instrument Sans", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, "Cascadia Mono", Consolas, monospace;
}
/*
* Dark mode — clean deep-violet base. Foreground stays high-L for the
* vivid, never-faint feel. Corner accents live in the body bg block below.
*/
.dark {
--background: oklch(0.10 0.015 285);
--foreground: oklch(0.97 0.005 280);
--card: oklch(0.22 0.04 290 / 0.55);
--card-foreground: oklch(0.97 0.005 280);
--popover: oklch(0.22 0.04 290 / 0.78);
--popover-foreground: oklch(0.97 0.005 280);
--primary: oklch(0.7 0.2 295);
--primary-foreground: oklch(0.14 0.04 295);
--secondary: oklch(0.26 0.03 290);
--secondary-foreground: oklch(0.95 0.005 280);
--muted: oklch(0.26 0.03 290);
--muted-foreground: oklch(0.78 0.015 285);
--accent: oklch(0.32 0.06 295);
--accent-foreground: oklch(0.95 0.04 295);
--destructive: oklch(0.7 0.22 25);
--success: oklch(0.74 0.16 150);
--success-foreground: oklch(0.12 0.02 150);
--warning: oklch(0.78 0.16 75);
--warning-foreground: oklch(0.16 0.04 75);
--border: oklch(1 0 0 / 0.1);
--input: oklch(1 0 0 / 0.1);
--ring: oklch(0.7 0.2 295 / 0.55);
--chart-1: oklch(0.68 0.24 295);
--chart-2: oklch(0.72 0.2 230);
--chart-3: oklch(0.88 0.18 80);
--chart-4: oklch(0.82 0.2 350);
--chart-5: oklch(0.6 0.18 150);
--sidebar: oklch(0.2 0.04 290 / 0.55);
--sidebar-foreground: oklch(0.97 0.005 280);
--sidebar-primary: oklch(0.7 0.2 295);
--sidebar-primary-foreground: oklch(0.14 0.04 295);
--sidebar-accent: oklch(0.32 0.06 295);
--sidebar-accent-foreground: oklch(0.95 0.04 295);
--sidebar-border: oklch(1 0 0 / 0.08);
--sidebar-ring: oklch(0.7 0.2 295 / 0.55);
--syntax-keyword: oklch(0.78 0.18 295);
--syntax-string: oklch(0.8 0.16 150);
--syntax-number: oklch(0.82 0.16 35);
--syntax-comment: oklch(0.65 0.02 285);
--syntax-function: oklch(0.78 0.16 235);
--syntax-type: oklch(0.78 0.14 195);
--elevation-1: 0 1px 2px oklch(0 0 0 / 0.4);
--elevation-2: 0 2px 8px oklch(0 0 0 / 0.4), 0 1px 2px oklch(0 0 0 / 0.3);
--elevation-3: 0 4px 16px oklch(0 0 0 / 0.45), 0 2px 4px oklch(0 0 0 / 0.3);
--elevation-4: 0 12px 36px oklch(0.4 0.16 295 / 0.18), 0 4px 12px oklch(0 0 0 / 0.5);
--elevation-5: 0 20px 56px oklch(0.4 0.16 295 / 0.22), 0 8px 20px oklch(0 0 0 / 0.55);
--glass-brightness: 0.92;
--glass-saturate: 200%;
}
@layer base {
body {
background-attachment: fixed;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
letter-spacing: -0.011em;
font-family: var(--font-sans);
font-size: var(--text-body-size);
line-height: var(--text-body-lh);
}
/* Themed text selection — keeps the violet identity. */
::selection {
background-color: oklch(0.55 0.22 295 / 0.28);
color: var(--foreground);
}
.dark ::selection {
background-color: oklch(0.7 0.2 295 / 0.4);
color: var(--foreground);
}
/* Themed scrollbars (Firefox + Chromium; macOS overlay ignores both). */
* {
scrollbar-width: thin;
scrollbar-color: oklch(0 0 0 / 0.18) transparent;
}
.dark * {
scrollbar-color: oklch(1 0 0 / 0.18) transparent;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background-color: oklch(0 0 0 / 0.18);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
background-color: oklch(0 0 0 / 0.28);
background-clip: padding-box;
}
.dark ::-webkit-scrollbar-thumb {
background-color: oklch(1 0 0 / 0.18);
background-clip: padding-box;
}
.dark ::-webkit-scrollbar-thumb:hover {
background-color: oklch(1 0 0 / 0.28);
background-clip: padding-box;
}
/* Headings get the display family — Instrument Sans's tighter counters
* against Inter's open body feel. */
h1, h2, h3, h4, h5, h6,
[data-slot="card-title"],
[data-slot="dialog-title"],
[data-slot="alert-dialog-title"],
[data-slot="sheet-title"],
[data-slot="drawer-title"] {
font-family: var(--font-heading);
letter-spacing: -0.02em;
font-weight: 600;
}
/*
* ── Light-mode body ── iridescent drift, the brand surface.
*/
body,
body[data-bg="drift"],
.bg-variant-drift {
background-image:
radial-gradient(ellipse 80% 60% at 12% 8%, oklch(0.92 0.09 295 / 0.7), transparent 60%),
radial-gradient(ellipse 70% 60% at 88% 14%, oklch(0.93 0.1 350 / 0.7), transparent 55%),
radial-gradient(ellipse 80% 60% at 92% 88%, oklch(0.93 0.1 200 / 0.7), transparent 60%),
radial-gradient(ellipse 70% 60% at 8% 92%, oklch(0.94 0.09 145 / 0.65), transparent 55%),
linear-gradient(180deg, oklch(0.985 0.02 295), oklch(0.96 0.025 320));
}
/*
* ── Dark-mode body ── deep clean base + four soft corner glows.
*
* Design: blob L pushed to ~0.30 (vs skyrise's 0.18) so they sit well
* above the L=0.10 base — perceived as *light* rather than coloured
* fog. Chroma dropped to 0.060.09 (vs 0.120.14) to stay inside OKLCH
* dark-gamut sweet-spot and avoid the gray clip. Base gradient is
* near-monochrome (chroma 0.010.02) so the accents do all the colour
* work — same hue palette as light (purple→pink→cyan→mint), but
* articulated through light, not stain.
*/
html.dark body,
html.dark body[data-bg="drift"],
html.dark .bg-variant-drift {
background-image:
radial-gradient(ellipse 70% 55% at 10% 8%, oklch(0.32 0.09 295 / 0.65), transparent 60%),
radial-gradient(ellipse 60% 50% at 90% 14%, oklch(0.30 0.09 340 / 0.60), transparent 55%),
radial-gradient(ellipse 70% 55% at 92% 90%, oklch(0.28 0.07 200 / 0.55), transparent 60%),
radial-gradient(ellipse 60% 50% at 8% 92%, oklch(0.30 0.06 145 / 0.50), transparent 55%),
linear-gradient(180deg, oklch(0.10 0.012 285), oklch(0.08 0.018 295));
}
/*
* Animated drift layer — two large radial-blob fields that translate
* and rotate slowly, driving the chroma the glass refracts.
*/
[data-slot="aurora-field"] { display: none; }
body[data-bg="drift"] [data-slot="aurora-field"],
body:not([data-bg]) [data-slot="aurora-field"] { display: block; }
.aurora-blob {
position: absolute;
inset: -25%;
background-repeat: no-repeat;
will-change: transform;
}
.aurora-blob-1 {
background-image:
radial-gradient(ellipse 44% 36% at 22% 28%, oklch(0.9 0.13 295 / 0.85), transparent 60%),
radial-gradient(ellipse 38% 32% at 78% 22%, oklch(0.9 0.13 350 / 0.8), transparent 55%);
animation: skyfin-drift-a 32s ease-in-out infinite;
}
.aurora-blob-2 {
background-image:
radial-gradient(ellipse 48% 38% at 82% 74%, oklch(0.9 0.14 200 / 0.8), transparent 60%),
radial-gradient(ellipse 42% 34% at 16% 80%, oklch(0.92 0.12 145 / 0.75), transparent 55%);
animation: skyfin-drift-b 40s ease-in-out infinite;
}
/* Dark variant of the drifting aurora — same L/chroma discipline as
* the static dark base above. Same hues, tame chroma, well above base. */
html.dark .aurora-blob-1 {
background-image:
radial-gradient(ellipse 44% 36% at 22% 28%, oklch(0.32 0.09 295 / 0.55), transparent 60%),
radial-gradient(ellipse 38% 32% at 78% 22%, oklch(0.30 0.10 340 / 0.50), transparent 55%);
}
html.dark .aurora-blob-2 {
background-image:
radial-gradient(ellipse 48% 38% at 82% 74%, oklch(0.28 0.08 200 / 0.50), transparent 60%),
radial-gradient(ellipse 42% 34% at 16% 80%, oklch(0.30 0.07 145 / 0.45), transparent 55%);
}
@keyframes skyfin-drift-a {
0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
33% { transform: translate3d(5%, -3%, 0) rotate(10deg) scale(1.07); }
66% { transform: translate3d(-3%, 4%, 0) rotate(-8deg) scale(0.96); }
}
@keyframes skyfin-drift-b {
0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
33% { transform: translate3d(-5%, 4%, 0) rotate(-12deg) scale(1.09); }
66% { transform: translate3d(4%, -3%, 0) rotate(8deg) scale(0.94); }
}
@media (prefers-reduced-motion: reduce) {
.aurora-blob { animation: none; }
}
/*
* ── Glass surfaces ── Apple-vibrancy stack on every shadcn surface
* that exposes a [data-slot]. blur softens, saturate pumps the
* gradient chroma the glass is sampling, brightness/contrast keep
* edges legible.
*/
[data-slot="card"],
[data-slot="popover-content"],
[data-slot="dropdown-menu-content"],
[data-slot="context-menu-content"],
[data-slot="menubar-content"],
[data-slot="hover-card-content"],
[data-slot="select-content"],
[data-slot="combobox-content"],
[data-slot="command"],
[data-slot="tooltip-content"],
[data-slot="sheet-content"],
[data-slot="dialog-content"],
[data-slot="alert-dialog-content"],
[data-slot="drawer-content"],
[data-slot="sidebar"] {
position: relative;
backdrop-filter:
blur(var(--glass-blur))
saturate(var(--glass-saturate))
brightness(var(--glass-brightness))
contrast(var(--glass-contrast));
-webkit-backdrop-filter:
blur(var(--glass-blur))
saturate(var(--glass-saturate))
brightness(var(--glass-brightness))
contrast(var(--glass-contrast));
transition-timing-function: var(--ease-spring-snappy);
animation-timing-function: var(--ease-spring-snappy);
animation-duration: var(--duration-spring);
}
/* Springy press feel on interactive bits. */
[data-slot="button"],
[data-slot="chip"],
[data-slot="toggle"],
[data-slot="item"] {
transition-timing-function: var(--ease-spring-bouncy);
transition-duration: var(--duration-fast);
}
/*
* Premium primary button — violet→fuchsia gradient + top-edge sheen.
*/
[data-slot="button"].bg-primary {
background-image: linear-gradient(
135deg,
oklch(0.56 0.24 290) 0%,
oklch(0.6 0.26 320) 50%,
oklch(0.58 0.24 350) 100%
);
position: relative;
isolation: isolate;
}
[data-slot="button"].bg-primary::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
background: linear-gradient(
180deg,
oklch(1 0 0 / 0.35) 0,
oklch(1 0 0 / 0.08) 45%,
oklch(1 0 0 / 0) 60%
);
mix-blend-mode: plus-lighter;
}
.dark [data-slot="button"].bg-primary {
background-image: linear-gradient(
135deg,
oklch(0.66 0.22 290) 0%,
oklch(0.7 0.24 320) 50%,
oklch(0.66 0.22 350) 100%
);
}
.dark [data-slot="button"].bg-primary::after {
background: linear-gradient(
180deg,
oklch(1 0 0 / 0.18) 0,
oklch(1 0 0 / 0.04) 45%,
oklch(1 0 0 / 0) 60%
);
}
/*
* Premium card padding — roomy 24px vs the lib's compact 16px.
*/
[data-slot="card"],
[data-slot="dialog-content"],
[data-slot="alert-dialog-content"],
[data-slot="sheet-content"],
[data-slot="drawer-content"],
[data-slot="popover-content"] {
--card-padding: calc(var(--spacing) * 6);
gap: calc(var(--spacing) * 5);
}
[data-slot="card-header"],
[data-slot="card-content"],
[data-slot="card-footer"] {
padding-inline: var(--card-padding, calc(var(--spacing) * 6));
}
/*
* AI orb avatar — any avatar fallback marked `data-ai` gets the
* rainbow gradient signature.
*/
[data-slot="avatar-fallback"][data-ai] {
background-image: conic-gradient(
from 210deg at 50% 50%,
oklch(0.6 0.24 295) 0deg,
oklch(0.7 0.22 350) 90deg,
oklch(0.78 0.18 35) 180deg,
oklch(0.7 0.18 200) 270deg,
oklch(0.6 0.24 295) 360deg
);
background-color: transparent;
color: oklch(1 0 0);
font-weight: 600;
position: relative;
}
[data-slot="avatar-fallback"][data-ai]::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
background: linear-gradient(
180deg,
oklch(1 0 0 / 0.3) 0,
oklch(1 0 0 / 0) 50%
);
mix-blend-mode: plus-lighter;
}
.dark [data-slot="avatar-fallback"][data-ai] {
background-image: conic-gradient(
from 210deg at 50% 50%,
oklch(0.7 0.22 295) 0deg,
oklch(0.78 0.2 350) 90deg,
oklch(0.84 0.16 35) 180deg,
oklch(0.78 0.16 200) 270deg,
oklch(0.7 0.22 295) 360deg
);
}
/*
* Inner top highlight — the wet-glass sheen Apple uses on surfaces.
*/
[data-slot="card"]::after,
[data-slot="popover-content"]::after,
[data-slot="dropdown-menu-content"]::after,
[data-slot="context-menu-content"]::after,
[data-slot="menubar-content"]::after,
[data-slot="hover-card-content"]::after,
[data-slot="select-content"]::after,
[data-slot="combobox-content"]::after,
[data-slot="command"]::after,
[data-slot="tooltip-content"]::after,
[data-slot="sheet-content"]::after,
[data-slot="dialog-content"]::after,
[data-slot="alert-dialog-content"]::after,
[data-slot="drawer-content"]::after,
[data-slot="sidebar"]::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
background:
linear-gradient(
180deg,
oklch(1 0 0 / 0.6) 0,
oklch(1 0 0 / 0) 1.5px,
oklch(1 0 0 / 0.14) 1.5px,
oklch(1 0 0 / 0) 35%
);
mix-blend-mode: plus-lighter;
}
.dark [data-slot="card"]::after,
.dark [data-slot="popover-content"]::after,
.dark [data-slot="dropdown-menu-content"]::after,
.dark [data-slot="context-menu-content"]::after,
.dark [data-slot="menubar-content"]::after,
.dark [data-slot="hover-card-content"]::after,
.dark [data-slot="select-content"]::after,
.dark [data-slot="combobox-content"]::after,
.dark [data-slot="command"]::after,
.dark [data-slot="tooltip-content"]::after,
.dark [data-slot="sheet-content"]::after,
.dark [data-slot="dialog-content"]::after,
.dark [data-slot="alert-dialog-content"]::after,
.dark [data-slot="drawer-content"]::after,
.dark [data-slot="sidebar"]::after {
background:
linear-gradient(
180deg,
oklch(1 0 0 / 0.2) 0,
oklch(1 0 0 / 0) 1.5px,
oklch(1 0 0 / 0.05) 1.5px,
oklch(1 0 0 / 0) 35%
);
}
/*
* ── `.num` utility ── Apply to any element that displays money or
* counts. Forces tabular monospaced digits, the unambiguous slashed-
* zero stylistic set Inter ships, and resets the body's negative
* tracking that would otherwise crash digits into each other.
*/
.num {
font-feature-settings: "tnum", "ss01", "cv01";
font-variant-numeric: tabular-nums;
letter-spacing: 0;
}
}
/* ── Root size & font-scale ──
* 18px root by default. Body lands ~19px once the --text-body-size
* 1.0625rem multiplier applies. The data-font-scale hook lets the
* consuming app expose a user-facing size toggle. */
@layer base {
html { font-size: 18px; }
html[data-font-scale="sm"] { font-size: 16px; }
html[data-font-scale="md"] { font-size: 18px; }
html[data-font-scale="lg"] { font-size: 20px; }
html[data-font-scale="xl"] { font-size: 22px; }
}