Files
Katia Bulatova fbd6df33b4 feat(webapp): Themes + contrast settings update (#4206)
Adds System Preferences, Dark and Light themes, gated by the
`hasThemeSwitcher` feature flag (off by default — dark stays the default
theme for everyone).

Old theme is now "Classic"and set as default. 
"System preferences" theme has both Light and Dark modes and uses your
laptop settings to use a correct one.
It has less color accents (specifically less colored text), and they are
the same for both modes, only grayscale values change between them. And
Light/Dark themes can be used separately.

New Contrast setting is available for System Preferences, Dark and Light
themes - it changes the contrast for the whole app. All new visual
Settings live in Account.
2026-08-03 19:29:33 +02:00

992 lines
35 KiB
CSS

@import "react-grid-layout/css/styles.css" layer(base);
@import "react-resizable/css/styles.css" layer(base);
@import "tailwindcss";
@import "tw-animate-css";
@import "tailwind-scrollbar-hide/v4";
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography";
@plugin "tailwind-scrollbar";
/* Streamdown ships prebuilt Tailwind classes - scan it for used utilities */
@source "../node_modules/streamdown/dist";
/* Raw palettes and static tokens - theme-independent, never override per theme */
@theme {
--font-sans: "Geist Variable", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-mono: "Geist Mono Variable", monaco, Consolas, "Lucida Console", monospace;
--text-xxs: 0.65rem;
--text-xxs--line-height: 0.75rem;
--text-xxs--letter-spacing: -0.01em;
--text-xxs--font-weight: 500;
--text-2sm: 0.8125rem;
--text-2sm--line-height: 0.875rem;
--text-2sm--letter-spacing: -0.01em;
--text-2sm--font-weight: 500;
--color-charcoal-100: #e8e9ec;
--color-charcoal-200: #d7d9dd;
--color-charcoal-300: #b5b8c0;
--color-charcoal-400: #878c99;
--color-charcoal-500: #5f6570;
--color-charcoal-550: #4d525b;
--color-charcoal-600: #3b3e45;
--color-charcoal-650: #2c3034;
--color-charcoal-700: #272a2e;
--color-charcoal-750: #212327;
--color-charcoal-775: #1c1e21;
--color-charcoal-800: #1a1b1f;
--color-charcoal-850: #15171a;
--color-charcoal-900: #121317;
--color-charcoal-950: #0d0e12;
--color-charcoal-1000: #0b0c0f;
--color-apple-100: #e4ffc9;
--color-apple-200: #cfffa0;
--color-apple-300: #bfff81;
--color-apple-400: #afff62;
--color-apple-500: #a8ff53;
--color-apple-600: #82d134;
--color-apple-700: #6fb12f;
--color-apple-750: #5e932a;
--color-apple-800: #45711a;
--color-apple-850: #2e4e10;
--color-apple-900: #20370a;
--color-apple-950: #152506;
--color-mint-50: #f0fdf4;
--color-mint-100: #ddfbe6;
--color-mint-200: #bdf5d0;
--color-mint-300: #87eba9;
--color-mint-400: #4fd97e;
--color-mint-500: #28bf5c;
--color-mint-600: #1b9e48;
--color-mint-700: #197c3c;
--color-mint-800: #196233;
--color-mint-900: #16512c;
--color-mint-950: #062d15;
--color-sun-50: #fdfee8;
--color-sun-100: #fdffc2;
--color-sun-200: #ffff89;
--color-sun-300: #fff852;
--color-sun-400: #fdea12;
--color-sun-500: #eccf06;
--color-sun-600: #cca302;
--color-sun-700: #a37505;
--color-sun-800: #865b0d;
--color-sun-900: #724b11;
--color-sun-950: #432705;
--color-lavender-50: #f4f2ff;
--color-lavender-100: #eae8ff;
--color-lavender-200: #d7d4ff;
--color-lavender-300: #bab2ff;
--color-lavender-400: #826dff;
--color-lavender-500: #7655fd;
--color-lavender-600: #6532f5;
--color-lavender-700: #5620e1;
--color-lavender-800: #481abd;
--color-lavender-900: #3d189a;
--color-lavender-950: #230c69;
--text-shadow-custom: 1px 1px 1px rgba(0, 0, 0, 0.5);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-tile-scroll: tile-move 0.5s infinite linear;
--animate-tile-scroll-offset: tile-move-offset 0.5s infinite linear;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes tile-move {
0% {
background-position: 0px;
}
100% {
background-position: 8px;
}
}
@keyframes tile-move-offset {
0% {
background-position: -1px;
}
100% {
background-position: 7px;
}
}
}
/*
Semantic tokens - the themable layer. Utilities compile to var(--color-*),
so a theme overrides these vars in a scoped selector, e.g.
[data-theme="light"] { --color-text-bright: #1a1b1f; }
Defaults below are the original dark theme.
*/
@theme {
/* Text colors */
--color-primary: var(--color-apple-500);
--color-secondary: var(--color-charcoal-650);
--color-tertiary: var(--color-charcoal-700);
--color-text-link: var(--color-lavender-400);
--color-text-faint: var(--color-charcoal-500);
--color-text-dimmed: var(--color-charcoal-400);
--color-text-bright: var(--color-charcoal-200);
/* Surfaces, from deepest to most raised */
--color-background-deep: var(--color-charcoal-900);
--color-background-dimmed: var(--color-charcoal-850);
--color-background-bright: var(--color-charcoal-800);
--color-background-hover: var(--color-charcoal-750);
--color-background-raised: var(--color-charcoal-700);
--color-surface-control: var(--color-charcoal-600);
--color-surface-control-hover: var(--color-charcoal-550);
--color-surface-control-active: var(--color-charcoal-500);
--color-input-bg: var(--color-charcoal-750);
/* Borders, from subtlest to most visible */
--color-grid-dimmed: var(--color-charcoal-750);
--color-grid-bright: var(--color-charcoal-700);
--color-border-bright: var(--color-charcoal-600);
--color-border-brighter: var(--color-charcoal-550);
--color-border-brightest: var(--color-charcoal-500);
--color-success: var(--color-mint-500);
--color-pending: var(--color-blue-500);
--color-warning: var(--color-amber-500);
--color-error: var(--color-rose-600);
/* Environment colors */
--color-dev: var(--color-pink-500);
--color-staging: var(--color-orange-400);
--color-prod: var(--color-mint-500);
--color-preview: var(--color-yellow-400);
/* Icon colors */
--color-tasks: var(--color-blue-500);
--color-runs: var(--color-indigo-500);
--color-batches: var(--color-pink-500);
--color-schedules: var(--color-yellow-500);
--color-queues: var(--color-purple-500);
--color-query: var(--color-blue-500);
--color-metrics: var(--color-green-500);
--color-customDashboards: var(--color-charcoal-400);
--color-deployments: var(--color-blue-500);
--color-concurrency: var(--color-amber-500);
--color-limits: var(--color-purple-500);
--color-regions: var(--color-green-500);
--color-logs: var(--color-pink-500);
--color-tests: var(--color-pink-500);
--color-apiKeys: var(--color-amber-500);
--color-environmentVariables: var(--color-pink-500);
--color-alerts: var(--color-red-500);
--color-projectSettings: var(--color-blue-500);
--color-orgSettings: var(--color-blue-500);
--color-docs: var(--color-blue-500);
--color-bulkActions: var(--color-emerald-500);
--color-aiPrompts: var(--color-blue-500);
--color-aiMetrics: var(--color-green-500);
--color-errors: var(--color-amber-500);
--color-agents: var(--color-purple-500);
--color-sessions: var(--color-pink-500);
--color-playgrounds: var(--color-fuchsia-500);
--color-models: var(--color-violet-500);
--color-previewBranches: var(--color-yellow-500);
}
/* Callout variant accents (see components/primitives/Callout.tsx) */
@theme {
--color-callout-warning-text: var(--color-yellow-200);
--color-callout-error-text: var(--color-rose-200);
--color-callout-success-text: var(--color-green-200);
--color-callout-docs: var(--color-blue-400);
--color-callout-docs-text: var(--color-blue-200);
--color-callout-pending: var(--color-blue-400);
--color-callout-pending-bg: var(--color-blue-800);
--color-callout-pending-text: var(--color-blue-300);
--color-callout-pricing: var(--color-indigo-400);
--color-callout-pricing-bg: var(--color-indigo-800);
--color-callout-pricing-text: var(--color-indigo-300);
}
/*
Theme overrides. A theme re-maps ONLY the semantic tokens above, never the raw
palette values. That invariant is what lets a future [data-contrast] overlay
re-map semantic tokens on top of whichever theme is active.
*/
/*
System preference unified accents (dark+light); Classic keeps the original
set. One shared value per accent token, >=3:1 against both dark cards and
white. Accents whose Classic default already clears both modes (blue-500,
indigo-500, pink-500, purple-500, red-500, violet-500, fuchsia-500,
rose-600) are not repeated here. Text-sized tokens (text-link, callout
text) stay per-mode: no color reaches 4.5:1 on both #1a1b1f and #ffffff.
Dark derives everything else (monochrome surfaces etc.) from Classic.
*/
:is([data-theme="dark"], [data-theme="light"]) {
/* Status */
--color-success: var(--color-mint-600);
--color-warning: var(--color-amber-600);
/* Env set: pink dev and green prod as in Classic, preview moves to blue
(yellow's unified mid-tone reads muddy brown). Staging stays warm orange
and is the one per-mode env color - see the light block. */
--color-prod: var(--color-mint-600);
--color-preview: var(--color-blue-500);
/* Icons */
--color-schedules: var(--color-yellow-700);
/* Matches the preview env color */
--color-previewBranches: var(--color-blue-500);
--color-metrics: var(--color-green-600);
--color-regions: var(--color-green-600);
--color-aiMetrics: var(--color-green-600);
--color-bulkActions: var(--color-emerald-600);
--color-concurrency: var(--color-amber-600);
--color-errors: var(--color-amber-600);
--color-apiKeys: var(--color-amber-600);
/* Queue charts read blue here; Classic keeps the queues purple */
--color-queues-chart: var(--color-blue-500);
--color-queues-chart-ref: var(--color-charcoal-500);
/* Callout accents (callout text/bg tints stay per-mode) */
--color-callout-docs: var(--color-blue-500);
--color-callout-pending: var(--color-blue-500);
--color-callout-pricing: var(--color-indigo-500);
/* Amber run statuses: three distinct steps, Classic's dark-to-light order
kept (waiting-for-deploy < pending-version < paused) */
--color-run-waiting-for-deploy: var(--color-amber-700);
--color-run-pending-version: #c76508;
--color-run-paused: var(--color-amber-600);
/* Nudged under 3:1's ceiling on white; stays lighter than system-failure */
--color-run-timed-out: #ed5f74;
}
/* System themes drop decorative icon accents to monochrome; Classic keeps the
colored icons. side-menu-active-icon is set in SideMenuItem for the active
nav item; system-mono-icon marks section-header icons (e.g. the limits page). */
:is([data-theme="dark"], [data-theme="light"]) :is(.side-menu-active-icon, .system-mono-icon) {
color: var(--color-text-bright);
}
/* System themes: status/env labels follow the surrounding text color, only the
icon keeps its tint. Classic colors both. Set in EnvironmentLabel and the
status combo components. */
:is([data-theme="dark"], [data-theme="light"]) .system-mono-label {
color: inherit;
}
/* Tinted status chips respond to the contrast control: a ring in the chip's
own text color fades in as contrast rises, so the soft tint keeps its
footprint but the chip gains definition. Transparent at contrast 0;
Classic never sets the variable. Marker set by the chip components. */
:is([data-theme="dark"], [data-theme="light"]) .contrast-chip {
box-shadow: inset 0 0 0 1px
color-mix(in srgb, currentcolor calc(var(--theme-contrast, 0) * 70%), transparent);
}
/*
Code syntax palette - the trigger-dark highlight theme, shared by the shiki
theme (streamdown) and the prism theme (CodeBlock). Consumed from JS via
var(), so declared static to always be emitted.
*/
@theme static {
--color-code-background: #212327;
--color-code-foreground: #878c99;
--color-code-line-number: #484c54;
--color-code-plain: #d4d4d4;
--color-code-muted: #5f6570;
--color-code-comment: #6f736d;
--color-code-keyword: #e888f8;
--color-code-storage: #8271ed;
--color-code-type: #f770c6;
--color-code-function: #d9f07c;
--color-code-variable: #cccbff;
--color-code-constant: #9c9af2;
--color-code-language: #9b99ff;
--color-code-object-key: #8b89ff;
--color-code-string: #afec73;
--color-code-template-punctuation: #7a78ea;
--color-code-number: #b5cea8;
--color-code-builtin: #e5c07b;
--color-code-attribute: #c39eff;
--color-code-escape: #d7ba7d;
--color-code-regexp: #d16969;
--color-code-regexp-constant: #646695;
--color-code-invalid: #f44747;
--color-code-deleted: #f85149;
--color-code-jsx-text: #d7d9dd;
}
/*
CodeMirror editor palette (see components/code/codeMirrorTheme.ts).
Consumed from JS via var(), so declared static to always be emitted.
*/
@theme static {
--color-editor-background: var(--color-background-deep);
--color-editor-foreground: #abb2bf;
--color-editor-keyword: #c678dd;
--color-editor-name: #9b99ff;
--color-editor-function: #61afef;
--color-editor-constant: #d19a66;
--color-editor-type: #e5c07b;
--color-editor-operator: #56b6c2;
--color-editor-comment: #7d8799;
--color-editor-heading: #e06c75;
--color-editor-string: #afec73;
--color-editor-invalid: #ffffff;
--color-editor-panel-background: #21252b;
--color-editor-highlight-background: rgba(71, 85, 105, 0.2);
--color-editor-tooltip-background: #353a42;
--color-editor-selection: rgb(71 85 105);
--color-editor-cursor: #528bff;
--color-editor-search-match: #72a1ff59;
--color-editor-search-match-outline: #457dff;
--color-editor-search-match-selected: #6199ff2f;
--color-editor-selection-match: #aafe661a;
--color-editor-matching-bracket: rgba(18, 19, 23, 0.9);
--color-editor-matching-bracket-outline: rgba(81, 90, 107, 0.5);
--color-editor-fold-placeholder: #dddddd;
--color-editor-scrollbar-track-active: #131b2b;
--color-editor-scrollbar-thumb: #293649;
--color-editor-scrollbar-thumb-active: #3c4b62;
}
/* Queue chart series colors - queues purple in Classic, overridden to blue in
the System themes; the grey reference series lightens per mode there.
Consumed from JS via var(), so declared static. */
@theme static {
--color-queues-chart: var(--color-queues);
--color-queues-chart-ref: #4d525b;
}
/*
Run-status chart colors. In-between shades are intentional - statuses within
a family (blues, roses, charcoals) are evenly spaced so chart series stay
distinguishable. Referenced from JS via var(--color-run-*), so declared
static to always be emitted.
*/
@theme static {
--color-run-pending: var(--color-charcoal-500);
--color-run-delayed: #6b7580;
--color-run-pending-version: var(--color-warning);
--color-run-waiting-for-deploy: #d97706;
--color-run-executing: var(--color-pending);
--color-run-retrying-after-failure: #2f6fec;
--color-run-dequeued: #4d8ef5;
--color-run-waiting-to-resume: #555d67;
--color-run-paused: #fbbf24;
--color-run-canceled: #78828c;
--color-run-expired: #848d96;
--color-run-interrupted: #d52c4d;
--color-run-completed-successfully: var(--color-success);
--color-run-completed-with-errors: #de405c;
--color-run-system-failure: #e7536c;
--color-run-crashed: #cc193d;
--color-run-timed-out: #f0667b;
}
/*
shadcn/ui tokens used by streamdown's internals. Inline so the vars resolve
where the utility is used - .streamdown-container redefines them locally.
*/
@theme inline {
--color-background: var(--background, #121317);
--color-foreground: var(--foreground, #d7d9dd);
--color-muted: var(--muted, #1c1e21);
--color-muted-foreground: var(--muted-foreground, #878c99);
--color-border: var(--border, #2c3034);
--color-sidebar: var(--sidebar, #15171a);
--color-primary-foreground: var(--primary-foreground, #121317);
}
@custom-variant lg-height (@media (max-height: 750px));
@custom-variant md-height (@media (max-height: 600px));
/* dark: follows the app theme (data-theme on <html>), not the OS preference.
Classic and Dark are both dark-mode themes, so the variant matches both. */
@custom-variant dark (&:where([data-theme="dark"], [data-theme="classic"], [data-theme="dark"] *, [data-theme="classic"] *));
/* system: matches the System preference themes (Dark and Light) but never
Classic - for restyles that must leave Classic untouched. */
@custom-variant system (&:where([data-theme="dark"], [data-theme="light"], [data-theme="dark"] *, [data-theme="light"] *));
/* light: the Light theme only - for values that are fine on every dark theme
but illegible on white. Classic is never matched. */
@custom-variant light (&:where([data-theme="light"], [data-theme="light"] *));
@utility focus-custom {
&:focus-visible {
outline: 1px solid var(--color-text-link);
/* Inset the ring inside the box; at offset 0 the outline sits outside the border edge and gets
clipped by ancestors with overflow hidden/auto (scroll areas, the side menu grid). */
outline-offset: -1px;
border-radius: 5px;
}
}
@utility scrollbar-gutter-stable {
scrollbar-gutter: stable;
}
/* Fade the thumb in only while the pointer is in the scroll area. Uses
::-webkit-scrollbar (no scrollbar-width) so Chrome keeps a classic always-present
bar to reveal, not the auto-hiding overlay; Firefox falls back to standard props.
Thumb color is a registered @property so it can transition. */
@property --sm-sb-thumb {
syntax: "<color>";
initial-value: transparent;
inherits: true;
}
@utility scrollbar-thumb-on-hover {
transition: --sm-sb-thumb 200ms ease;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: var(--sm-sb-thumb);
/* transparent border + padding-box clip = thinner thumb, inset from the channel edges */
border: 2px solid transparent;
border-radius: 9999px;
background-clip: padding-box;
}
@supports (-moz-appearance: none) {
scrollbar-width: thin;
scrollbar-color: var(--sm-sb-thumb) transparent;
}
&:hover {
--sm-sb-thumb: var(--color-surface-control);
}
}
/* Shared stop list for the animated glow utilities below. The gradient itself
is assembled per-element so the animated --gradient-angle resolves there. */
:root {
--glow-gradient-stops: rgb(99 102 241), rgb(245 158 11), rgb(236 72 153), rgb(245 158 11),
rgb(99 102 241);
}
@utility animated-gradient-glow {
position: relative;
overflow: visible;
&::before {
content: "";
position: absolute;
inset: -8px;
z-index: -1;
background: conic-gradient(from var(--gradient-angle), var(--glow-gradient-stops));
border-radius: inherit;
animation: gradient-rotation 3s linear infinite;
pointer-events: none;
filter: blur(0.5rem);
opacity: 0.1;
}
}
@utility animated-gradient-glow-small {
position: relative;
overflow: visible;
&::before {
content: "";
position: absolute;
inset: -1px;
z-index: -1;
background: conic-gradient(from var(--gradient-angle), var(--glow-gradient-stops));
border-radius: inherit;
animation: gradient-rotation 3s linear infinite;
pointer-events: none;
filter: blur(0.2rem);
opacity: 0.3;
}
}
@property --gradient-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@keyframes gradient-rotation {
0% {
--gradient-angle: 0deg;
}
100% {
--gradient-angle: 360deg;
}
}
@layer base {
/* v4 defaults border color to currentcolor - keep the app-wide default from v3 */
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-grid-bright);
}
body {
@apply bg-background-dimmed text-text-dimmed;
font-feature-settings: "rlig" 1, "calt" 1;
}
/* Text selection styles */
::selection {
@apply bg-text-bright/30 text-text-bright;
}
/* Kept separate from ::selection on purpose: grouping them makes older
Firefox drop the whole rule. */
/* stylelint-disable-next-line selector-no-vendor-prefix */
::-moz-selection {
@apply bg-text-bright/30 text-text-bright;
}
/* Native form controls, scrollbars etc. follow the app theme */
:root {
color-scheme: dark;
}
[data-theme="light"] {
color-scheme: light;
}
/* shadcn charts: https://ui.shadcn.com/docs/components/chart#add-a-grid */
:root {
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
/* Classic and Dark share these HSL chart vars - they were the app-wide dark values. */
:is([data-theme="dark"], [data-theme="classic"]) {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
/* System preference unified chart set (dark+light): Classic's hues, with
chart-2/3 darkened to clear 3:1 on white. Classic keeps the set above. */
:is([data-theme="dark"], [data-theme="light"]) {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 40%;
--chart-3: 30 80% 47%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
/* Override react-grid-layout placeholder color (default is red) */
.react-grid-item.react-grid-placeholder {
background: rgb(99 102 241) !important; /* indigo-500 */
border-radius: 0.375rem !important; /* rounded-md */
}
/* Sidebar reorder grid: subtle placeholder */
.sidebar-reorder-grid .react-grid-item.react-grid-placeholder {
background: var(--color-background-raised) !important;
border-radius: 0.25rem;
opacity: 1 !important;
}
/* Sidebar reorder grid: only animate transform (vertical position), not width/height */
.sidebar-reorder-grid .react-grid-item {
transition: transform 200ms ease !important;
}
/* Override resize handle icon to white */
.react-resizable-handle {
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiNmZmZmZmYiLz48L2c+PC9zdmc+') !important;
}
.react-resizable-handle::after {
border-bottom-color: rgba(255, 255, 255, 0.4) !important;
border-right-color: rgba(255, 255, 255, 0.4) !important;
border-top-color: rgba(255, 255, 255, 0.4) !important;
border-left-color: rgba(255, 255, 255, 0.4) !important;
}
}
@layer components {
@supports (-webkit-hyphens: none) {
.safari-only {
@apply border-b border-grid-dimmed;
}
}
}
/* Streamdown markdown styling */
.streamdown-container {
/* Streamdown uses shadcn/ui CSS variables - derive them from our semantic
tokens so they follow the theme. They map Tailwind utility classes like
bg-background, bg-primary, etc. that streamdown uses internally for its
link safety modal, code blocks, and other interactive elements. */
--background: var(--color-background-deep);
--foreground: var(--color-text-bright);
--muted: var(--color-charcoal-775);
--muted-foreground: var(--color-text-dimmed);
--border: var(--color-border-bright);
--primary: var(--color-primary);
--primary-foreground: var(--color-background-deep);
--sidebar: var(--color-background-dimmed);
/* Code block styling */
& [data-code-block-container] {
@apply rounded-sm my-2 border-charcoal-700;
}
& [data-code-block] {
border-top: none;
}
& p {
@apply my-1;
}
& h1,
& h2,
& h3,
& h4,
& h5,
& h6 {
@apply font-semibold text-text-bright mt-2 mb-1;
}
& h1 {
@apply text-base;
}
& h2 {
@apply text-sm;
}
& h3,
& h4,
& h5,
& h6 {
@apply text-xs;
}
& ul,
& ol {
@apply ml-4 my-1;
}
& ul {
@apply list-disc;
}
& ol {
@apply list-decimal;
}
& li {
@apply my-0.5;
}
/* Inline code (not in pre blocks) */
& code:not(pre code) {
@apply bg-charcoal-700 px-1 py-0.5 rounded-sm text-text-bright font-mono;
}
& blockquote {
@apply border-l-2 border-charcoal-600 pl-3 my-2 italic;
}
& a {
@apply text-text-link hover:underline;
}
& strong {
@apply font-semibold text-text-bright;
}
& em {
@apply italic;
}
& hr {
@apply my-2 border-charcoal-600;
}
& table {
@apply w-full my-2 border-collapse;
}
& th,
& td {
@apply border border-charcoal-600 px-2 py-1 text-left;
}
& th {
@apply bg-charcoal-700 font-semibold;
}
& [data-code-block-header] {
@apply bg-charcoal-800 text-text-dimmed border-b border-charcoal-700;
}
/* Override the bg-muted/40 class to let inline styles work */
& [data-code-block] pre {
background-color: inherit !important;
@apply scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600;
}
& [data-code-block] pre code {
@apply bg-transparent;
}
& [data-code-block] .line {
@apply leading-relaxed;
}
}
/*
Light theme. Overrides the themable variables only; raw palettes stay put.
Code/editor values come from the trigger.light VS Code theme.
*/
[data-theme="light"] {
--color-secondary: #ffffff;
--color-input-bg: #ffffff;
/* shadcn vars consumed by charts/streamdown (tooltip cursor fill etc.) */
--background: #ffffff;
--foreground: #1a1b1f;
--muted: #eceef1;
--muted-foreground: #5f6570;
--border: #e2e4e9;
--sidebar: #f6f7f8;
--primary-foreground: #ffffff;
/* Text */
--color-primary: var(--color-apple-600);
--color-tertiary: #eef0f3;
--color-text-link: var(--color-lavender-600);
--color-text-faint: var(--color-charcoal-400);
--color-text-dimmed: var(--color-charcoal-500);
--color-text-bright: var(--color-charcoal-800);
/* Surfaces */
--color-background-deep: #f1f2f4;
--color-background-dimmed: #fbfbfc;
--color-background-bright: #ffffff;
--color-background-hover: #f2f3f5;
--color-background-raised: #e9eaee;
--color-surface-control: #dcdee3;
--color-surface-control-hover: #cfd2d9;
--color-surface-control-active: #b8bcc6;
/* Borders */
--color-grid-dimmed: #eceef1;
--color-grid-bright: #e2e4e9;
--color-border-bright: #d2d5db;
--color-border-brighter: #b9bdc7;
--color-border-brightest: #9ba1ad;
/* Status/env/icon accents live in the unified dark+light block above;
dev keeps its Classic pink-500, which passes on white too. */
/* Staging is the one per-mode env color: warm orange has no clean unified
mid-tone, so dark keeps Classic's bright orange-400 and light deepens it */
--color-staging: var(--color-orange-600);
/* Grey chart reference series, light enough not to dominate on white */
--color-queues-chart-ref: #c3c7cf;
/* Neutral run statuses: soft light grays for sparkbars and charts */
--color-run-pending: #ccd0d6;
--color-run-delayed: #d3d6db;
--color-run-waiting-to-resume: #c5c9d0;
--color-run-canceled: #d8dbdf;
--color-run-expired: #dee0e4;
/* Monochrome icon gray - per-mode, not part of the unified accent set */
--color-customDashboards: var(--color-charcoal-500);
/* Callout text/bg - deep tints instead of the dark theme's pastels; text
can't unify across modes (see the unified accents block) */
--color-callout-warning-text: var(--color-yellow-800);
--color-callout-error-text: var(--color-rose-700);
--color-callout-success-text: var(--color-green-800);
--color-callout-docs-text: var(--color-blue-800);
--color-callout-pending-bg: var(--color-blue-100);
--color-callout-pending-text: var(--color-blue-800);
--color-callout-pricing-bg: var(--color-indigo-100);
--color-callout-pricing-text: var(--color-indigo-800);
/* Code syntax - trigger.light */
--color-code-background: #ffffff;
--color-code-foreground: #333333;
--color-code-line-number: #a8a8ad;
--color-code-plain: #2e2e4b;
--color-code-muted: #333333;
--color-code-comment: #767a81;
--color-code-keyword: #b114d3;
--color-code-storage: #b114d3;
--color-code-type: #b114d3;
--color-code-function: #6532f5;
--color-code-variable: #404040;
--color-code-constant: #1e1e1e;
--color-code-language: #b114d3;
--color-code-object-key: #222222;
--color-code-string: #262626;
--color-code-template-punctuation: #0879e2;
--color-code-number: #262626;
--color-code-builtin: #3080e0;
--color-code-attribute: #222222;
--color-code-escape: #222222;
--color-code-regexp: #dc3545;
--color-code-regexp-constant: #5f6570;
--color-code-invalid: #dc3545;
--color-code-deleted: #dc3545;
--color-code-jsx-text: #2e2e4b;
/* CodeMirror - trigger.light */
--color-editor-background: #ffffff;
--color-editor-foreground: #2e2e4b;
--color-editor-keyword: #b114d3;
--color-editor-name: #197c3c;
--color-editor-function: #6532f5;
--color-editor-constant: #3080e0;
--color-editor-type: #2980b9;
--color-editor-operator: #333333;
--color-editor-comment: #767a81;
--color-editor-heading: #2c3e50;
--color-editor-string: #262626;
--color-editor-invalid: #dc3545;
--color-editor-panel-background: #f4f4f6;
--color-editor-highlight-background: #f8f8fa;
--color-editor-tooltip-background: #ffffff;
--color-editor-selection: #d9dce3;
--color-editor-cursor: #2e2e4b;
--color-editor-search-match: #0879e226;
--color-editor-search-match-outline: #0879e2;
--color-editor-search-match-selected: #0879e240;
--color-editor-selection-match: #e8e8ed;
--color-editor-matching-bracket: rgba(240, 241, 244, 0.9);
--color-editor-matching-bracket-outline: rgba(160, 166, 180, 0.5);
--color-editor-fold-placeholder: #555555;
--color-editor-scrollbar-track-active: #e8e9ec;
--color-editor-scrollbar-thumb: #c9ccd4;
--color-editor-scrollbar-thumb-active: #aeb3be;
}
/* Streamdown's muted surface has no semantic token (charcoal-775); theme it here */
[data-theme="light"] .streamdown-container {
--muted: #eceef1;
}
/* The timeline label shadow is a Classic legibility aid; the System themes
drop it entirely */
:is([data-theme="dark"], [data-theme="light"]) .text-shadow-custom {
text-shadow: none;
}
/* Neutral timeline points: invert to a light dot with a gray ring */
[data-theme="light"] .timeline-point.bg-surface-control-active {
border-color: var(--color-surface-control-active);
background-color: var(--color-background-bright);
}
/* Run timeline bars: no fade gradient on light */
[data-theme="light"] .timeline-span {
background-image: none;
}
/* On saturated bars the duration label keeps the dark-theme treatment,
but only when the bar is wide enough to contain the label — on narrow
bars the sticky label overflows onto the page background, where the
default dark-on-light text is correct. */
[data-theme="light"] .timeline-span.bg-success,
[data-theme="light"] .timeline-span.bg-error,
[data-theme="light"] .timeline-span.bg-blue-500 {
container-type: inline-size;
}
@container (min-width: 3.5rem) {
[data-theme="light"] .timeline-span.bg-success .text-shadow-custom,
[data-theme="light"] .timeline-span.bg-error .text-shadow-custom,
[data-theme="light"] .timeline-span.bg-blue-500 .text-shadow-custom {
color: #ffffff;
}
}
/* The table row-hover menu wraps the always-visible "Suggest a region" button
in a ring container; on light that ring doubles up with the button's own
border, so drop it here. */
[data-theme="light"] .suggest-region-cell > div > div {
box-shadow: none;
}
/*
Interface contrast (System themes only). --theme-contrast is 0..1, set on
<html> from the dashboard preference. It pulls the whole monochrome scale
apart: text mixes toward the mode's foreground, backgrounds toward its
depth, borders step up. Accents are untouched. srgb mixing keeps contrast 0
byte-identical to the base values, and Classic never reads the variable.
These blocks must stay below the [data-theme="light"] theme block so they
win the cascade.
*/
[data-theme="dark"] {
/* Text brightens toward white */
--color-text-bright: color-mix(in srgb, var(--color-charcoal-200), #fff calc(var(--theme-contrast, 0) * 100%));
--color-text-dimmed: color-mix(in srgb, var(--color-charcoal-400), #fff calc(var(--theme-contrast, 0) * 60%));
--color-text-faint: color-mix(in srgb, var(--color-charcoal-500), #fff calc(var(--theme-contrast, 0) * 45%));
/* Backgrounds deepen toward black, keeping their relative order */
--color-background-deep: color-mix(in srgb, var(--color-charcoal-900), #000 calc(var(--theme-contrast, 0) * 60%));
--color-background-dimmed: color-mix(in srgb, var(--color-charcoal-850), #000 calc(var(--theme-contrast, 0) * 55%));
--color-background-bright: color-mix(in srgb, var(--color-charcoal-800), #000 calc(var(--theme-contrast, 0) * 45%));
--color-background-hover: color-mix(in srgb, var(--color-charcoal-750), #000 calc(var(--theme-contrast, 0) * 35%));
--color-background-raised: color-mix(in srgb, var(--color-charcoal-700), #000 calc(var(--theme-contrast, 0) * 25%));
--color-input-bg: color-mix(in srgb, var(--color-charcoal-750), #000 calc(var(--theme-contrast, 0) * 35%));
/* Controls and borders step up toward white */
--color-surface-control: color-mix(in srgb, var(--color-charcoal-600), #fff calc(var(--theme-contrast, 0) * 12%));
--color-surface-control-hover: color-mix(in srgb, var(--color-charcoal-550), #fff calc(var(--theme-contrast, 0) * 14%));
--color-surface-control-active: color-mix(in srgb, var(--color-charcoal-500), #fff calc(var(--theme-contrast, 0) * 16%));
--color-grid-dimmed: color-mix(in srgb, var(--color-charcoal-750), #fff calc(var(--theme-contrast, 0) * 18%));
--color-grid-bright: color-mix(in srgb, var(--color-charcoal-700), #fff calc(var(--theme-contrast, 0) * 20%));
--color-border-bright: color-mix(in srgb, var(--color-charcoal-600), #fff calc(var(--theme-contrast, 0) * 24%));
--color-border-brighter: color-mix(in srgb, var(--color-charcoal-550), #fff calc(var(--theme-contrast, 0) * 27%));
--color-border-brightest: color-mix(in srgb, var(--color-charcoal-500), #fff calc(var(--theme-contrast, 0) * 30%));
}
[data-theme="light"] {
/* Text deepens toward black */
--color-text-bright: color-mix(in srgb, #1a1b1f, #000 calc(var(--theme-contrast, 0) * 100%));
--color-text-dimmed: color-mix(in srgb, var(--color-charcoal-500), #000 calc(var(--theme-contrast, 0) * 85%));
--color-text-faint: color-mix(in srgb, var(--color-charcoal-400), #000 calc(var(--theme-contrast, 0) * 70%));
/* On white the foregrounds carry the contrast: cards stay white while the
page-behind surfaces darken a touch so panels separate */
--color-background-deep: color-mix(in srgb, #f1f2f4, #000 calc(var(--theme-contrast, 0) * 10%));
--color-background-hover: color-mix(in srgb, #f2f3f5, #000 calc(var(--theme-contrast, 0) * 8%));
--color-background-raised: color-mix(in srgb, #e9eaee, #000 calc(var(--theme-contrast, 0) * 10%));
/* Controls and borders push hard toward black - this is where light-mode
contrast is actually visible */
--color-surface-control: color-mix(in srgb, #dcdee3, #000 calc(var(--theme-contrast, 0) * 25%));
--color-surface-control-hover: color-mix(in srgb, #cfd2d9, #000 calc(var(--theme-contrast, 0) * 28%));
--color-surface-control-active: color-mix(in srgb, #b8bcc6, #000 calc(var(--theme-contrast, 0) * 32%));
--color-grid-dimmed: color-mix(in srgb, #eceef1, #000 calc(var(--theme-contrast, 0) * 28%));
--color-grid-bright: color-mix(in srgb, #e2e4e9, #000 calc(var(--theme-contrast, 0) * 32%));
--color-border-bright: color-mix(in srgb, #d2d5db, #000 calc(var(--theme-contrast, 0) * 38%));
--color-border-brighter: color-mix(in srgb, #b9bdc7, #000 calc(var(--theme-contrast, 0) * 42%));
--color-border-brightest: color-mix(in srgb, #9ba1ad, #000 calc(var(--theme-contrast, 0) * 46%));
}
form:has(.unlock-hint-staging-env:hover) [data-unlock-target="staging-env"],
form:has(.unlock-hint-pull-prod:hover) [data-unlock-target="pull-prod"],
form:has(.unlock-hint-pull-stg:hover) [data-unlock-target="pull-stg"],
form:has(.unlock-hint-pull-preview:hover) [data-unlock-target="pull-preview"],
form:has(.unlock-hint-pull-dev:hover) [data-unlock-target="pull-dev"] {
border-radius: 0.25rem;
outline: 2px dashed var(--color-warning);
outline-offset: 2px;
}