bb551d1d59
- Sidebar: the hairline above the user block is inset to the content edges instead of running edge-to-edge (concept language). - User menu: the Discord community link renders the actual Discord mark (inlined simple-icons path, CC0) instead of lucide MessagesSquare. - Dialogs: open/close animation toned down, zoom 98 instead of 95 and 150ms instead of 200ms. - Settings modal: switching tabs no longer remounts the intercepted route (and replayed the whole open animation on every click). The rail now swaps sections via history.replaceState, which Next syncs into usePathname(), so the dialog stays mounted and tab switches are instant. Verified via Playwright: dialog DOM node survives three tab switches, URL tracks the section, Esc still closes back. - /chat loading: the shared dashboard skeleton stretched edge-to-edge in chat's full-bleed wrapper (chat's own layout is what suspends, so a chat/loading.tsx cannot catch it). The shared fallback is now route- aware and renders a two-pane chat silhouette for /chat. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
513 lines
15 KiB
CSS
513 lines
15 KiB
CSS
@import "tailwindcss";
|
|
@plugin "@tailwindcss/typography";
|
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
|
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
* Spacing scale: locked.
|
|
* Card padding: p-6. Section gap: space-y-8. Form/control gap: gap-4.
|
|
* Allowed Tailwind values: 1, 2, 3, 4, 6, 8, 10, 12. No 2.5 / 5.
|
|
* Sidebar width is md:w-64 (256px); main offset matches md:pl-64.
|
|
* ───────────────────────────────────────────────────────────────────────── */
|
|
|
|
:root {
|
|
color-scheme: light;
|
|
|
|
/* Sidebar column width (frame layout). Default for shells that don't
|
|
override it; the dashboard shell (#dash-shell) sets it inline from
|
|
user_preferences.ui_state (248px expanded / 64px collapsed rail) and
|
|
the nav toggle updates it client-side. aside and <main> both read the
|
|
variable so they stay in lockstep. */
|
|
--nav-w: 248px;
|
|
|
|
/* Grayscale Chrome Palette: paper white + warm beige */
|
|
|
|
--background: 0 0% 100%;
|
|
--foreground: 0 0% 9%;
|
|
|
|
/* Warm-toned frame behind the floating page panel (frame layout) */
|
|
--frame: 40 18% 96%;
|
|
|
|
--card: 0 0% 100%;
|
|
--card-foreground: 0 0% 9%;
|
|
|
|
--popover: 0 0% 100%;
|
|
--popover-foreground: 0 0% 9%;
|
|
|
|
--primary: 0 0% 9%;
|
|
--primary-foreground: 0 0% 100%;
|
|
|
|
--secondary: 40 11% 89%; /* Warm beige paper chip */
|
|
--secondary-foreground: 0 0% 9%;
|
|
|
|
--muted: 40 8% 93%;
|
|
--muted-foreground: 0 0% 40%;
|
|
|
|
--accent: 40 8% 93%;
|
|
--accent-foreground: 0 0% 9%;
|
|
|
|
--destructive: 6 36% 48%; /* Muted terracotta */
|
|
--destructive-foreground: 0 0% 100%;
|
|
|
|
--border: 45 5% 85%;
|
|
--input: 45 5% 85%;
|
|
--ring: 0 0% 9%;
|
|
|
|
/* Radius */
|
|
--radius: 0.5rem;
|
|
|
|
/* Success: Sage green */
|
|
--success: 155 25% 40%;
|
|
--success-foreground: 0 0% 100%;
|
|
|
|
/* Warning: Muted ochre */
|
|
--warning: 38 55% 50%;
|
|
--warning-foreground: 0 0% 9%;
|
|
|
|
/* Attn: darker ochre for the one-sentence attention line (.attn / AttnLine),
|
|
derived from --warning but AA-safe as small text on the page background. */
|
|
--attn: 38 50% 34%;
|
|
|
|
--warm-accent: 38 45% 52%;
|
|
|
|
/* Chart colors */
|
|
--chart-1: 155 25% 40%;
|
|
--chart-2: 6 36% 48%;
|
|
--chart-3: 38 55% 50%;
|
|
--chart-4: 0 0% 50%;
|
|
|
|
/* Custom tokens */
|
|
--font-display: var(--font-hedvig-serif), Georgia, serif;
|
|
--font-body: var(--font-geist-sans), system-ui, sans-serif;
|
|
|
|
/* Shadows: barely-there; chrome relies on hairline borders */
|
|
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.03), 0 1px 1px rgba(15, 23, 42, 0.02);
|
|
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.03);
|
|
--shadow-lg: 0 12px 40px rgba(15, 23, 42, 0.05);
|
|
|
|
/* Animation */
|
|
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
--duration-fast: 150ms;
|
|
--duration-base: 300ms;
|
|
--duration-slow: 500ms;
|
|
}
|
|
|
|
.dark {
|
|
color-scheme: dark;
|
|
|
|
--background: 0 0% 7%;
|
|
--foreground: 0 0% 87%;
|
|
|
|
--frame: 0 0% 5%;
|
|
|
|
--card: 0 0% 11%;
|
|
--card-foreground: 0 0% 87%;
|
|
|
|
--popover: 0 0% 11%;
|
|
--popover-foreground: 0 0% 87%;
|
|
|
|
--primary: 0 0% 87%;
|
|
--primary-foreground: 0 0% 7%;
|
|
|
|
--secondary: 0 0% 14%;
|
|
--secondary-foreground: 0 0% 87%;
|
|
|
|
--muted: 0 0% 16%;
|
|
--muted-foreground: 0 0% 50%;
|
|
|
|
--accent: 0 0% 16%;
|
|
--accent-foreground: 0 0% 87%;
|
|
|
|
--destructive: 6 32% 56%;
|
|
--destructive-foreground: 0 0% 100%;
|
|
|
|
--border: 45 4% 18%;
|
|
--input: 45 4% 18%;
|
|
--ring: 0 0% 87%;
|
|
|
|
--success: 155 22% 48%;
|
|
--success-foreground: 0 0% 100%;
|
|
|
|
--warning: 38 50% 55%;
|
|
--warning-foreground: 38 50% 90%;
|
|
|
|
--attn: 38 45% 62%;
|
|
|
|
--warm-accent: 38 42% 58%;
|
|
|
|
--chart-1: 155 22% 48%;
|
|
--chart-2: 6 32% 56%;
|
|
--chart-3: 38 50% 55%;
|
|
--chart-4: 0 0% 55%;
|
|
|
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
|
|
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
|
|
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.45);
|
|
}
|
|
|
|
@theme inline {
|
|
--color-background: hsl(var(--background));
|
|
--color-foreground: hsl(var(--foreground));
|
|
--color-frame: hsl(var(--frame));
|
|
--color-card: hsl(var(--card));
|
|
--color-card-foreground: hsl(var(--card-foreground));
|
|
--color-popover: hsl(var(--popover));
|
|
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
--color-primary: hsl(var(--primary));
|
|
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
--color-secondary: hsl(var(--secondary));
|
|
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
--color-muted: hsl(var(--muted));
|
|
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
--color-accent: hsl(var(--accent));
|
|
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
--color-destructive: hsl(var(--destructive));
|
|
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
--color-border: hsl(var(--border));
|
|
--color-input: hsl(var(--input));
|
|
--color-ring: hsl(var(--ring));
|
|
--color-success: hsl(var(--success));
|
|
--color-success-foreground: hsl(var(--success-foreground));
|
|
--color-warning: hsl(var(--warning));
|
|
--color-warning-foreground: hsl(var(--warning-foreground));
|
|
--color-attn: hsl(var(--attn));
|
|
--color-warm-accent: hsl(var(--warm-accent));
|
|
--radius-lg: var(--radius);
|
|
--radius-md: calc(var(--radius) - 2px);
|
|
--radius-sm: calc(var(--radius) - 4px);
|
|
--font-sans: var(--font-body);
|
|
--font-mono: var(--font-geist-mono), monospace;
|
|
--font-serif: var(--font-display);
|
|
}
|
|
|
|
* {
|
|
border-color: hsl(var(--border));
|
|
}
|
|
|
|
|
|
body {
|
|
background-color: hsl(var(--background));
|
|
color: hsl(var(--foreground));
|
|
font-family: var(--font-body);
|
|
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
|
|
/* Display typography - Fraunces */
|
|
.font-display {
|
|
font-family: var(--font-display);
|
|
font-optical-sizing: auto;
|
|
}
|
|
|
|
/* Layered so utility classes (e.g. font-sans on a pane title) can override:
|
|
unlayered element rules would beat Tailwind's layered utilities. */
|
|
@layer base {
|
|
h1, h2, h3 {
|
|
font-family: var(--font-display);
|
|
font-weight: 500;
|
|
letter-spacing: -0.015em;
|
|
font-optical-sizing: auto;
|
|
}
|
|
}
|
|
|
|
/* Number styling - tabular for alignment */
|
|
.tabular-nums {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* Text balance for better line wrapping on descriptions */
|
|
.text-balance {
|
|
text-wrap: balance;
|
|
}
|
|
|
|
/* Safe area padding for mobile devices with notch/home indicator */
|
|
.safe-area-bottom {
|
|
padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
|
|
}
|
|
|
|
/* Main content padding: clears bottom nav + safe area on mobile */
|
|
.safe-area-main-padding {
|
|
padding-bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
|
|
}
|
|
|
|
/* Animation Keyframes */
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(16px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes slideInLeft {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-8px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes scaleIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.96);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes gentlePulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.6; }
|
|
}
|
|
|
|
/* Three-dot "typing" indicator used by the agent chat pre-token bubble.
|
|
Each dot rides the same wave but with staggered animation-delay so the
|
|
bounce reads left-to-right. */
|
|
@keyframes typingDot {
|
|
0%, 60%, 100% { transform: translateY(0); opacity: 0.35; }
|
|
30% { transform: translateY(-3px); opacity: 1; }
|
|
}
|
|
|
|
.animate-typing-dot {
|
|
animation: typingDot 1.1s ease-in-out infinite;
|
|
}
|
|
|
|
/* Marching-ants dashed border used by the import entity selector. */
|
|
@keyframes marchingAnts {
|
|
to { stroke-dashoffset: -14; }
|
|
}
|
|
|
|
.animate-marching-ants {
|
|
animation: marchingAnts 1.2s linear infinite;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.animate-typing-dot {
|
|
animation: gentlePulse 1.6s ease-in-out infinite;
|
|
transform: none;
|
|
}
|
|
.animate-marching-ants {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
* Enter/exit animation utilities (animate-in / animate-out vocabulary).
|
|
* The codebase uses the shadcn/tailwindcss-animate classes but no animate
|
|
* plugin was ever installed, so Tailwind v4 silently dropped them all:
|
|
* every popover, dialog and slide-over appeared instantly. These implement
|
|
* the exact subset in use, plugin-free. The duration and ease utilities
|
|
* compose via Tailwind v4's --tw-duration and --tw-ease variables; the
|
|
* global prefers-reduced-motion block collapses them like everything else.
|
|
* ───────────────────────────────────────────────────────────────────────── */
|
|
@keyframes accEnter {
|
|
from {
|
|
opacity: var(--tw-enter-opacity, 1);
|
|
transform: translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0)
|
|
scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), 1);
|
|
}
|
|
}
|
|
@keyframes accExit {
|
|
to {
|
|
opacity: var(--tw-exit-opacity, 1);
|
|
transform: translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0)
|
|
scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), 1);
|
|
}
|
|
}
|
|
@utility animate-in {
|
|
animation-name: accEnter;
|
|
animation-duration: var(--tw-duration, 150ms);
|
|
animation-timing-function: var(--tw-ease, cubic-bezier(0.32, 0.72, 0, 1));
|
|
animation-fill-mode: both;
|
|
}
|
|
@utility animate-out {
|
|
animation-name: accExit;
|
|
animation-duration: var(--tw-duration, 150ms);
|
|
animation-timing-function: var(--tw-ease, cubic-bezier(0.32, 0.72, 0, 1));
|
|
animation-fill-mode: both;
|
|
}
|
|
@utility fade-in { --tw-enter-opacity: 0; }
|
|
@utility fade-in-0 { --tw-enter-opacity: 0; }
|
|
@utility fade-out { --tw-exit-opacity: 0; }
|
|
@utility fade-out-0 { --tw-exit-opacity: 0; }
|
|
@utility zoom-in-95 { --tw-enter-scale: 0.95; }
|
|
@utility zoom-out-95 { --tw-exit-scale: 0.95; }
|
|
@utility zoom-in-98 { --tw-enter-scale: 0.98; }
|
|
@utility zoom-out-98 { --tw-exit-scale: 0.98; }
|
|
@utility slide-in-from-top { --tw-enter-translate-y: -100%; }
|
|
@utility slide-in-from-top-1 { --tw-enter-translate-y: -0.25rem; }
|
|
@utility slide-in-from-top-2 { --tw-enter-translate-y: -0.5rem; }
|
|
@utility slide-in-from-top-full { --tw-enter-translate-y: -100%; }
|
|
@utility slide-in-from-bottom { --tw-enter-translate-y: 100%; }
|
|
@utility slide-in-from-bottom-1 { --tw-enter-translate-y: 0.25rem; }
|
|
@utility slide-in-from-bottom-2 { --tw-enter-translate-y: 0.5rem; }
|
|
@utility slide-in-from-left { --tw-enter-translate-x: -100%; }
|
|
@utility slide-in-from-left-1 { --tw-enter-translate-x: -0.25rem; }
|
|
@utility slide-in-from-left-2 { --tw-enter-translate-x: -0.5rem; }
|
|
@utility slide-in-from-right { --tw-enter-translate-x: 100%; }
|
|
@utility slide-in-from-right-2 { --tw-enter-translate-x: 0.5rem; }
|
|
@utility slide-in-from-right-full { --tw-enter-translate-x: 100%; }
|
|
@utility slide-out-to-top { --tw-exit-translate-y: -100%; }
|
|
@utility slide-out-to-bottom { --tw-exit-translate-y: 100%; }
|
|
@utility slide-out-to-left { --tw-exit-translate-x: -100%; }
|
|
@utility slide-out-to-right { --tw-exit-translate-x: 100%; }
|
|
@utility slide-out-to-right-full { --tw-exit-translate-x: 100%; }
|
|
|
|
/* Staggered entrance animation */
|
|
.stagger-enter > * {
|
|
animation: slideUp var(--duration-slow) var(--ease-out) both;
|
|
}
|
|
.stagger-enter > *:nth-child(1) { animation-delay: 0ms; }
|
|
.stagger-enter > *:nth-child(2) { animation-delay: 80ms; }
|
|
.stagger-enter > *:nth-child(3) { animation-delay: 160ms; }
|
|
.stagger-enter > *:nth-child(4) { animation-delay: 240ms; }
|
|
.stagger-enter > *:nth-child(5) { animation-delay: 320ms; }
|
|
.stagger-enter > *:nth-child(6) { animation-delay: 400ms; }
|
|
.stagger-enter > *:nth-child(7) { animation-delay: 480ms; }
|
|
.stagger-enter > *:nth-child(8) { animation-delay: 560ms; }
|
|
.stagger-enter > *:nth-child(9) { animation-delay: 640ms; }
|
|
.stagger-enter > *:nth-child(10) { animation-delay: 720ms; }
|
|
|
|
/* Utility animations */
|
|
.animate-fade-in {
|
|
animation: fadeIn var(--duration-base) var(--ease-out);
|
|
}
|
|
|
|
.animate-slide-up {
|
|
animation: slideUp var(--duration-slow) var(--ease-out);
|
|
}
|
|
|
|
.animate-scale-in {
|
|
animation: scaleIn var(--duration-base) var(--ease-out);
|
|
}
|
|
|
|
/* Soft focus states */
|
|
.focus-ring:focus-visible {
|
|
outline: none;
|
|
box-shadow: 0 0 0 2px hsl(var(--background)),
|
|
0 0 0 4px hsl(var(--primary) / 0.35);
|
|
}
|
|
|
|
/* Overlay-style scrollbars: invisible at rest, revealed only while the
|
|
container is actually scrolling. ScrollbarReveal (mounted in the root
|
|
layout) toggles .is-scrolling on the scrolled element; the thumb fades
|
|
back out when scrolling stops. The gutter is reserved either way, so
|
|
revealing never shifts layout. */
|
|
::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: transparent;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.is-scrolling::-webkit-scrollbar-thumb {
|
|
background: hsl(var(--muted-foreground) / 0.25);
|
|
}
|
|
|
|
@supports (scrollbar-color: auto) {
|
|
/* Layered: the universal transition must not beat Tailwind's layered
|
|
transition-* utilities (unlayered rules win over layers regardless of
|
|
specificity: this silently killed every width/margin/color transition
|
|
in the app). scrollbar-color still animates via the base default. */
|
|
@layer base {
|
|
* {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: transparent transparent;
|
|
transition: scrollbar-color 300ms ease-out;
|
|
}
|
|
}
|
|
.is-scrolling {
|
|
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
|
|
}
|
|
}
|
|
|
|
/* Hover-reveal fallback for touch: coarse pointers have no hover, so the
|
|
controls that desktop reveals on hover (row checkboxes, chevrons, quiet
|
|
row actions) stay visible. Targets exactly the opacity-0 +
|
|
group-hover:opacity-100 pairing and nothing else. */
|
|
@media (pointer: coarse) {
|
|
.group .opacity-0.group-hover\:opacity-100 {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
/* Always-visible thumb for modal scroll containers (DialogContent,
|
|
SlideOverBody): inside an overlay the scrollbar is the only cue that
|
|
content continues below the fold, so the at-rest auto-hide would read
|
|
as "the dialog is cut off". */
|
|
.scrollbar-visible::-webkit-scrollbar-thumb {
|
|
background: hsl(var(--muted-foreground) / 0.25);
|
|
}
|
|
@supports (scrollbar-color: auto) {
|
|
.scrollbar-visible {
|
|
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
|
|
}
|
|
}
|
|
|
|
/* Hide scrollbar utility */
|
|
.scrollbar-hide {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
.scrollbar-hide::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
/* Hide number input spinners */
|
|
input[type="number"]::-webkit-inner-spin-button,
|
|
input[type="number"]::-webkit-outer-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
input[type="number"] {
|
|
-moz-appearance: textfield;
|
|
}
|
|
|
|
/* Selection color */
|
|
::selection {
|
|
background: hsl(var(--primary) / 0.15);
|
|
color: hsl(var(--foreground));
|
|
}
|
|
|
|
.dark ::selection {
|
|
background: hsl(var(--primary) / 0.3);
|
|
color: hsl(var(--foreground));
|
|
}
|
|
|
|
/* Respect reduced motion preference */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*, *::before, *::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
}
|
|
|
|
.stagger-enter > * {
|
|
animation: none;
|
|
opacity: 1;
|
|
}
|
|
}
|