Files
AgentHub/client/src/index.css
T
nntrivi2001 1c5521cf2e perf: rest animations, cheaper chat typing and server polls, async per-tool hooks
UI
- No animation runs forever: Sagi moods, the custom pet sprite, status dots
  (pulse-dot, pulse-slow, ping) and the chat shimmer play a few cycles and
  rest. An infinite animation kept the browser drawing 60 frames a second.
- useSagiBrain: frames that only move lastActivityAt by <1 s keep the old
  state; the 1 s tick re-renders only when the mood changes.
- ConversationView and MessageList are memoized and PaneChat passes stable
  callbacks: typing in the chat box no longer re-renders the conversation.
- The chat's 700 ms menu poll reads plain text first and scans the terminal
  cell by cell (reusing one cell) only when a row could be a menu cursor row.

Server
- GET /sessions/:id/transcript?after=N skips lines <= N before JSON.parse.
- GET /sessions/:id/outline is cached until the session's events change.
- broadcastLane uses the cached tmux.liveSessions(); tmux.hasSession answers
  "yes" from the fresh shared list instead of a synchronous spawn (50-150 ms
  per psmux call on Windows, blocking terminal echo).
- pane-mode polls every 150 ms; writeHourly writes only changed hour buckets.
- /api/metrics: agenthub_event_loop_delay_seconds{quantile}.

Hooks
- PreToolUse/PostToolUse run with "async": true (settings installer,
  plugin.json, S3 managed settings): Claude Code no longer waits a Node start
  before and after every tool call.

Committed with --no-verify at the owner's request: the pre-commit full server
suite fails on 9 tests that fail identically on 0c2f498 (lane-sync merge: no
git identity on this host; read-pool; resolveHost; codex-usage 1 ms flake).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:55:53 +07:00

720 lines
27 KiB
CSS

/**
* @file index.css
* @description Global styles and Tailwind CSS configuration for the application.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Palette sourced from Radix Colors (radix-ui.com/colors), an accessible
* 12-step system built for UI, contrast-checked with APCA — chosen after
* three rounds of hand-picked values that kept overshooting (too flat, then
* too dark, then too glaring). Radix's own role guidance for a 12-step scale:
* 1-2 app/chrome background 6-8 borders (6 subtle, 7-8 stronger)
* 3-5 component background 9-10 solid (buttons, vibrant fills)
* 11-12 text (11 secondary, 12 primary)
* The DARK theme follows it literally: `slate` supplies surface/border/fg and
* `blue` supplies accent. The LIGHT theme keeps the structure (same roles, same
* rising-lightness ramp) with IntelliJ Light's neutrals and blue accent. `green`/
* `red`/`amber` step 11 supplies the status tokens — the same step used for
* fg-secondary, since 11 is Radix's guaranteed-readable-text step. Values are
* the literal Radix hex constants (steps 1/2/3/4/5/6/7/9/11/12), converted to
* RGB triplets for Tailwind's `<alpha-value>` opacity support.
*/
:root,
[data-mode="light"] {
/* Light theme — the CSS default, so it applies with no class on <html>.
`[data-mode="light"]` lets a light theme preview re-scope inside a dark
page (the Settings appearance cards).
IntelliJ Light's cool neutrals, ordered so lightness rises with the index
exactly as it does in dark: the page is the DARKEST step and a card is
brighter than the page it sits on, so every `hover:bg-*` lifts. */
--surface-0: 236 238 242; /* #eceef2 — page background */
--surface-1: 241 242 245; /* #f1f2f5 — sub-headers inside a card */
--surface-2: 247 248 250; /* #f7f8fa — inputs, chips, toolbars */
--surface-3: 253 253 254; /* #fdfdfe — `.card` */
--surface-4: 255 255 255; /* #ffffff — hover / active lift */
--surface-5: 255 255 255; /* #ffffff — topmost, rare */
--border: 211 213 219; /* #d3d5db */
--border-light: 228 230 235; /* #e4e6eb */
--accent: 40 90 192; /* #285ac0 */
--accent-hover: 33 74 158; /* #214a9e */
/* The FILL behind white text (`.btn-primary`, a Switch's on-track, a ticked
Checkbox, `bg-accent-solid`). In light themes the text accent is already
dark enough to carry white, so it simply IS the accent — resolved per
scope, like `--tint-base`. Dark themes declare their own: there the text
accent must be LIGHT to read on a near-black card, which makes it a fill
white text cannot sit on (3.3:1). */
--accent-solid: var(--accent);
--accent-solid-hover: var(--accent-hover);
--fg-primary: 30 31 34; /* #1e1f22 */
--fg-secondary: 73 75 87; /* #494b57 */
/* Carries real content (timestamps, counts), so it is held above 4.5:1 on a
card and still sits a clear step below fg-secondary. */
--fg-muted: 103 106 120; /* #676a78 */
/* The SHADOW carries "raised" here, not the quiet border. */
--shadow-card: 0 1px 2px rgb(30 31 34 / 0.05), 0 2px 6px rgb(30 31 34 / 0.06);
/* Status labels clear 4.5:1 on the card AND on their own 20% chip. */
--status-success: 23 106 45; /* #176a2d */
--status-danger: 173 28 38; /* #ad1c26 */
--status-warning: 124 82 0; /* #7c5200 */
/* What every status/accent FILL is pre-mixed against (see `tint()` in
tailwind.config.js) — the card surface, because that is where chips and
alert strips overwhelmingly sit. */
/* Re-declared by every scope that sets a theme (this block, `.dark`), so it
resolves against THAT scope's surface-3; a new scope must re-declare it too. */
--tint-base: var(--surface-3);
/* Syntax colours for `lib/highlight.ts` — IntelliJ Light's editor scheme. */
--syn-comment: 112 112 112; /* #707070 */
--syn-string: 6 125 23; /* #067d17 */
--syn-number: 23 80 235; /* #1750eb */
--syn-keyword: 0 51 179; /* #0033b3 */
--syn-builtin: 0 0 128; /* #000080 */
--syn-function: 0 98 122; /* #00627a */
--syn-operator: 75 75 75; /* #4b4b4b */
--syn-punctuation: 75 75 75; /* #4b4b4b */
--syn-property: 135 16 148; /* #871094 */
--syn-tag: 0 51 179; /* #0033b3 */
--syn-plain: var(--fg-primary);
--syn-add: 6 125 23; /* #067d17 */
--syn-del: 192 31 42; /* #c01f2a */
/* A diff line edited in place (not wholly added or removed). */
--diff-mod: 29 78 216; /* blue-700 #1d4ed8 */
/* An unversioned (untracked) file in the Changes panel, JetBrains' orange. */
--kept: 194 65 12; /* orange-700 #c2410c */
}
.dark {
/* Radix `slate` (dark), steps 1/2/3/4/5/6/7. The scale is built in the
opposite direction on purpose — step 6 is LIGHTER than step 3/4 here,
which is what makes borders rise off a panel instead of sinking into it
(the bug in the previous, hand-picked dark border). */
--surface-0: 17 17 19; /* slate-1 #111113 */
--surface-1: 24 25 27; /* slate-2 #18191b */
--surface-2: 33 34 37; /* slate-3 #212225 */
--surface-3: 39 42 45; /* slate-4 #272a2d */
--surface-4: 46 49 53; /* slate-5 #2e3135 */
--surface-5: 54 58 63; /* slate-6 #363a3f */
--border: 54 58 63; /* slate-6 #363a3f */
--border-light: 67 72 78; /* slate-7 #43484e */
/* Text/indicator accent is Radix blue-11 (its text step): blue-9 #0090ff was
4.4:1 on a card and 3.4:1 on its own selected chip. The solid blue-9 fill
lives on as the hue of `--accent-solid`, darkened until white text on it
clears 4.5:1 (it was 3.3:1). */
--accent: 112 184 255; /* blue-11 #70b8ff */
--accent-hover: 158 206 255; /* #9eceff */
--accent-solid: 0 118 209; /* #0076d1 — white 4.8:1 */
--accent-solid-hover: 0 102 181; /* #0066b5 */
--fg-primary: 237 238 240; /* slate-12 #edeef0 */
--fg-secondary: 176 180 186; /* slate-11 #b0b4ba */
/* slate-9 #696e77 was 2.8:1 on a card. Raised until it clears 4.5:1 on the
hover lift (surface-4) too, still a clear step below fg-secondary. */
--fg-muted: 150 154 160; /* #969aa0 */
/* Dark surfaces already separate by lightness step; a shadow adds nothing
visible against a dark page and reads as a stray dark smudge. */
--shadow-card: none;
--status-success: 61 214 140; /* green-11 dark #3dd68c */
--status-danger: 255 149 146; /* red-11 dark #ff9592 */
--status-warning: 255 202 22; /* amber-11 dark #ffca16 */
--tint-base: var(--surface-3);
/* Unchanged from the pastels this file has always drawn code in. */
--syn-comment: 107 114 128; /* gray-500 #6b7280 */
--syn-string: 110 231 183; /* emerald-300 #6ee7b7 */
--syn-number: 253 186 116; /* orange-300 #fdba74 */
--syn-keyword: 196 181 253; /* violet-300 #c4b5fd */
--syn-builtin: 125 211 252; /* sky-300 #7dd3fc */
--syn-function: 254 240 138; /* yellow-200 #fef08a */
--syn-operator: 249 168 212; /* pink-300 #f9a8d4 */
--syn-punctuation: 156 163 175; /* gray-400 #9ca3af */
--syn-property: 103 232 249; /* cyan-300 #67e8f9 */
--syn-tag: 253 164 175; /* rose-300 #fda4af */
--syn-plain: 229 231 235; /* gray-200 #e5e7eb — kept off fg-primary so Default is unchanged */
--syn-add: 110 231 183; /* emerald-300 */
--syn-del: 252 165 165; /* red-300 #fca5a5 */
--diff-mod: 96 165 250; /* blue-400 #60a5fa */
--kept: 251 146 60; /* orange-400 #fb923c */
}
/* Named themes (`data-theme` on <html>, set by hooks/useTheme.ts, picked in
* Settings → Appearance). Each overrides only what differs from its mode's
* default block above; `--tint-base` and light `--syn-plain` are derived there.
* Dark ones pair with `.dark`, light ones with `[data-mode="light"]`, so a
* theme can never paint over the wrong mode whatever order the attributes
* land in. Contrast is held to the defaults' own bar: fg-muted ≥4.5:1 on a
* light card, status labels ≥4.5:1 on their own /20 chip in light themes. */
.dark[data-theme="darcula"] {
--surface-0: 30 31 32; /* #1e1f20 */
--surface-1: 35 36 37; /* #232425 */
--surface-2: 39 41 42; /* #27292a */
--surface-3: 43 43 43; /* #2b2b2b */
--surface-4: 51 53 55; /* #333537 */
--surface-5: 60 63 65; /* #3c3f41 */
--border: 63 66 68; /* #3f4244 */
--border-light: 77 81 83; /* #4d5153 */
--accent: 132 189 219; /* #84bddb */
--accent-hover: 168 209 230; /* #a8d1e6 */
--accent-solid: 45 124 167; /* #2d7ca7 */
--accent-solid-hover: 38 106 143; /* #266a8f */
--fg-primary: 220 220 220; /* #dcdcdc */
--fg-secondary: 169 183 198; /* #a9b7c6 */
--fg-muted: 154 159 163; /* #9a9fa3 */
--status-success: 132 200 141; /* #84c88d */
--status-danger: 255 153 150; /* #ff9996 */
--status-warning: 232 191 106; /* #e8bf6a */
--syn-comment: 128 128 128; /* #808080 */
--syn-string: 127 159 105; /* #7f9f69 */
--syn-number: 104 151 187; /* #6897bb */
--syn-keyword: 204 120 50; /* #cc7832 */
--syn-builtin: 136 136 198; /* #8888c6 */
--syn-function: 255 198 109; /* #ffc66d */
--syn-operator: 169 183 198; /* #a9b7c6 */
--syn-punctuation: 169 183 198; /* #a9b7c6 */
--syn-property: 168 139 192; /* #a88bc0 */
--syn-tag: 232 191 106; /* #e8bf6a */
--syn-add: 106 171 115; /* #6aab73 */
--syn-del: 255 107 104; /* #ff6b68 */
--syn-plain: 169 183 198; /* #a9b7c6 */
}
.dark[data-theme="apple-dark"] {
--surface-0: 22 22 24; /* #161618 */
--surface-1: 28 28 30; /* #1c1c1e */
--surface-2: 36 36 38; /* #242426 */
--surface-3: 44 44 46; /* #2c2c2e */
--surface-4: 54 54 56; /* #363638 */
--surface-5: 58 58 60; /* #3a3a3c */
--border: 56 56 58; /* #38383a */
--border-light: 72 72 74; /* #48484a */
--accent: 123 189 255; /* #7bbdff */
--accent-hover: 163 209 255; /* #a3d1ff */
--accent-solid: 9 115 222; /* #0973de */
--accent-solid-hover: 7 98 190; /* #0762be */
--fg-primary: 245 245 247; /* #f5f5f7 */
/* Lifted with fg-muted so the two stay a visible step apart. */
--fg-secondary: 199 199 204; /* #c7c7cc */
--fg-muted: 160 160 164; /* #a0a0a4 */
--status-success: 48 209 88; /* #30d158 */
--status-danger: 255 155 150; /* #ff9b96 */
--status-warning: 255 214 10; /* #ffd60a */
--syn-comment: 127 140 152; /* #7f8c98 */
--syn-string: 252 106 93; /* #fc6a5d */
--syn-number: 208 191 105; /* #d0bf69 */
--syn-keyword: 252 95 163; /* #fc5fa3 */
--syn-builtin: 93 216 255; /* #5dd8ff */
--syn-function: 103 183 164; /* #67b7a4 */
--syn-operator: 161 161 166; /* #a1a1a6 */
--syn-punctuation: 161 161 166; /* #a1a1a6 */
--syn-property: 181 140 240; /* #b58cf0 */
--syn-tag: 252 95 163; /* #fc5fa3 */
--syn-add: 48 209 88; /* #30d158 */
--syn-del: 255 105 97; /* #ff6961 */
--syn-plain: 229 229 234; /* #e5e5ea */
}
/* Orca Dark — Orca's neutral palette (src/renderer/src/assets/main.css `.dark`).
* Accent is NOT Orca's #e5e5e5 primary: AgentHub puts white text on `bg-accent`
* (.btn-primary), which would read 1.3:1 there. Text accent and the solid fill
* behind white text (`--accent-solid`) are two shades of #3b82f6, each held at
* 4.5:1. Syntax colours are Apple Dark's. */
.dark[data-theme="orca-dark"] {
--surface-0: 10 10 10; /* #0a0a0a */
--surface-1: 17 17 17; /* #111111 */
--surface-2: 23 23 23; /* #171717 */
--surface-3: 28 28 28; /* #1c1c1c */
--surface-4: 38 38 38; /* #262626 */
--surface-5: 64 64 64; /* #404040 */
--border: 31 31 31; /* #1f1f1f */
--border-light: 46 46 46; /* #2e2e2e */
--accent: 106 160 248; /* #6aa0f8 */
--accent-hover: 147 187 250; /* #93bbfa */
--accent-solid: 52 114 216; /* #3472d8 */
--accent-solid-hover: 44 97 184; /* #2c61b8 */
--fg-primary: 250 250 250; /* #fafafa */
--fg-secondary: 212 212 212; /* #d4d4d4 */
--fg-muted: 161 161 161; /* #a1a1a1 */
--status-success: 134 239 172; /* #86efac */
--status-danger: 255 112 115; /* #ff7073 */
--status-warning: 234 179 8; /* #eab308 */
--syn-comment: 127 140 152; /* #7f8c98 */
--syn-string: 252 106 93; /* #fc6a5d */
--syn-number: 208 191 105; /* #d0bf69 */
--syn-keyword: 252 95 163; /* #fc5fa3 */
--syn-builtin: 93 216 255; /* #5dd8ff */
--syn-function: 103 183 164; /* #67b7a4 */
--syn-operator: 161 161 166; /* #a1a1a6 */
--syn-punctuation: 161 161 166; /* #a1a1a6 */
--syn-property: 181 140 240; /* #b58cf0 */
--syn-tag: 252 95 163; /* #fc5fa3 */
--syn-add: 48 209 88; /* #30d158 */
--syn-del: 255 105 97; /* #ff6961 */
--syn-plain: 229 229 234; /* #e5e5ea */
}
[data-mode="light"][data-theme="apple-light"] {
--surface-0: 233 233 238; /* #e9e9ee */
--surface-1: 239 239 244; /* #efeff4 */
--surface-2: 245 245 248; /* #f5f5f8 */
--surface-3: 251 251 253; /* #fbfbfd */
--surface-4: 253 253 254; /* #fdfdfe */
--surface-5: 255 255 255; /* #ffffff */
--border: 209 209 214; /* #d1d1d6 */
--border-light: 229 229 234; /* #e5e5ea */
--accent: 0 92 184; /* #005cb8 */
--accent-hover: 0 76 153; /* #004c99 */
--fg-primary: 29 29 31; /* #1d1d1f */
--fg-secondary: 81 81 84; /* #515154 */
--fg-muted: 103 103 108; /* #67676c */
--shadow-card: 0 1px 2px rgb(0 0 0 / 0.05), 0 2px 6px rgb(0 0 0 / 0.06);
--status-success: 23 107 46; /* #176b2e */
--status-danger: 179 0 14; /* #b3000e */
--status-warning: 122 79 0; /* #7a4f00 */
--syn-comment: 93 108 121; /* #5d6c79 */
--syn-string: 196 26 22; /* #c41a16 */
--syn-number: 28 0 207; /* #1c00cf */
--syn-keyword: 155 35 147; /* #9b2393 */
--syn-builtin: 11 79 121; /* #0b4f79 */
--syn-function: 50 109 116; /* #326d74 */
--syn-operator: 75 75 80; /* #4b4b50 */
--syn-punctuation: 75 75 80; /* #4b4b50 */
--syn-property: 57 0 160; /* #3900a0 */
--syn-tag: 155 35 147; /* #9b2393 */
--syn-add: 31 122 54; /* #1f7a36 */
--syn-del: 197 0 15; /* #c5000f */
}
[data-mode="light"][data-theme="canvas"] {
--surface-0: 227 220 205; /* #e3dccd */
--surface-1: 232 226 212; /* #e8e2d4 */
--surface-2: 237 232 220; /* #ede8dc */
--surface-3: 243 239 230; /* #f3efe6 */
--surface-4: 247 244 237; /* #f7f4ed */
--surface-5: 250 248 243; /* #faf8f3 */
--border: 203 191 166; /* #cbbfa6 */
--border-light: 220 211 192; /* #dcd3c0 */
--accent: 119 80 47; /* #77502f */
--accent-hover: 102 63 34; /* #663f22 */
--fg-primary: 43 38 32; /* #2b2620 */
--fg-secondary: 94 85 74; /* #5e554a */
--fg-muted: 104 94 82; /* #685e52 */
--status-success: 54 93 36; /* #365d24 */
--status-danger: 147 41 31; /* #93291f */
--status-warning: 109 76 0; /* #6d4c00 */
--syn-comment: 125 114 98; /* #7d7262 */
--syn-string: 77 107 31; /* #4d6b1f */
--syn-number: 154 91 0; /* #9a5b00 */
--syn-keyword: 138 59 18; /* #8a3b12 */
--syn-builtin: 47 94 110; /* #2f5e6e */
--syn-function: 122 90 0; /* #7a5a00 */
--syn-operator: 138 48 80; /* #8a3050 */
--syn-punctuation: 107 97 85; /* #6b6155 */
--syn-property: 59 102 96; /* #3b6660 */
--syn-tag: 142 47 47; /* #8e2f2f */
--syn-add: 77 107 31; /* #4d6b1f */
--syn-del: 159 47 35; /* #9f2f23 */
}
/* Orca Light — Orca's neutral palette (`:root`). The surfaces rise with their
* index, so the page sits below a #fafafa card instead of Orca's white-on-white;
* status colours are two steps darker than Orca's so each label clears 4.5:1 on
* its own /20 chip. Syntax colours are Apple Light's. */
[data-mode="light"][data-theme="orca-light"] {
--surface-0: 235 235 235; /* #ebebeb */
--surface-1: 240 240 240; /* #f0f0f0 */
--surface-2: 245 245 245; /* #f5f5f5 */
--surface-3: 250 250 250; /* #fafafa */
--surface-4: 253 253 253; /* #fdfdfd */
--surface-5: 255 255 255; /* #ffffff */
--border: 229 229 229; /* #e5e5e5 */
--border-light: 212 212 212; /* #d4d4d4 */
--accent: 23 23 23; /* #171717 */
--accent-hover: 38 38 38; /* #262626 */
--fg-primary: 10 10 10; /* #0a0a0a */
--fg-secondary: 64 64 64; /* #404040 */
--fg-muted: 105 105 105; /* #696969 */
--status-success: 22 101 52; /* #166534 */
--status-danger: 168 0 15; /* #a8000f */
--status-warning: 133 77 14; /* #854d0e */
--syn-comment: 93 108 121; /* #5d6c79 */
--syn-string: 196 26 22; /* #c41a16 */
--syn-number: 28 0 207; /* #1c00cf */
--syn-keyword: 155 35 147; /* #9b2393 */
--syn-builtin: 11 79 121; /* #0b4f79 */
--syn-function: 50 109 116; /* #326d74 */
--syn-operator: 75 75 80; /* #4b4b50 */
--syn-punctuation: 75 75 80; /* #4b4b50 */
--syn-property: 57 0 160; /* #3900a0 */
--syn-tag: 155 35 147; /* #9b2393 */
--syn-add: 31 122 54; /* #1f7a36 */
--syn-del: 197 0 15; /* #c5000f */
}
@layer base {
body {
font-weight: 500;
}
* {
scrollbar-width: thin;
/* Claude's: a faint thumb on no track. */
scrollbar-color: rgb(var(--fg-muted) / 0.35) transparent;
}
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background: rgb(var(--border));
border-radius: 3px;
}
*::-webkit-scrollbar-thumb:hover {
background: rgb(var(--border-light));
}
::selection {
background: rgb(var(--accent) / 0.3);
color: rgb(var(--fg-primary));
}
::-webkit-calendar-picker-indicator {
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
/* The browser draws this glyph near-black. Inverting it is a DARK-mode fix;
applied globally it painted a white icon onto a white field. */
.dark ::-webkit-calendar-picker-indicator {
filter: invert(1) brightness(0.8);
}
::-webkit-calendar-picker-indicator:hover {
opacity: 1;
}
.time-input-custom::-webkit-calendar-picker-indicator {
display: none;
-webkit-appearance: none;
}
}
@layer components {
.card {
@apply bg-surface-3 border border-border rounded-xl;
box-shadow: var(--shadow-card);
}
.card-hover {
@apply card transition-all duration-200 hover:border-border-light hover:bg-surface-4;
}
/* Buttons. Every variant shares one keyboard focus ring (`focus-visible`
only, so a mouse click draws none). `.btn-primary` fills with
`accent-solid`, the shade white text clears 4.5:1 on in every theme —
never `bg-accent` behind white text. */
.btn-primary {
@apply inline-flex items-center gap-2 px-4 py-2 bg-accent-solid hover:bg-accent-solid-hover text-white text-sm font-medium rounded-lg transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50;
}
.btn-secondary {
@apply inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-fg-primary bg-surface-2 border border-border-light hover:bg-surface-4 rounded-lg transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 disabled:cursor-not-allowed disabled:opacity-50;
}
.btn-danger {
@apply inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-status-danger bg-status-danger/10 border border-status-danger/40 hover:bg-status-danger/20 rounded-lg transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-status-danger/60 disabled:cursor-not-allowed disabled:opacity-50;
}
.btn-ghost {
@apply inline-flex items-center gap-2 px-3 py-1.5 text-sm text-fg-secondary hover:text-fg-primary hover:bg-surface-4 rounded-lg transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 disabled:cursor-not-allowed disabled:opacity-50;
}
/* An icon-only button: a 28px target (WCAG 2.5.8 asks ≥24px). It has no
text, so give it an `aria-label` (its `title` becomes the tooltip). */
.btn-icon {
@apply inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-fg-secondary hover:text-fg-primary hover:bg-surface-4 transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 disabled:cursor-not-allowed disabled:opacity-50;
}
.badge {
@apply inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-full border whitespace-nowrap shrink-0;
}
.input {
@apply bg-surface-2 border border-border rounded-lg px-3 py-2 text-sm text-fg-primary placeholder-fg-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition-colors;
}
/* A native <select>, themed: the browser's arrow is replaced by one drawn in
fg-muted (the native one is near-black, invisible on a dark field), and the
open list follows the mode through `color-scheme`. */
.select {
@apply input appearance-none cursor-pointer pr-8 bg-no-repeat disabled:cursor-not-allowed disabled:opacity-60;
background-position: right 0.6rem center;
background-size: 0.75rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23888' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4.5l3 3 3-3'/%3E%3C/svg%3E");
}
}
/* The open list of a native <select> and the date picker follow the mode. */
:root,
[data-mode="light"] {
color-scheme: light;
}
.dark {
color-scheme: dark;
}
/* Vestibular safety (WCAG 2.3.3): with the OS asking for reduced motion, every
animation and transition collapses to an instant change — the pulsing
status dots, `animate-ping`, fade/slide-in, spinners included (a spinner
still shows; it just stops turning). Outside any layer so it beats the
utilities. */
@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;
}
}
@layer utilities {
/* Hide the scrollbar on horizontally scrollable strips (e.g. the Settings
TOC) where chevron affordances replace it. */
.no-scrollbar {
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Visually hidden until focused — first Tab lands here before the sidebar. */
.skip-to-content {
position: absolute;
left: 0.75rem;
top: 0.75rem;
z-index: 100;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
border: 1px solid rgb(var(--border-light));
background: rgb(var(--surface-2));
color: rgb(var(--fg-primary));
font-size: 0.8125rem;
font-weight: 500;
text-decoration: none;
transform: translateY(-150%);
transition: transform 0.15s ease;
}
.skip-to-content:focus {
transform: translateY(0);
outline: 2px solid rgb(var(--accent));
outline-offset: 2px;
}
}
@keyframes pulse-dot {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
/* A few pulses when the dot appears, then it rests lit. Status dots sit in
always-visible chrome; an infinite animation there makes the browser draw a
frame 60 times a second for as long as the page is open, which made the
whole desktop stutter. A state change re-adds the class and pulses again. */
.animate-pulse-dot {
animation: pulse-dot 2s cubic-bezier(0.4, 0, 0.6, 1) 3;
}
/* The chat's live step, like Claude Code's: a light sweeping over the words —
a few sweeps, then it rests on the muted colour (a background-position
animation repaints the text every frame; the spinner beside it keeps saying
the step runs). Reduced motion stops it at once. */
.text-shimmer {
background: linear-gradient(
90deg,
rgb(var(--fg-muted)) 40%,
rgb(var(--fg-primary)) 50%,
rgb(var(--fg-muted)) 60%
)
0 0 / 300% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: text-shimmer 2.5s linear 3;
}
@keyframes text-shimmer {
from {
background-position: 100% 0;
}
to {
background-position: 0 0;
}
}
/* Rendered markdown (MarkdownPreview.tsx): theme tokens only, so every theme
reads it; no typography plugin. */
.md-preview {
color: rgb(var(--fg-secondary));
font-size: 13px;
line-height: 1.6;
overflow-wrap: anywhere;
}
.md-preview > :first-child {
margin-top: 0;
}
.md-preview h1,
.md-preview h2,
.md-preview h3,
.md-preview h4,
.md-preview h5,
.md-preview h6 {
color: rgb(var(--fg-primary));
font-weight: 600;
line-height: 1.3;
margin: 1.4em 0 0.6em;
}
.md-preview h1 {
font-size: 1.6em;
padding-bottom: 0.3em;
border-bottom: 1px solid rgb(var(--border));
}
.md-preview h2 {
font-size: 1.3em;
padding-bottom: 0.25em;
border-bottom: 1px solid rgb(var(--border));
}
.md-preview h3 {
font-size: 1.12em;
}
.md-preview h4,
.md-preview h5,
.md-preview h6 {
font-size: 1em;
}
.md-preview p,
.md-preview ul,
.md-preview ol,
.md-preview blockquote,
.md-preview pre,
.md-preview table {
margin: 0 0 0.9em;
}
.md-preview ul {
list-style: disc;
padding-left: 1.5em;
}
.md-preview ol {
list-style: decimal;
padding-left: 1.5em;
}
.md-preview li + li {
margin-top: 0.2em;
}
.md-preview li > ul,
.md-preview li > ol {
margin: 0.2em 0 0;
}
.md-preview li.task-list-item {
list-style: none;
margin-left: -1.3em;
}
.md-preview li.task-list-item input {
margin-right: 0.4em;
vertical-align: middle;
}
.md-preview a {
color: rgb(var(--accent));
text-decoration: underline;
text-underline-offset: 2px;
}
.md-preview strong {
color: rgb(var(--fg-primary));
font-weight: 600;
}
.md-preview code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.88em;
padding: 0.1em 0.35em;
border-radius: 4px;
background: rgb(var(--surface-3));
color: rgb(var(--fg-primary));
}
.md-preview pre,
.md-preview .md-frontmatter {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
line-height: 1.5;
padding: 0.75em 1em;
border-radius: 6px;
border: 1px solid rgb(var(--border));
background: rgb(var(--surface-0));
overflow-x: auto;
white-space: pre;
}
.md-preview .md-frontmatter {
margin: 0 0 1em;
color: rgb(var(--fg-muted));
}
.md-preview pre code {
padding: 0;
background: none;
font-size: inherit;
}
.md-preview blockquote {
padding: 0 1em;
border-left: 3px solid rgb(var(--border));
color: rgb(var(--fg-muted));
}
.md-preview hr {
margin: 1.5em 0;
border: 0;
border-top: 1px solid rgb(var(--border));
}
.md-preview table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
}
.md-preview th,
.md-preview td {
padding: 0.35em 0.75em;
border: 1px solid rgb(var(--border));
}
.md-preview th {
color: rgb(var(--fg-primary));
font-weight: 600;
background: rgb(var(--surface-2));
}
.md-preview img {
max-width: 100%;
}
/* The run console's terminal attaches to tmux, which repaints the whole pane
on every frame — xterm's own scrollback stays empty forever, so its
viewport scrollbar renders as a groove with no thumb that never moves.
Scrolling there goes through mouse reports to the pane's program, not the
DOM, so hide the dead affordance. Deliberately OUTSIDE `@layer utilities`:
Tailwind purges a layered rule whose class never appears in the source, and
xterm creates `.xterm-viewport` at runtime. */
.xterm-viewport {
scrollbar-width: none;
}
.xterm-viewport::-webkit-scrollbar {
display: none;
}