app/tokens.css
Rama main · réplica de lectura del repositorio original.
/* ============================================================================
Minddy design tokens — radix-nova style, OKLch color space.
Imported by app/globals.css right after `@import "tailwindcss";`. The
primitives of components/ui/ and the application read these variables.
Fonts (--font-inter, --font-space-grotesk, --font-instrument-serif) are
provided by next/font in app/layout.tsx. If absent, the browser default
sans-serif is used.
── Brand color ────────────────────────────────────────────────────────────
minddy's color is lavender (MIN-840): `#7059C4` in light mode, `#B6A5F0` in
dark mode. It is both the default accent (`--primary`, `--ring` and the
sidebar's: the primary button, segmented controls, switches, the focus
ring) and the identity color (`--brand` and the surfaces derived from it).
The account palettes of app/theme-presets.css override the accent per mode
(MIN-696); the "Noir" / "Blanc" presets bring back ink actions. `--brand`
stays lavender under every accent preset.
============================================================================ */
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
/* ── State variants ──────────────────────────────────────────────────────────
Restored from the shadcn base (lost on extraction). They map short names to
the data attributes Radix/cmdk actually emit, so utilities like
`data-horizontal:h-1`, `data-active:shadow-sm` or `data-selected:bg-muted`
target the right element instead of matching every node (or none). */
@custom-variant data-open {
&:where([data-state="open"]),
&:where([data-open]:not([data-open="false"])) {
@slot;
}
}
@custom-variant data-closed {
&:where([data-state="closed"]),
&:where([data-closed]:not([data-closed="false"])) {
@slot;
}
}
@custom-variant data-checked {
&:where([data-state="checked"]),
&:where([data-checked]:not([data-checked="false"])) {
@slot;
}
}
@custom-variant data-unchecked {
&:where([data-state="unchecked"]),
&:where([data-unchecked]:not([data-unchecked="false"])) {
@slot;
}
}
@custom-variant data-indeterminate {
&:where([data-state="indeterminate"]),
&:where([data-indeterminate]:not([data-indeterminate="false"])) {
@slot;
}
}
@custom-variant data-selected {
&:where([data-selected="true"]) {
@slot;
}
}
@custom-variant data-disabled {
&:where([data-disabled="true"]),
&:where([data-disabled]:not([data-disabled="false"])) {
@slot;
}
}
@custom-variant data-active {
&:where([data-state="active"]),
&:where([data-active]:not([data-active="false"])) {
@slot;
}
}
@custom-variant data-horizontal {
&:where([data-orientation="horizontal"]) {
@slot;
}
}
@custom-variant data-vertical {
&:where([data-orientation="vertical"]) {
@slot;
}
}
@utility no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
/**
* NAVIGATION columns (primary sidebar, secondary sidebar): the thumb only shows
* while the column is hovered. A navigation column is read at a glance; a
* vertical bar parked alongside the list weighs more than it informs — and
* becomes useful again the moment you come to scroll it.
*
* A 10px track for a comfortable pointer target, and a thumb that only fills 4
* of it: the 3px transparent border thins it without shrinking what you can grab
* (`background-clip: content-box`). It is painted from `--scrollbar-thumb`,
* defined below in ALPHA — black in light theme, white in dark — so it sits as
* well on a card as on the sidebar, without a tone per surface.
*
* WORTH KNOWING: styling a scrollbar makes it lose its OVERLAY mode. On macOS,
* where it floated above the content taking nothing from it, it now reserves its
* 10px as soon as there is something to scroll. `no-scrollbar` above stays there
* for the places that would rather have nothing at all.
*/
@utility scrollbar-quiet {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
&::-webkit-scrollbar {
width: 10px;
height: 10px;
}
&::-webkit-scrollbar-track,
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
background-clip: content-box;
border: 3px solid transparent;
border-radius: 9999px;
}
&:hover {
scrollbar-color: var(--scrollbar-thumb) transparent;
}
&:hover::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
}
&:hover::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover);
}
}
/* Elevation. Only what sits ABOVE the page casts a shadow: menus, popovers,
dialogs, sheets, toasts, a dragged item. Tailwind's defaults pair every
shadow with a tight 1-2px band right against the edge, which read as a thin
dark outline around each surface rather than as depth; these are wide and
soft, with no band at the edge. The small steps stay for the few raised
controls (a switch thumb) and are nearly flat. */
@theme {
--shadow-2xs: 0 1px 2px rgb(0 0 0 / 0.03);
--shadow-xs: 0 1px 3px rgb(0 0 0 / 0.04);
--shadow-sm: 0 1px 4px rgb(0 0 0 / 0.06), 0 2px 8px -4px rgb(0 0 0 / 0.04);
--shadow: 0 1px 4px rgb(0 0 0 / 0.06), 0 2px 8px -4px rgb(0 0 0 / 0.04);
--shadow-md: 0 8px 24px -6px rgb(0 0 0 / 0.1), 0 2px 10px -4px rgb(0 0 0 / 0.04);
--shadow-lg: 0 14px 36px -10px rgb(0 0 0 / 0.14), 0 4px 14px -6px rgb(0 0 0 / 0.05);
--shadow-xl: 0 22px 48px -14px rgb(0 0 0 / 0.18), 0 6px 18px -8px rgb(0 0 0 / 0.06);
--shadow-2xl: 0 30px 70px -18px rgb(0 0 0 / 0.24);
}
@theme inline {
/* Menu separators: the general border, light enough to divide a list
without drawing a line across it. Its own name keeps menus apart should
the two ever differ again. */
--color-menu-separator: var(--border);
/* The scrim behind every modal layer (MIN-931): dialogs, wizards, alert
dialogs, sheets and side panels. Menus, popovers and the Numo shell
float without one. */
--color-scrim: var(--overlay-scrim);
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-inter);
--font-mono: var(--font-inter);
--font-serif: var(--font-instrument-serif);
/* Headings use Inter too: Space Grotesk is not loaded. */
--font-display: var(--font-inter);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
/* Semantic color mappings */
--color-control: var(--control);
--color-control-hover: var(--control-hover);
--color-primary-hover: var(--primary-hover);
--color-foreground-hover: var(--foreground-hover);
--color-destructive-surface: var(--destructive-surface);
--color-destructive-surface-hover: var(--destructive-surface-hover);
--color-destructive-highlight: var(--destructive-highlight);
--color-destructive-button: var(--destructive-button);
--color-destructive-button-foreground: var(--destructive-button-foreground);
--color-destructive-border: var(--destructive-border);
--color-destructive-border-hover: var(--destructive-border-hover);
--color-tabs-hover: var(--tabs-hover);
--color-slider-thumb: var(--slider-thumb);
--color-surface-raised: var(--surface-raised);
--color-surface-sunken: var(--surface-sunken);
--color-surface-glass: var(--surface-glass);
--color-surface-brand: var(--surface-brand);
--color-surface-brand-hover: var(--surface-brand-hover);
--color-surface-info: var(--surface-info);
--color-surface-success: var(--surface-success);
--color-surface-warning: var(--surface-warning);
--color-surface-danger: var(--surface-danger);
--color-surface-type-video: var(--surface-type-video);
--color-surface-type-clip: var(--surface-type-clip);
--color-surface-type-screenshot: var(--surface-type-screenshot);
--color-surface-type-locked: var(--surface-type-locked);
--color-border-subtle: var(--border-subtle);
--color-accent-glow: var(--accent-glow);
--color-accent-glow-foreground: var(--accent-glow-foreground);
--color-accent-glow-muted: var(--accent-glow-muted);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-warning-strong: var(--warning-strong);
--color-text-tertiary: var(--text-tertiary);
/* Brand accent — identity color (badges, indicators). Derived from primary. */
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
/* Typography scale extensions (UI coherence tokens) */
--text-2xs: 10px;
--text-3xs: 11px;
--text-md: 13px;
/* Layout spacing */
--spacing-header: 60px;
--spacing-sidebar: 256px;
--spacing-sidebar-sm: 56px;
/* Component sizing */
--spacing-icon: 16px;
--spacing-icon-sm: 14px;
--spacing-favicon: 20px;
--radius-favicon: 4px;
/* App shell max size (ultrawide monitors) */
--spacing-app-max-w: 1800px;
--spacing-app-max-h: 1100px;
--breakpoint-3xl: 2200px;
/* Default only: app/globals.css redefines `desktop` as 1024px and adds
`wide` at 1200px. App chrome follows the `app-mobile:` / `app-desktop:`
variants (lib/app-layout.ts), not these width breakpoints; see DESIGN.md. */
--breakpoint-desktop: 1200px;
}
:root {
/* Dialog max size — 90% of app shell on ultrawide, 90% of viewport below. */
--spacing-dialog-w: min(90vw, calc(var(--spacing-app-max-w) * 0.9));
--spacing-dialog-h: min(90vh, calc(var(--spacing-app-max-h) * 0.9));
--radius: 1rem;
--background: oklch(0.975 0.006 266);
--foreground: oklch(0.145 0 0);
--card: oklch(0.995 0.002 266);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(0.995 0.002 266);
--popover-foreground: oklch(0.145 0 0);
/* Lavender, with white text on top (5.4:1). */
--primary: #7059c4;
--primary-foreground: #ffffff;
--secondary: oklch(0.920 0.020 268);
--secondary-foreground: oklch(0.256 0.102 268);
--muted: oklch(0.935 0.008 268);
--muted-foreground: oklch(0.500 0.010 266);
--accent: oklch(0.945 0.010 268);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.58 0.22 27);
/* Borders are a light veil of the text color rather than a fixed gray, so
an edge reads the same on the page, a card or a menu: the weight of the
menus' separators, carried to every border (scratchpad, 2026-10-30). */
--border: color-mix(in oklab, var(--foreground) 6%, transparent);
--input: oklch(0.910 0.012 268);
--ring: #7059c4;
/* Scrollbar thumb, read by `scrollbar-quiet`. Repeat that utility's rules on
`*` in your app's CSS to make it the scrollbar of every surface. */
--scrollbar-thumb: oklch(0 0 0 / 18%);
--scrollbar-thumb-hover: oklch(0 0 0 / 30%);
--chart-1: oklch(0.85 0.12 266);
--chart-2: oklch(0.754 0.120 270);
--chart-3: oklch(0.65 0.15 266);
--chart-4: oklch(0.498 0.282 266);
--chart-5: oklch(0.45 0.12 266);
--sidebar: oklch(0.975 0.006 266);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: #7059c4;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: oklch(0.940 0.012 268);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.900 0.012 268);
--sidebar-ring: #7059c4;
/* Semantic tokens */
/* Shared control fill — inputs, selects, outline buttons and neutral chips use
the same opaque gray. It stays visible on both background and card without
relying on a border. */
--control: oklch(0.935 0.008 268);
--control-hover: oklch(0.900 0.010 268);
--primary-hover: var(--primary);
--foreground-hover: var(--foreground);
--destructive-button: var(--destructive);
--destructive-button-foreground: oklch(0.985 0 0);
--destructive-surface: oklch(0.925 0.040 27);
--destructive-surface-hover: oklch(0.895 0.060 27);
/* A destructive menu row's highlight: a tint of the red over the menu's
own surface (translucent menus included), not an opaque pink block. */
--destructive-highlight: oklch(0.58 0.22 27 / 0.10);
--destructive-border: oklch(0.820 0.080 27);
--destructive-border-hover: oklch(0.750 0.100 27);
--tabs-hover: var(--control-hover);
--slider-thumb: oklch(1 0 0);
--surface-raised: oklch(0.995 0.002 266);
--surface-sunken: oklch(0.940 0.008 268);
--surface-glass: oklch(0.965 0.006 268 / 60%);
--surface-brand: #efebf8;
--surface-brand-hover: #e4ddf4;
--surface-info: oklch(0.950 0.022 265);
--surface-success: oklch(0.920 0.032 155);
--surface-warning: oklch(0.937 0.021 75);
--surface-danger: var(--destructive-surface);
--surface-type-video: oklch(0.930 0.030 303);
--surface-type-clip: oklch(0.930 0.030 163);
--surface-type-screenshot: oklch(0.930 0.030 230);
--surface-type-locked: oklch(0.930 0.030 55);
--border-subtle: color-mix(in oklab, var(--foreground) 4%, transparent);
--accent-glow: #7059c4;
--accent-glow-foreground: #ffffff;
--accent-glow-muted: oklch(0.541 0.160 289.9 / 10%);
--success: oklch(0.68 0.16 155);
--success-foreground: oklch(1 0 0);
--warning: oklch(0.78 0.14 75);
--warning-foreground: oklch(0.25 0 0);
--warning-strong: oklch(0.53 0.17 52);
--text-tertiary: oklch(0.500 0.010 266);
/* Brand — lavender, the identity color, under every accent preset. */
--brand: #7059c4;
--brand-foreground: #ffffff;
/* Modal scrim (MIN-931): dims the whole app behind a dialog so the layer
reads as separate from it. A cool near-black, the page's own hue. */
--overlay-scrim: oklch(0.2 0.02 266 / 32%);
/* Ultrawide canvas — backdrop behind the centered app shell on ≥3xl screens */
--canvas: oklch(0.945 0.025 266);
--canvas-glow-1: oklch(0.80 0.10 290 / 0.55);
--canvas-glow-2: oklch(0.80 0.09 320 / 0.45);
}
.dark {
/* Linear-style dark — true black/gray with an imperceptible cool cast. */
--background: oklch(0.135 0.003 250);
--foreground: oklch(0.985 0 0);
--card: oklch(0.170 0.003 250);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.170 0.003 250);
--popover-foreground: oklch(0.985 0 0);
/* Lavender, with deep-lavender text on top (8.1:1). */
--primary: #b6a5f0;
--primary-foreground: #1b1530;
--secondary: oklch(0.220 0.003 250);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.200 0.003 250);
--muted-foreground: oklch(0.620 0.003 250);
--accent: oklch(0.230 0.003 250);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 6%);
--input: oklch(1 0 0 / 12%);
--ring: #b6a5f0;
--scrollbar-thumb: oklch(1 0 0 / 16%);
--scrollbar-thumb-hover: oklch(1 0 0 / 28%);
--chart-1: oklch(0.85 0 0);
--chart-2: oklch(0.70 0 0);
--chart-3: oklch(0.55 0 0);
--chart-4: oklch(0.65 0.18 265);
--chart-5: oklch(0.40 0 0);
--sidebar: oklch(0.110 0.003 250);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: #b6a5f0;
--sidebar-primary-foreground: #1b1530;
--sidebar-accent: oklch(0.200 0.003 250);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 6%);
--sidebar-ring: #b6a5f0;
/* Semantic tokens dark */
/* Opaque on purpose: controls must stay visible without relying on a border. */
--control: oklch(0.245 0.006 250);
--control-hover: oklch(0.285 0.006 250);
--primary-hover: var(--primary);
--foreground-hover: var(--foreground);
--destructive-button: oklch(0.50 0.20 22.216);
--destructive-button-foreground: oklch(0.985 0 0);
--destructive-surface: oklch(0.250 0.050 22.216);
--destructive-surface-hover: oklch(0.310 0.070 22.216);
--destructive-highlight: oklch(0.704 0.191 22.216 / 0.16);
--destructive-border: oklch(0.360 0.080 22.216);
--destructive-border-hover: oklch(0.420 0.100 22.216);
--tabs-hover: var(--control-hover);
--slider-thumb: oklch(0.985 0 0);
--surface-raised: oklch(0.170 0.003 250);
--surface-sunken: oklch(0.090 0.003 250);
--surface-glass: oklch(0.170 0.003 250 / 60%);
--surface-brand: #29233d;
--surface-brand-hover: #332b4b;
--surface-info: oklch(0.210 0.033 265);
--surface-success: oklch(0.210 0.032 155);
--surface-warning: oklch(0.240 0.028 75);
--surface-danger: var(--destructive-surface);
--surface-type-video: oklch(0.230 0.030 303);
--surface-type-clip: oklch(0.230 0.030 163);
--surface-type-screenshot: oklch(0.230 0.030 230);
--surface-type-locked: oklch(0.230 0.030 55);
--border-subtle: oklch(1 0 0 / 4%);
--accent-glow: #b6a5f0;
--accent-glow-foreground: #1b1530;
--accent-glow-muted: oklch(0.764 0.107 294.1 / 12%);
--success: oklch(0.72 0.16 155);
--success-foreground: oklch(0.95 0 0);
--warning: oklch(0.82 0.14 75);
--warning-foreground: oklch(0.95 0 0);
--warning-strong: oklch(0.82 0.14 75);
--text-tertiary: oklch(0.555 0 0);
/* Brand dark */
--brand: #b6a5f0;
--brand-foreground: #1b1530;
/* Modal scrim dark: the page is already near black, so the scrim needs
more weight to push it back behind the raised card surface. */
--overlay-scrim: oklch(0 0 0 / 56%);
/* Ultrawide canvas dark */
--canvas: oklch(0.060 0.003 250);
--canvas-glow-1: oklch(0.28 0.08 292 / 0.30);
--canvas-glow-2: oklch(0.20 0.05 292 / 0.25);
}
@supports (color: color-mix(in oklab, red, blue)) {
:root {
--primary-hover: color-mix(in oklab, var(--primary) 90%, var(--background));
--foreground-hover: color-mix(in oklab, var(--foreground) 90%, var(--background));
--destructive-surface: color-mix(in oklab, var(--destructive) 10%, var(--background));
--destructive-surface-hover: color-mix(in oklab, var(--destructive) 20%, var(--background));
--destructive-border: color-mix(in oklab, var(--destructive) 30%, var(--background));
--destructive-border-hover: color-mix(in oklab, var(--destructive) 40%, var(--background));
--tabs-hover: color-mix(in oklab, var(--foreground) 5%, var(--muted));
/* Lavender tint over the page: about #efebf8 / #e4ddf4 on white. */
--surface-brand: color-mix(in oklab, var(--brand) 10%, var(--background));
--surface-brand-hover: color-mix(in oklab, var(--brand) 20%, var(--background));
--surface-info: color-mix(in oklab, var(--brand) 10%, var(--background));
--surface-success: color-mix(in oklab, var(--success) 10%, var(--background));
--surface-warning: color-mix(in oklab, var(--warning) 15%, var(--background));
--surface-type-video: color-mix(in oklab, oklch(0.62 0.21 303) 10%, var(--background));
--surface-type-clip: color-mix(in oklab, oklch(0.62 0.17 163) 10%, var(--background));
--surface-type-screenshot: color-mix(in oklab, oklch(0.62 0.16 230) 10%, var(--background));
--surface-type-locked: color-mix(in oklab, oklch(0.66 0.17 55) 10%, var(--background));
}
.dark {
--primary-hover: color-mix(in oklab, var(--primary) 90%, var(--background));
--foreground-hover: color-mix(in oklab, var(--foreground) 90%, var(--background));
--destructive-surface: color-mix(in oklab, var(--destructive) 20%, var(--background));
--destructive-surface-hover: color-mix(in oklab, var(--destructive) 30%, var(--background));
--destructive-border: color-mix(in oklab, var(--destructive) 40%, var(--background));
--destructive-border-hover: color-mix(in oklab, var(--destructive) 50%, var(--background));
--tabs-hover: color-mix(in oklab, var(--foreground) 5%, var(--muted));
--surface-brand: color-mix(in oklab, var(--brand) 20%, var(--background));
--surface-brand-hover: color-mix(in oklab, var(--brand) 30%, var(--background));
--surface-info: color-mix(in oklab, var(--brand) 20%, var(--background));
--surface-success: color-mix(in oklab, var(--success) 20%, var(--background));
--surface-warning: color-mix(in oklab, var(--warning) 25%, var(--background));
--surface-type-video: color-mix(in oklab, oklch(0.68 0.19 303) 20%, var(--background));
--surface-type-clip: color-mix(in oklab, oklch(0.68 0.15 163) 20%, var(--background));
--surface-type-screenshot: color-mix(in oklab, oklch(0.68 0.14 230) 20%, var(--background));
--surface-type-locked: color-mix(in oklab, oklch(0.72 0.15 55) 20%, var(--background));
}
}
html,
body {
font-family:
var(--font-inter),
ui-sans-serif,
system-ui,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji";
}
html {
scroll-behavior: smooth;
}
/* Keyframes used by primitives (skeleton shimmer, accordion, collapsible) */
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
@keyframes accordion-down {
from { height: 0; opacity: 0; }
to { height: var(--radix-accordion-content-height); opacity: 1; }
}
@keyframes accordion-up {
from { height: var(--radix-accordion-content-height); opacity: 1; }
to { height: 0; opacity: 0; }
}
@keyframes collapsible-down {
from { height: 0; opacity: 0; }
to { height: var(--radix-collapsible-content-height); opacity: 1; }
}
@keyframes collapsible-up {
from { height: var(--radix-collapsible-content-height); opacity: 1; }
to { height: 0; opacity: 0; }
}
/* The drawn caret of the one-time-code boxes (components/otp-input.tsx). */
@keyframes caret-blink {
0%, 70%, 100% { opacity: 1; }
20%, 50% { opacity: 0; }
}
/* Text shimmer — the "this is running" signal of the AI surfaces (ToolCallList's
in-flight action, WorkAccordion's header). A bright band sweeps across dimmed
text, IN READING ORDER — left to right.
That direction is a sign, not a duration, and it is easy to get backwards.
With `background-size: 300%`, a `background-position` of X% offsets the image
by (width − 3×width)·X — i.e. LEFTWARDS as X grows. So the bright band (at
50% of the gradient) sits 1.5×width to the RIGHT of the text at 0%, and
0.5×width to the left at 100%. Sweeping with the text therefore means going
from 100% to 0%, not the other way round: 0%→100% enters from the right and
reads against the text, which is instantly noticeable.
The gradient is THREE times wider than the text (300%) with no-repeat, and
that is not cosmetic either — it is what makes the loop seamless. At 200% the
band lands on one edge at each extreme: half-visible at BOTH ends, so every
lap it teleported across. At 300% it is off the text at both ends, so the two
extremes look IDENTICAL (uniformly dimmed text) and the seam is invisible.
The short breath between passes is intended. At any position the image still
covers the whole text — no transparent gap. */
@keyframes text-shimmer {
from { background-position: 100% center; }
to { background-position: 0% center; }
}
@utility text-shimmer {
/* NB: `background-clip: text` requires `color: transparent`, which makes
`currentColor` transparent too (it RESOLVES to `color`) — so this uses the
theme tokens directly rather than currentColor. Base = muted-foreground
(the normal label color), sweeping band = foreground (brighter). */
background-image: linear-gradient(
90deg,
var(--muted-foreground) 0%,
var(--muted-foreground) 35%,
var(--foreground) 50%,
var(--muted-foreground) 65%,
var(--muted-foreground) 100%
);
background-size: 300% 100%;
background-repeat: no-repeat;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
animation: text-shimmer 2.4s linear infinite;
@media (prefers-reduced-motion: reduce) {
animation: none;
background: none;
color: var(--muted-foreground);
-webkit-text-fill-color: var(--muted-foreground);
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
/* The containing block of the coarse-pointer hit areas
(components/ui/touch-target.ts). In the base layer on purpose: a control
positioned by a utility (`absolute`, `fixed`, `sticky` — the "back to
bottom" arrow of a thread, the close cross of a dialog) keeps its own
positioning, which already makes it a containing block. */
[data-slot="button"] {
position: relative;
}
@media (pointer: coarse) {
:is([data-slot="select-trigger"], [data-slot="dialog-close"]) {
position: relative;
}
}
}
/* Card hover-lift — subtle rise + shadow for a fine pointer that hovers. */
.lift {
transition:
transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 0.3s ease,
border-color 0.3s ease;
}
@media (hover: hover) and (pointer: fine) {
.lift:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px -20px rgba(0, 0, 0, 0.28);
}
}
/* Reduced motion — kill CSS animations globally */
@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;
}
}
/* Selected text takes the accent: a light tint of `--primary` behind text
that keeps its own color, so contrast stays the text's own on every theme
and accent (a black or white accent gives a neutral gray tint).
`--selection-block` is that same tint, for what an editor selects as a
whole (selected blocks, an image, an equation: app/globals.css). Declared
again under `.dark`, like `--primary-hover`, so it follows the theme's
accent wherever the class sits. */
:root,
.dark {
--selection-block: color-mix(in oklab, var(--primary) 28%, transparent);
}
::selection {
background-color: var(--selection-block);
color: inherit;
}
/* Global user-select: none (outside @layer for higher priority) */
*:not(input):not(textarea):not([contenteditable="true"]):not(pre):not(code) {
user-select: none;
}
input,
textarea,
[contenteditable="true"],
pre,
code,
.select-text {
user-select: text;
}
/* The text INSIDE an editable surface too — its paragraphs, list items and
marks are elements the reset above reached. A desktop browser selects in
an editing host anyway; a touch screen honors the reset, and long-press
selection did nothing in every editor. Atomic pieces the editor marks
non-editable (a mention pill, a task's checkbox) keep the reset. */
[contenteditable="true"] :not([contenteditable="false"], [contenteditable="false"] *) {
user-select: text;
}
/* A READ-ONLY editor too — a reader's view of a description, a page or a
plan — so whoever cannot edit the text can still select and copy it. */
.ProseMirror[contenteditable="false"],
.ProseMirror[contenteditable="false"] * {
user-select: text;
}
/* iOS Safari zooms a focused control whose font-size < 16px. Enforce 16px floor
on small screens. Excludes checkbox/radio/buttons. */
@media (max-width: 767px) {
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="button"]):not([type="submit"]),
/* A title field is already 18 px or more: the floor would SHRINK it, and a
page title read 16 px on a phone. */
textarea:not(.title-field),
select,
[contenteditable="true"] {
font-size: 16px;
}
}
/* react-colorful — match the radix-nova look (rounded, soft shadows). */
.color-input-picker .react-colorful {
width: 220px;
height: 220px;
gap: 10px;
}
.color-input-picker .react-colorful__saturation {
border-radius: calc(var(--radius) - 2px);
border-bottom: none;
box-shadow: inset 0 0 0 1px var(--border);
}
.color-input-picker .react-colorful__last-control {
border-radius: 999px;
}
.color-input-picker .react-colorful__hue,
.color-input-picker .react-colorful__alpha {
height: 12px;
border-radius: 999px;
box-shadow: inset 0 0 0 1px var(--border);
}
.color-input-picker .react-colorful__pointer {
width: 18px;
height: 18px;
border-width: 2px;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.12),
0 2px 4px rgba(0, 0, 0, 0.2);
}
.color-input-picker .react-colorful__interactive:focus .react-colorful__pointer {
transform: translate(-50%, -50%) scale(1.15);
}
/* ── Ultrawide canvas ─────────────────────────────────────────────────────────
On ≥3xl screens the <AppShell> becomes a centered card (via its `3xl:*` classes
and the body flex/height it applies). This paints the aurora backdrop revealed
around the card, and re-anchors floating panels to the app zone edges. */
.ultrawide-canvas {
background-color: var(--canvas);
background-image:
radial-gradient(ellipse 70% 55% at 12% 18%, var(--canvas-glow-1), transparent 62%),
radial-gradient(ellipse 75% 55% at 88% 82%, var(--canvas-glow-2), transparent 62%);
background-attachment: fixed;
}
@layer utilities {
/* The floating <SidePanel> is fixed to the viewport. On ultrawide the app is a
centered card, so re-anchor the panel to the app-zone edges (+12px) instead
of the viewport — top AND bottom, so it never exceeds the app height. */
.ultrawide-canvas [data-slot="side-panel-content"][data-side="right"] {
right: calc((100dvw - var(--spacing-app-max-w)) / 2 + 12px) !important;
top: calc(max(0px, (100dvh - var(--spacing-app-max-h)) / 2) + 12px) !important;
bottom: calc(max(0px, (100dvh - var(--spacing-app-max-h)) / 2) + 12px) !important;
}
.ultrawide-canvas [data-slot="side-panel-content"][data-side="left"] {
left: calc((100dvw - var(--spacing-app-max-w)) / 2 + 12px) !important;
top: calc(max(0px, (100dvh - var(--spacing-app-max-h)) / 2) + 12px) !important;
bottom: calc(max(0px, (100dvh - var(--spacing-app-max-h)) / 2) + 12px) !important;
}
}
/* ─── Settings: naming one card ────────────────────────────────────────────
`SettingsLayout` takes you to the right tab, but a settings tab is a column of
cards that look alike: arriving does not yet say WHICH one you came for. The
ring names it for the length of a glance, then the layout removes the
attribute.
`box-shadow` rather than `outline`: it follows the card's radius and fades out
in a single property, and it pushes nothing — the column does not move by a
pixel for the two seconds it lasts.
No `fill-mode`: the landing frame is already transparent, and a frozen shadow
would stay a composited layer for nothing. */
@keyframes settings-section-focus {
0%,
55% {
box-shadow:
0 0 0 2px var(--ring),
0 0 0 7px color-mix(in oklab, var(--ring) 22%, transparent);
}
100% {
box-shadow:
0 0 0 2px transparent,
0 0 0 7px transparent;
}
}
[data-settings-focus] {
animation: settings-section-focus 2s cubic-bezier(0.16, 1, 0.3, 1);
}
/* "Less motion": we do not shorten it, we remove it. The ring itself STAYS — it
does not decorate, it points. */
@media (prefers-reduced-motion: reduce) {
[data-settings-focus] {
animation: none;
box-shadow:
0 0 0 2px var(--ring),
0 0 0 7px color-mix(in oklab, var(--ring) 22%, transparent);
}
}