Files
openclaw/ui/src/styles/base.css
T
Peter Steinberger 3dff1727b1 fix(control-ui): give chat links a contrast-safe color token (#123824)
chat links used --accent while user bubbles are filled from --accent-subtle (and peer bubbles from a per-sender hue), so links sat on their own hue at 3.95:1 worst case. New --link/--link-hover tokens derive from each palette's audited --accent-hover and clear WCAG AA on every bubble surface and sender hue; the hover opacity fade, which lowered contrast, is replaced by a color shift.
2026-08-14 14:03:28 -07:00

1085 lines
31 KiB
CSS

:root {
/* Background - Deep, rich dark with layered depth */
--bg: #0e1015;
--bg-accent: #13151b;
--bg-elevated: #191c24;
--bg-hover: #1f2330;
--bg-muted: #1f2330;
/* Card / Surface - Clear hierarchy between levels */
--card: #161920;
--card-foreground: #f0f0f2;
--card-highlight: rgba(255, 255, 255, 0.04);
--popover: #191c24;
--popover-foreground: #f0f0f2;
/* Panel */
--panel: #0e1015;
--panel-strong: #191c24;
--panel-hover: #1f2330;
--chrome: rgba(14, 16, 21, 0.96);
--chrome-strong: rgba(14, 16, 21, 0.98);
/* Text - Clean contrast
* Keep --muted AA on dark surfaces; do not dim muted body text with opacity.
* --text targets ~10:1 on --bg #0e1015 (10.1; 9.3 on --card) — the dark
* comfort band is ~10-12 and brighter body text halates on dark. */
--text: #bcbcc0;
--text-strong: #f4f4f5;
--chat-text: #bcbcc0;
--muted: #8b8b94;
--muted-strong: #898990;
--muted-foreground: #8b8b94;
/* Border - Whisper-thin, barely there */
--border: #1e2028;
--border-strong: #2e3040;
--border-hover: #3e4050;
--input: #1e2028;
--ring: #ff5c5c;
/* Shared rail chrome. Light and dark themes inherit their actual colors
through --border/--accent while keeping one geometry contract. */
--rail-header-height: 48px;
--rail-header-padding-start: 12px;
--rail-header-padding-end: 8px;
--rail-header-action-size: 28px;
--rail-header-action-glyph-size: 16px;
--rail-header-action-color: var(--muted);
--rail-header-action-hover-color: var(--text);
--rail-header-action-active-color: var(--accent);
--rail-header-action-disabled-opacity: 0.4;
--rail-header-action-gap: 2px;
--rail-header-copy-gap: 2px;
--rail-header-background: var(--bg);
--rail-header-eyebrow-size: var(--control-ui-text-xs);
--rail-header-eyebrow-letter-spacing: 0.04em;
--rail-header-title-size: var(--control-ui-text-sm);
--rail-header-title-weight: 600;
--rail-divider-color: var(--border);
--rail-divider-size: 1px;
--rail-divider-active-size: 2px;
--rail-resizer-size: 4px;
/*
* Accent - Punchy signature red
*
* WCAG 2.1 AA audit (bg = #0e1015, relative luminance ≈ 0.005):
* --accent #ff5c5c on bg: 6.3:1 AA text ✓
* --primary-foreground #ffffff on #d13c3c button: 4.75:1 AA ✓
* --primary-foreground #ffffff on #c22e2e hover: 5.64:1 AA ✓
* --destructive-foreground #fafafa on #d32f2f button: 4.77:1 AA ✓
* --destructive-foreground #fafafa on #bc2a2a hover: 5.74:1 AA ✓
* --card/--popover/--secondary foreground pairs: 15.4:1 / 14.9:1 / 15.4:1 ✓
* --selection-fg #ffffff on --selection-bg #005fcc: 6.0:1 AA ✓
*/
--accent: #ff5c5c;
--accent-hover: #ff7070;
--accent-muted: #ff5c5c;
--accent-subtle: rgba(255, 92, 92, 0.1);
--accent-foreground: #fafafa;
--accent-glow: rgba(255, 92, 92, 0.2);
/* Link text keeps the accent hue but cannot be --accent: chat paints user
bubbles from that same token, so a link there fights its own background.
Derived, not per-theme — --accent-hover is every palette's audited lifted
accent and custom themes derive it too, and the extra 15% of --text buys
the margin the sender-tinted peer bubbles need (the lightest tint hue on
openknot sits at 4.3:1 against bare --accent-hover). Both steps move toward
the text color, which raises contrast in light and dark alike, so hover is
feedback that never costs readability. */
--link: color-mix(in srgb, var(--accent-hover) 85%, var(--text));
--link-hover: color-mix(in srgb, var(--link) 82%, var(--text));
--selection-bg: #005fcc;
--selection-fg: #ffffff;
--primary: #d13c3c;
--primary-hover: #c22e2e;
--primary-foreground: #ffffff;
/* Secondary */
--secondary: #161920;
--secondary-foreground: #f0f0f2;
--accent-2: #14b8a6;
--accent-2-muted: rgba(20, 184, 166, 0.7);
--accent-2-subtle: rgba(20, 184, 166, 0.1);
/* Theme-invariant presentation colors. */
--button-icon-bg: #ffffff;
--brand-logo-chip-bg: #ffffff;
--qr-bg: #ffffff;
--qr-text: #444444;
--media-bg: #000000;
--media-foreground: #ffffff;
--exec-highlight: #f97316;
--moon-highlight: #fef3c7;
--moon-base: #fbbf24;
--workshop-warm: #ff8a5c;
--workshop-teal: #14b8a6;
--workshop-cyan: #0891b2;
/* Provider and product brand colors never follow the active UI theme. */
--brand-codex: #10a37f;
--brand-claude: #d97757;
--brand-google: #4285f4;
/*
* Semantic status
*
* Each `--x` doubles as label text on its own `--x-subtle` tint, so the pair
* is one contrast decision: the 8% tint composites onto the surface and the
* label must still clear WCAG AA 4.5:1 on the lightest dark surface
* (dash `--card` #221a16, worse than claw `--card` #161920). Keep every
* `-subtle`/`-muted` rgba in sync with its base hex — they are the same
* color at 8%/75%, and re-tinting one without the other silently drops the
* pair below AA. Bases stay literal hex: `widget-theme.ts` publishes them to
* MCP app guest documents, where a `color-mix(... var(--x) ...)` string
* would not resolve.
*
* WCAG 2.1 AA audit (label on own subtle tint, worst of --card / --bg):
* --ok #22c55e 6.66:1 ✓ --warn #f59e0b 6.93:1 ✓
* --danger #f87171 5.52:1 ✓ --info #60a5fa 5.97:1 ✓
*
* Bounded to --card and --bg. A status label can also land on --bg-hover,
* --bg-muted, --panel-hover or --input, which this audit does not cover.
*/
--ok: #22c55e;
--ok-muted: rgba(34, 197, 94, 0.75);
--ok-subtle: rgba(34, 197, 94, 0.08);
--destructive: #ef4444;
--destructive-foreground: #fafafa;
--warn: #f59e0b;
--warn-muted: rgba(245, 158, 11, 0.75);
--warn-subtle: rgba(245, 158, 11, 0.08);
--danger: #f87171;
--danger-muted: rgba(248, 113, 113, 0.75);
--danger-subtle: rgba(248, 113, 113, 0.08);
--info: #60a5fa;
--info-subtle: rgba(96, 165, 250, 0.08);
--warn-bright: #f59e0b;
--warn-strong: #d97706;
--danger-solid-hover: #dc2626;
--pr-merged: #a78bfa;
/* Run state needs its own token: --accent is red in every shipped theme and
sits on top of --danger, so a spinning ring reads as a failed session.
Graphical indicator, judged against WCAG 1.4.11 (3:1), not the label audit
above: #14b8a6 is 7.7:1 on --bg. */
--run: #14b8a6;
/* Focus */
--focus: rgba(255, 92, 92, 0.2);
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 80%, transparent);
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 16px var(--accent-glow);
/* Grid */
--grid-line: rgba(255, 255, 255, 0.03);
/* GitHub syntax palette shared by highlighted code and file previews. */
--hljs-keyword: #ff8a80;
--hljs-number: #79c0ff;
--hljs-string: #a5d6ff;
--hljs-title: #d2a8ff;
--hljs-type: #ffa657;
--hljs-attribute: #7ee787;
--hljs-meta: #c9d1d9;
--hljs-deletion: #ffa198;
--hljs-addition: #7ee787;
/* Theme transition */
--theme-switch-x: 50%;
--theme-switch-y: 50%;
/* Typography */
--mono:
"JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
--font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-display: var(--font-body);
--control-ui-text-scale: 1;
--control-ui-text-xs: calc(11px * var(--control-ui-text-scale));
--control-ui-text-sm: calc(12px * var(--control-ui-text-scale));
--control-ui-text-md: calc(14px * var(--control-ui-text-scale));
--control-ui-text-lg: calc(16px * var(--control-ui-text-scale));
--control-ui-input-text-size: max(16px, calc(14px * var(--control-ui-text-scale)));
--chat-text-size: var(--control-ui-text-md);
/* Shadows - Subtle, layered depth */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.4);
--shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.5);
--shadow-glow: 0 0 24px var(--accent-glow);
/* Spacing scale. Settings surfaces use these (see
ui/docs/design-system/settings-design.md); elsewhere raw px padding/gap is
grandfathered — adopt the scale when rewriting a rule, don't mass-migrate. */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-7: 32px;
--space-8: 40px;
/* One inset shared by every widget frame (chat preview panel, board card)
so agent-authored widget documents meet identical breathing room on all
surfaces. Board layout math mirrors this value numerically. */
--widget-frame-inset: var(--space-3);
/* Radii - Slightly larger for modern feel */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 20px;
--radius-full: 9999px;
--radius: 10px;
/* Menu items stay optically parallel to their panel edge: radius 6 + padding 4
= panel radius 10. Shared knobs keep transient menus from drifting again. */
--menu-radius: var(--radius-md);
--menu-item-radius: var(--radius-sm);
--menu-item-height: 28px;
--menu-padding: 4px;
/* Transitions - Crisp and responsive */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration-fast: 100ms;
--duration-normal: 180ms;
--duration-slow: 300ms;
--safe-area-top: env(safe-area-inset-top, 0px);
--safe-area-right: env(safe-area-inset-right, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-left: env(safe-area-inset-left, 0px);
/* Layering */
--z-dropdown: 100;
--z-toast: 2000;
/* Custom select chevron. Data URIs cannot read var(--muted), so the stroke
color is baked per theme mode; light mode overrides the whole image. */
--select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23a1a1aa' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
color-scheme: dark;
}
/* Menu actions follow the platform 44px touch-target contract for coarse pointers. */
@media (pointer: coarse) {
:root {
--menu-item-height: 44px;
}
}
/* The claw family deepens only the text-bearing destructive fill. */
:root[data-theme="dark"] {
--destructive: #d32f2f;
--destructive-hover: #bc2a2a;
}
/* Light theme tokens apply to every light-mode family. */
:root[data-theme-mode="light"] {
/*
* Warm paper light mode - terracotta accent on ivory
*
* WCAG 2.1 AA audit (bg = #faf9f7, relative luminance ≈ 0.941):
* --accent #bd4531 contrast ≈ 4.9:1 AA text ✓
* --primary-foreground #ffffff on #bd4531 button: 5.2:1 AA ✓
* --text #403c35 ≈ 10.4:1 AAA ✓, --muted #6e6960 ≈ 5.2:1 AA ✓
*/
--bg: #faf9f7;
--bg-accent: #f4f1ec;
--bg-elevated: #ffffff;
--bg-hover: #efebe4;
--bg-muted: #efebe4;
--bg-content: #f4f1ec;
--card: #ffffff;
--card-foreground: #211e1a;
--card-highlight: rgba(60, 42, 24, 0.03);
--popover: #ffffff;
--popover-foreground: #211e1a;
--panel: #faf9f7;
--panel-strong: #f4f1ec;
--panel-hover: #e9e4dc;
--chrome: rgba(250, 249, 247, 0.96);
--chrome-strong: rgba(250, 249, 247, 0.98);
--text: #403c35;
--text-strong: #211e1a;
--chat-text: #403c35;
--muted: #6e6960;
--muted-strong: #56524a;
--muted-foreground: #6e6960;
--border: #e8e4dc;
--border-strong: #d6d0c5;
--border-hover: #b4ac9e;
--input: #e8e4dc;
--ring: #bd4531;
--accent: #bd4531;
--accent-hover: #a83c29;
--accent-muted: #bd4531;
--accent-subtle: rgba(189, 69, 49, 0.08);
--accent-foreground: #ffffff;
--accent-glow: rgba(189, 69, 49, 0.12);
--primary: #bd4531;
--primary-foreground: #ffffff;
--secondary: #f4f1ec;
--secondary-foreground: #403c35;
--accent-2: #0d9488;
--accent-2-muted: rgba(13, 148, 136, 0.75);
--accent-2-subtle: rgba(13, 148, 136, 0.08);
/*
* Semantic status — see the `:root` block for the pairing rule. Light mode
* is bounded by `--bg` (#faf9f7, and the darker dash-light #f7f2ec) rather
* than the white `--card`, so a token that passes on a card can still fail
* on the page behind it.
*
* WCAG 2.1 AA audit (label on own subtle tint, worst of --card / --bg):
* --ok #166534 5.70:1 ✓ --warn #92400e 5.66:1 ✓
* --danger #b91c1c 5.10:1 ✓ --info #1d4ed8 5.34:1 ✓
*
* Bounded to --card and --bg. Hover and input surfaces are not covered, and
* one gap is reachable: .plugins-row-message--error sits on --danger-subtle
* over a hovered row, which is 4.28:1 under dash-light. That is an
* improvement on the 3.23:1 it replaced, not a regression, but it is not AA.
*/
--ok: #166534;
--ok-muted: rgba(22, 101, 52, 0.75);
--ok-subtle: rgba(22, 101, 52, 0.08);
--destructive: #dc2626;
--destructive-foreground: #fafafa;
--warn: #92400e;
--warn-muted: rgba(146, 64, 14, 0.75);
--warn-subtle: rgba(146, 64, 14, 0.08);
--danger: #b91c1c;
--danger-muted: rgba(185, 28, 28, 0.75);
--danger-subtle: rgba(185, 28, 28, 0.08);
--info: #1d4ed8;
--info-subtle: rgba(29, 78, 216, 0.08);
--pr-merged: #7c3aed;
/* See the dark-theme note: run state stays off --accent. #0d9488 is 3.6:1 on
--bg, above the 3:1 WCAG 1.4.11 bar for a non-text indicator. */
--run: #0d9488;
--focus: rgba(189, 69, 49, 0.15);
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 70%, transparent);
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 12px var(--accent-glow);
--grid-line: rgba(60, 42, 24, 0.05);
--hljs-keyword: #cf222e;
--hljs-number: #0550ae;
--hljs-string: #0a3069;
--hljs-title: #8250df;
--hljs-type: #953800;
--hljs-attribute: #116329;
--hljs-meta: #57606a;
/* Light shadows - Subtle, warm-tinted */
--shadow-sm: 0 1px 2px rgba(60, 42, 24, 0.05);
--shadow-md: 0 4px 12px rgba(60, 42, 24, 0.07);
--shadow-lg: 0 12px 28px rgba(60, 42, 24, 0.09);
--shadow-xl: 0 24px 48px rgba(60, 42, 24, 0.11);
--shadow-glow: 0 0 20px var(--accent-glow);
--select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23444' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
color-scheme: light;
}
/* Scrollbar - visible on light backgrounds */
:root[data-theme-mode="light"]::-webkit-scrollbar-thumb,
:root[data-theme-mode="light"] ::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
}
:root[data-theme-mode="light"]::-webkit-scrollbar-thumb:hover,
:root[data-theme-mode="light"] ::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25);
}
/* Theme families override accent tokens while keeping shared surfaces/layout. */
:root[data-theme="openknot"] {
/*
* Accent — crimson red on true black
*
* WCAG 2.1 AA audit (bg = #080808, relative luminance ≈ 0.003):
* --accent #e5243b L ≈ 0.118 contrast ≈ 5.8:1 AA text ✓
* --accent-hover #f03e52 L ≈ 0.163 contrast ≈ 7.2:1 ✓
* --primary-foreground #fafafa on #d92a3f button: 4.62:1 AA ✓
* --primary-foreground #fafafa on #c22537 hover: 5.57:1 AA ✓
* --destructive-foreground #fafafa on #d32f2f button: 4.77:1 AA ✓
* --destructive-foreground #fafafa on #bc2a2a hover: 5.74:1 AA ✓
* focus-ring at 70% opacity: ≈ 3.4:1 against bg ✓ (non-text 3:1 required)
*/
--ring: #e5243b;
--accent: #e5243b;
--accent-hover: #f03e52;
--accent-muted: #e5243b;
--accent-subtle: rgba(229, 36, 59, 0.12);
--accent-glow: rgba(229, 36, 59, 0.24);
--primary: #d92a3f;
--primary-hover: #c22537;
--primary-foreground: #fafafa;
/* Deepen only the text-bearing destructive fill; keep danger accents vivid. */
--destructive: #d32f2f;
--destructive-hover: #bc2a2a;
/* Focus — crimson ring */
--focus: rgba(229, 36, 59, 0.2);
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 70%, transparent);
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 16px var(--accent-glow);
/* Surfaces — true black canvas */
--bg: #080808;
--bg-accent: #0d0d0f;
--bg-elevated: #141416;
--bg-hover: #1a1a1e;
--bg-muted: #1a1a1e;
--card: #111113;
--card-foreground: #f0f0f2;
--card-highlight: rgba(255, 255, 255, 0.03);
--popover: #141416;
--popover-foreground: #f0f0f2;
--panel: #080808;
--panel-strong: #141416;
--panel-hover: #1a1a1e;
--chrome: rgba(8, 8, 8, 0.96);
--chrome-strong: rgba(8, 8, 8, 0.98);
/* Text — high contrast on black */
--text: #e0e0e2;
--text-strong: #f5f5f7;
--chat-text: #e0e0e2;
--muted: #8a8a94;
--muted-strong: #888892;
--muted-foreground: #8a8a94;
/* Borders — whisper-thin, barely visible */
--border: #1a1a1e;
--border-strong: #2a2a30;
--border-hover: #3a3a42;
--input: #1a1a1e;
--secondary: #111113;
--secondary-foreground: #f0f0f2;
--accent-2: #8b1a2b;
--accent-2-muted: rgba(139, 26, 43, 0.7);
--accent-2-subtle: rgba(139, 26, 43, 0.12);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.6);
--grid-line: rgba(255, 255, 255, 0.025);
}
:root[data-theme="openknot-light"] {
/*
* Accent — deep red on cool white
*
* WCAG 2.1 AA audit (bg = #f9f9fb, relative luminance ≈ 0.941):
* --accent #c41e30 L ≈ 0.078 contrast ≈ 7.5:1 AAA text ✓
* --accent-hover #a8192a L ≈ 0.054 contrast ≈ 9.1:1 ✓
* --primary-foreground #ffffff on #c41e30 button: 5.0:1 AA ✓
* focus-ring at 60% opacity: ≈ 3.2:1 against bg ✓ (non-text 3:1 required)
*/
--ring: #c41e30;
--accent: #c41e30;
--accent-hover: #a8192a;
--accent-muted: #c41e30;
--accent-subtle: rgba(196, 30, 48, 0.08);
--accent-glow: rgba(196, 30, 48, 0.12);
--primary: #c41e30;
--primary-foreground: #ffffff;
/* Surfaces — cool white with subtle warmth */
--bg: #f9f9fb;
--bg-accent: #f2f2f5;
--bg-elevated: #ffffff;
--bg-hover: #eaeaef;
--bg-muted: #eaeaef;
--bg-content: #f2f2f5;
--card: #ffffff;
--card-foreground: #18181b;
--card-highlight: rgba(0, 0, 0, 0.02);
--popover: #ffffff;
--popover-foreground: #18181b;
--panel: #f9f9fb;
--panel-strong: #f2f2f5;
--panel-hover: #e4e4ea;
--chrome: rgba(249, 249, 251, 0.96);
--chrome-strong: rgba(249, 249, 251, 0.98);
/* Text — crisp dark on white */
--text: #3a3a42;
--text-strong: #18181b;
--chat-text: #3a3a42;
--muted: #68676f;
--muted-strong: #52525a;
--muted-foreground: #68676f;
/* Borders — clean, minimal */
--border: #e2e2e8;
--border-strong: #ccccd4;
--border-hover: #adadb8;
--input: #e2e2e8;
--secondary: #f2f2f5;
--secondary-foreground: #3a3a42;
--accent-2: #7a1420;
--accent-2-muted: rgba(122, 20, 32, 0.75);
--accent-2-subtle: rgba(122, 20, 32, 0.08);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.08);
--grid-line: rgba(0, 0, 0, 0.04);
}
:root[data-theme="dash"] {
/*
* Accent — chocolate brown on deep cocoa
*
* WCAG 2.1 AA audit (bg = #1a1210, relative luminance ≈ 0.0069):
* --accent #b47840 L ≈ 0.235 contrast ≈ 5.0:1 AA text ✓ (floor: #a8703c → 4.4:1, fails)
* --accent-hover #c8885a L ≈ 0.306 contrast ≈ 6.3:1 ✓
* --primary-foreground #1a1210 on #b47840 button: 5.0:1 AA ✓
* --primary-foreground #1a1210 on #c8885a hover: 6.26:1 AA ✓
* --destructive-foreground #fafafa on #d32f2f button: 4.77:1 AA ✓
* --destructive-foreground #fafafa on #bc2a2a hover: 5.74:1 AA ✓
* focus-ring at 80% opacity: ≈ 3.5:1 against bg ✓ (non-text 3:1 required)
*/
--ring: #b47840;
--accent: #b47840;
--accent-hover: #c8885a;
--accent-muted: #b47840;
--accent-subtle: rgba(180, 120, 64, 0.16);
--accent-glow: rgba(180, 120, 64, 0.24);
--primary: #b47840;
--primary-foreground: #1a1210;
/* Deepen only the text-bearing destructive fill; keep danger accents vivid. */
--destructive: #d32f2f;
--destructive-hover: #bc2a2a;
/* Focus — match chocolate accent, slightly higher ring opacity for dark bg visibility */
--focus: rgba(180, 120, 64, 0.2);
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 80%, transparent);
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 16px var(--accent-glow);
/* Surfaces — deep cocoa tones */
--bg: #1a1210;
--bg-accent: #201816;
--bg-elevated: #28201c;
--bg-hover: #302822;
--bg-muted: #302822;
--card: #221a16;
--card-foreground: #ece0d8;
--card-highlight: rgba(255, 240, 225, 0.04);
--popover: #28201c;
--popover-foreground: #ece0d8;
--panel: #1a1210;
--panel-strong: #28201c;
--panel-hover: #302822;
--chrome: rgba(26, 18, 16, 0.96);
--chrome-strong: rgba(26, 18, 16, 0.98);
--text: #d8c8b8;
--text-strong: #f0e4da;
--chat-text: #d8c8b8;
--muted: #a18f80;
--muted-strong: #a08f7f;
--muted-foreground: #a18f80;
--border: #302418;
--border-strong: #443828;
--border-hover: #5a4c3a;
--input: #302418;
--secondary: #221a16;
--secondary-foreground: #ece0d8;
--accent-2: #a88050;
--accent-2-muted: rgba(168, 128, 80, 0.7);
--accent-2-subtle: rgba(168, 128, 80, 0.12);
--shadow-sm: 0 1px 2px rgba(10, 6, 4, 0.35);
--shadow-md: 0 4px 16px rgba(10, 6, 4, 0.45);
--shadow-lg: 0 12px 32px rgba(10, 6, 4, 0.55);
--grid-line: rgba(255, 240, 225, 0.03);
}
:root[data-theme="dash-light"] {
/*
* Accent — deep chocolate on warm parchment
*
* WCAG 2.1 AA audit (bg = #f7f2ec, relative luminance ≈ 0.893):
* --accent #6e4828 L ≈ 0.081 contrast ≈ 7.2:1 AAA text ✓
* --accent-hover #5c3c20 L ≈ 0.054 contrast ≈ 8.9:1 ✓
* --primary-foreground #ffffff on #6e4828 button: 8.0:1 AAA ✓
* focus-ring at 70% opacity: ≈ 3.4:1 against parchment bg ✓
*/
--ring: #6e4828;
--accent: #6e4828;
--accent-hover: #5c3c20;
--accent-muted: #6e4828;
--accent-subtle: rgba(110, 72, 40, 0.12);
--accent-glow: rgba(110, 72, 40, 0.16);
--primary: #6e4828;
--primary-foreground: #ffffff;
/* Surfaces — warm parchment tones */
--bg: #f7f2ec;
--bg-accent: #f0e8e0;
--bg-elevated: #ffffff;
--bg-hover: #e8ddd2;
--bg-muted: #e8ddd2;
--bg-content: #f0e8e0;
--card: #ffffff;
--card-foreground: #2c2118;
--card-highlight: rgba(80, 50, 20, 0.02);
--popover: #ffffff;
--popover-foreground: #2c2118;
--panel: #f7f2ec;
--panel-strong: #f0e8e0;
--panel-hover: #e0d4c8;
--chrome: rgba(247, 242, 236, 0.96);
--chrome-strong: rgba(247, 242, 236, 0.98);
--text: #4a3828;
--text-strong: #2c2118;
--chat-text: #4a3828;
--muted: #725d4d;
--muted-strong: #604838;
--muted-foreground: #725d4d;
--border: #ddd0c2;
--border-strong: #c8b8a6;
--border-hover: #b0a090;
--input: #ddd0c2;
--secondary: #f0e8e0;
--secondary-foreground: #4a3828;
--accent-2: #6a4e30;
--accent-2-muted: rgba(106, 78, 48, 0.75);
--accent-2-subtle: rgba(106, 78, 48, 0.1);
--shadow-sm: 0 1px 2px rgba(60, 40, 20, 0.06);
--shadow-md: 0 4px 12px rgba(60, 40, 20, 0.08);
--shadow-lg: 0 12px 28px rgba(60, 40, 20, 0.1);
--grid-line: rgba(80, 50, 20, 0.04);
}
* {
box-sizing: border-box;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
html,
body {
height: 100%;
/* App shell: inner panes scroll, the root never does — in every host
(browser tab, PWA, Mac app WKWebView). WKWebView drag/rubber-band scrolls
the document even when nothing overflows, dragging the whole shell with
it; `clip` also blocks programmatic root scrolls (focus/scrollIntoView)
that `hidden` would let strand the shell off-screen. */
overflow: clip;
overscroll-behavior: none;
/* The app supplies its own pressed and selected states; WebKit's tap flash
reads as foreign chrome on top of them. */
-webkit-tap-highlight-color: transparent;
/* iOS WKWebView font boosting inflates rendered text without re-running
layout, so labels spill out of fixed-size chrome (welcome chips, badges).
Locking text-size-adjust keeps rendered metrics equal to layout metrics. */
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
a,
button,
label,
summary,
input,
select,
textarea,
[role="button"] {
touch-action: manipulation;
}
body {
margin: 0;
font: 400 14px/1.55 var(--font-body);
letter-spacing: -0.01em;
background: var(--bg);
color: var(--text);
/* App chrome behaves like native controls: dragging across labels, tiles,
and navigation must not create accidental text selections. Text-entry
surfaces and document-like content opt back in below or in route CSS. */
user-select: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* iOS Safari zooms the viewport when a focused text control is under 16px;
important keeps feature-local typography from silently dropping this floor.
Controls that manage their own touch size declare it via
--control-ui-touch-input-size; the floor never caps above 16px. */
@media (pointer: coarse) {
input,
select,
textarea {
font-size: max(16px, var(--control-ui-touch-input-size, 1em)) !important;
}
}
@media (min-width: 1600px) {
body {
font-size: 15px;
}
}
/* Theme transition */
@keyframes theme-circle-transition {
0% {
clip-path: circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%));
}
100% {
clip-path: circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%));
}
}
html.theme-transition {
view-transition-name: theme;
}
html.theme-transition::view-transition-old(theme) {
mix-blend-mode: normal;
animation: none;
z-index: 1;
}
html.theme-transition::view-transition-new(theme) {
mix-blend-mode: normal;
z-index: 2;
animation: theme-circle-transition 0.4s var(--ease-out) forwards;
}
@media (prefers-reduced-motion: reduce) {
html.theme-transition::view-transition-old(theme),
html.theme-transition::view-transition-new(theme) {
animation: none !important;
}
}
openclaw-app {
display: block;
position: relative;
z-index: 1;
height: 100%;
}
@media (display-mode: standalone) {
@supports (height: 100dvh) {
html,
body {
height: 100dvh;
}
}
body {
position: fixed;
inset: 0;
padding-top: var(--safe-area-top);
padding-right: var(--safe-area-right);
padding-bottom: var(--safe-area-bottom);
padding-left: var(--safe-area-left);
}
}
/* Cursor policy. A browser tab gets the pointer hand, the only hover affordance
a page owns; installed/app-like windows are desktop chrome and keep the arrow.
Controls consume --cursor-action rather than hardcoding a cursor, and real
hyperlinks keep the UA pointer in every mode. */
/* stylelint-disable-next-line no-duplicate-selectors -- cursor policy block, see comment above */
:root {
--cursor-action: pointer;
}
/* Separate from the standalone block above, which owns viewport and safe-area
insets; widening its condition would change those too. `fullscreen` is absent
on purpose: it is a transient presentation state any window can enter, and
the manifest installs this UI as `standalone`. */
@media (display-mode: standalone),
(display-mode: minimal-ui),
(display-mode: window-controls-overlay) {
:root {
--cursor-action: default;
}
}
/* Native app hosts are installed desktop windows too, but they embed a plain
web view that reports `display-mode: browser`; they announce themselves with
these `<html>` markers instead (all three, as layout.css already matches). */
html:is(.openclaw-native-macos, .openclaw-native-nav, .openclaw-native-web-chrome) {
--cursor-action: default;
}
/* Generic actionable controls, at element/attribute specificity only so the
component rules that own semantic cursors (not-allowed, disabled, grab,
resize, deliberate `default` on non-actionable elements) still win. */
button,
summary,
select,
[role="button"],
[role="checkbox"],
[role="menuitem"],
[role="menuitemcheckbox"],
[role="menuitemradio"],
[role="option"],
[role="radio"],
[role="switch"],
[role="tab"],
input:is(
[type="button"],
[type="checkbox"],
[type="color"],
[type="file"],
[type="radio"],
[type="range"],
[type="reset"],
[type="submit"]
) {
cursor: var(--cursor-action);
}
a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
}
a:hover {
text-decoration-thickness: 2px;
}
/* Web Awesome dropdown items ship `cursor: pointer` on their shadow host.
Document rules beat `:host`, so route them through the policy here — and
re-add the disabled cue this override would otherwise clobber. */
wa-dropdown-item {
cursor: var(--cursor-action);
}
wa-dropdown-item[disabled] {
cursor: not-allowed;
}
/* Keep Web Awesome's checkbox rail trailing so it cannot shift menu icons and labels. */
wa-dropdown-item[checkbox-adjacent] {
padding-inline-start: 1em;
}
wa-dropdown-item[type="checkbox"]::part(checkmark) {
order: 1;
margin-inline: 0.75em 0;
}
wa-dropdown-item[type="checkbox"] {
display: flex;
align-items: center;
}
button,
input,
textarea,
select {
font: inherit;
color: inherit;
}
/* One accent for every native check control; without this, surfaces that
forget a local override render the browser's stock blue. */
input[type="checkbox"],
input[type="radio"] {
accent-color: var(--accent);
}
input,
textarea,
[contenteditable]:not([contenteditable="false"]) {
user-select: text;
}
::selection {
background: var(--selection-bg);
color: var(--selection-fg);
}
/* Main/document surfaces keep a normal scrollbar hit target. Dense sidebars
override this inherited size locally so their row actions retain room. */
::-webkit-scrollbar {
width: var(--scrollbar-size, 12px);
height: var(--scrollbar-size, 12px);
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.14);
}
/* Animations - Polished with spring feel */
@keyframes rise {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes menu-in {
from {
opacity: 0;
transform: translateY(6px) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes dashboard-enter {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Skeleton loading primitives */
.skeleton {
background: linear-gradient(90deg, var(--bg-muted) 25%, var(--bg-hover) 50%, var(--bg-muted) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: var(--radius-md);
}
.skeleton-line {
height: 14px;
border-radius: var(--radius-sm);
}
.skeleton-line--short {
width: 40%;
}
.skeleton-line--medium {
width: 65%;
}
.skeleton-line--long {
width: 85%;
}
@keyframes pulse-subtle {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@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;
}
}
/* Focus visible styles */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}