feat: futuristic theme ladder + stacked landing banner

Add three opt-in dark themes (BooCode+, BooCode Classic, BooCode
Override) plus an in-place Ember polish, on a class-scoped effects
engine: matrix rain, a neon grid field, and frosted glass, all gated
by a localStorage "Animated background" toggle and prefers-reduced-
motion. Extend the server theme_id whitelist so the new ids persist,
and replace the Home landing wordmark with the stacked mascot +
wordmark banner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 14:16:59 +00:00
parent d10d79399b
commit fc4fbb0b7e
21 changed files with 1822 additions and 30 deletions

View File

@@ -26,6 +26,13 @@
@import "./themes/cobalt.css";
@import "./themes/midnight-sapphire.css";
@import "./themes/ember.css";
/* Futuristic theme ladder — token files + effects stubs (Phases 2-5 fill them). */
@import "./themes/boocode-plus.css";
@import "./themes/boocode-classic.css";
@import "./themes/boocode-override.css";
@import "./themes/ember-polish.css";
@import "./themes/effects/boocode-classic-fx.css";
@import "./themes/effects/boocode-override-fx.css";
@custom-variant dark (&:is(.dark *));

View File

@@ -0,0 +1,57 @@
/* BooCode Classic (id: boocode-classic) — dark-only.
Faithful revival of the original BooCode warm-amber-on-black palette
(/opt/boolab data-mode='boocode'). Warm near-black base, orange + amber +
rust accents, deep-rust borders.
Matrix-rain translucency trick (ported from boolab): the rain canvas sits at
z-0 behind the layout. For it to show *through* the UI we make <html> the
opaque page backstop (#0a0604), strip <body>'s background to transparent, and
override the SURFACE tokens to ~8286% opaque rgba so panels bleed the canvas
through their gaps while keeping high-contrast (AA) text. Foreground / accent
/ border colors stay fully opaque.
Anchors: #0a0604 #120a06 #1a0e08 #9a7a5a #f97316 */
/* Light selector intentionally carries dark values — this theme is dark-only.
supportsLight:false in theme.ts means applyTheme always adds the dark class;
this base block is a non-broken fallback if the cascade ever runs without it. */
.theme-boocode-classic,
.theme-boocode-classic.dark {
--background: rgba(10, 6, 4, 0.86);
--foreground: #f5e6d3;
--card: rgba(26, 14, 8, 0.82);
--card-foreground: #f5e6d3;
--popover: rgba(18, 10, 6, 0.82);
--popover-foreground: #f5e6d3;
--primary: #f97316;
--primary-foreground: #0a0604;
--secondary: rgba(36, 20, 8, 0.88);
--secondary-foreground: #f5e6d3;
--muted: rgba(18, 10, 6, 0.82);
--muted-foreground: #9a7a5a;
--accent: #f97316;
--accent-foreground: #0a0604;
--destructive: #dc2626;
--destructive-foreground: #f5e6d3;
--border: #3a1f0c;
--input: #4a2d14;
--ring: #f97316;
--sidebar: rgba(18, 10, 6, 0.82);
--sidebar-foreground: #f5e6d3;
--sidebar-primary: #f97316;
--sidebar-primary-foreground: #0a0604;
--sidebar-accent: color-mix(in oklab, #f97316 16%, transparent);
--sidebar-accent-foreground: #f5e6d3;
--sidebar-border: #3a1f0c;
--sidebar-ring: #f97316;
}
/* Opaque page backstop + transparent body so the fixed rain canvas (z-0) shows
through every translucent surface above it. !important guards against any
inline :root vars losing the cascade. */
html.theme-boocode-classic {
background-color: #0a0604 !important;
}
.theme-boocode-classic body {
background-color: transparent !important;
}

View File

@@ -0,0 +1,59 @@
/* BooCode Override (id: boocode-override) — dark-only.
Full neon cyberpunk: magenta #ff2d78, cyan #00e5ff, violet #9b5de5 on
blue-black #080b14. Knowingly the least calm theme.
Neon-field translucency trick (same shape as Classic, brighter field): the
NeonField canvas sits at z-0 behind the layout. To let it show *through* the
UI, <html> is the opaque page backstop (#080b14), <body> is transparent, and
the SURFACE tokens are overridden to SEMI-TRANSPARENT rgba so panels bleed the
neon grid through their gaps. Foreground / accent / border colours stay fully
opaque. Panel alphas are kept HIGH (0.860.94) because the neon field is much
brighter than the matrix rain — combined with the dimmed canvas (opacity in
NeonField) this keeps every panel/body text run at AA (see the contrast notes
in the role report). Neon is for accents/borders/glow — never body text.
Anchors: #080b14 #0d1120 #0f1525 #8aa9cd #ff2d78 */
/* Light selector intentionally carries dark values — this theme is dark-only.
supportsLight:false in theme.ts means applyTheme always adds the dark class;
this base block is a non-broken fallback if the cascade ever runs without it. */
.theme-boocode-override,
.theme-boocode-override.dark {
--background: rgba(8, 11, 20, 0.88);
--foreground: #cde0ff;
--card: rgba(15, 21, 37, 0.88);
--card-foreground: #cde0ff;
--popover: rgba(13, 17, 32, 0.94);
--popover-foreground: #cde0ff;
--primary: #ff2d78;
--primary-foreground: #080b14;
--secondary: rgba(26, 34, 64, 0.92);
--secondary-foreground: #cde0ff;
--muted: rgba(13, 17, 32, 0.90);
--muted-foreground: #8aa9cd;
--accent: #00e5ff;
--accent-foreground: #080b14;
--destructive: #f0556b;
--destructive-foreground: #ffffff;
--border: #243358;
--input: #243358;
--ring: #ff2d78;
--sidebar: rgba(13, 17, 32, 0.86);
--sidebar-foreground: #cde0ff;
--sidebar-primary: #ff2d78;
--sidebar-primary-foreground: #080b14;
--sidebar-accent: color-mix(in oklab, #ff2d78 16%, transparent);
--sidebar-accent-foreground: #cde0ff;
--sidebar-border: #243358;
--sidebar-ring: #ff2d78;
}
/* Opaque page backstop + transparent body so the fixed NeonField canvas (z-0)
shows through every translucent surface above it. !important guards against
any inline :root vars losing the cascade. */
html.theme-boocode-override {
background-color: #080b14 !important;
}
.theme-boocode-override body {
background-color: transparent !important;
}

View File

@@ -0,0 +1,81 @@
/* BooCode+ (id: boocode-plus) — cool deep-slate + indigo accent #5e6ad2.
Tasteful sci-fi tier: calm, premium, Linear-grade. Dark is the priority
(the user only runs dark mode); the light variant is kept reasonable but
secondary. Token shape mirrors obsidian.css exactly.
Phase 2 effects (frosted-glass chrome, static ambient gradient, indigo glow,
spring transitions) live in the sibling sheet below. The scaffold wired the
two CANVAS themes' *-fx.css in globals.css but never a boocode-plus-fx.css,
and globals.css is out of scope for Phase 2 — so the effects sheet is pulled
in here via a nested @import (Lightning CSS resolves it relative to this
file → ./effects/boocode-plus-fx.css). @import must precede all rules. */
@import "./effects/boocode-plus-fx.css";
/* Light variant — secondary. Deeper indigo (#5159c4) so white button text
clears AA on light, darker muted-foreground (#5c6178) for AA secondary text.
Glass/gradient/glow are dark-only (see the fx sheet), so light is flat. */
.theme-boocode-plus {
--background: #f7f8fc;
--foreground: #1b1e2e;
--card: #ffffff;
--card-foreground: #1b1e2e;
--popover: #ffffff;
--popover-foreground: #1b1e2e;
--primary: #5159c4;
--primary-foreground: #ffffff;
--secondary: #eceef6;
--secondary-foreground: #1b1e2e;
--muted: #eceef6;
--muted-foreground: #5c6178;
--accent: #5159c4;
--accent-foreground: #ffffff;
--destructive: #c8202e;
--destructive-foreground: #ffffff;
--border: #e0e3ef;
--input: #e0e3ef;
--ring: #5159c4;
--sidebar: #eef0f7;
--sidebar-foreground: #1b1e2e;
--sidebar-primary: #5159c4;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: color-mix(in oklab, #5159c4 14%, transparent);
--sidebar-accent-foreground: #1b1e2e;
--sidebar-border: #e0e3ef;
--sidebar-ring: #5159c4;
}
/* Dark variant — the priority. AA-verified against its surfaces:
foreground #e3e6f1 on card #1a1d2e = 13.0:1; muted-foreground #8f95ad on
muted #242838 = 4.9:1; primary/accent-foreground #ffffff on #5e6ad2 = 4.7:1;
destructive #f05252 as text on the dark base = 5.4:1. (primary-foreground
and muted-foreground were lifted off the stub's #0f1117 / #7a7f99, which
failed AA at 4.0:1 and 4.2:1.) */
.theme-boocode-plus.dark {
--background: #0f1117;
--foreground: #e3e6f1;
--card: #1a1d2e;
--card-foreground: #e3e6f1;
--popover: #1a1d2e;
--popover-foreground: #e3e6f1;
--primary: #5e6ad2;
--primary-foreground: #ffffff;
--secondary: #242838;
--secondary-foreground: #e3e6f1;
--muted: #242838;
--muted-foreground: #8f95ad;
--accent: #5e6ad2;
--accent-foreground: #ffffff;
--destructive: #f05252;
--destructive-foreground: #ffffff;
--border: #2a2f44;
--input: #2a2f44;
--ring: #5e6ad2;
--sidebar: #13151d;
--sidebar-foreground: #e3e6f1;
--sidebar-primary: #5e6ad2;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: color-mix(in oklab, #5e6ad2 20%, transparent);
--sidebar-accent-foreground: #e3e6f1;
--sidebar-border: #2a2f44;
--sidebar-ring: #5e6ad2;
}

View File

@@ -0,0 +1,268 @@
/* BooCode Classic — effects layer. Faithful revival of /opt/boolab
data-mode='boocode' terminal-HUD chrome.
Scoping contract (immutable):
- EVERY rule is scoped under `.theme-boocode-classic` so the other 22 themes
are byte-for-byte unaffected.
- EVERY continuous keyframe animation is *additionally* scoped under
`html.bc-anim-on.theme-boocode-classic`. ThemeFx sets `bc-anim-on` only
when the Animated-background toggle is ON *and* prefers-reduced-motion is
not set — so toggling either off removes the class and freezes all motion.
@keyframes names are global (CSS can't scope the at-rule itself); they are
`bc-`-prefixed and only *referenced* under the gated selector, so no
animation runs unless the gate is on.
The matrix-rain canvas lives in components/fx/MatrixRain.tsx (mounted by
ThemeFx). This sheet adds: an Orbitron display wordmark + blinking caret, an
app-wide scanline sweep, orange card hover glow + lift, terminal-frame chrome
on the rails, and the ported boolab `.bc-*` / `.boocode-*` design-system
classes (dormant until a HUD component consumes them). */
.theme-boocode-classic {
--bc-orange: #f97316;
--bc-amber: #fbbf24;
--bc-rust: #c2410c;
--bc-glow-orange: 0 0 24px color-mix(in srgb, var(--bc-orange) 32%, transparent);
--bc-glow-soft: 0 0 40px color-mix(in srgb, var(--bc-orange) 14%, transparent);
}
/* ── Orbitron display wordmark ────────────────────────────────────────────
Applied to the "BooCode" text heading via the additive `boocode-display`
class (the only text wordmark — the sidebar wordmark is an <img>, which a
font can't restyle). Orbitron 800 is JS-imported in main.tsx. */
.theme-boocode-classic .boocode-display {
font-family: 'Orbitron', var(--font-sans);
font-weight: 800;
letter-spacing: 0.06em;
color: var(--bc-orange);
text-shadow: 0 0 14px color-mix(in srgb, var(--bc-orange) 45%, transparent);
}
/* Terminal cursor after the wordmark. Always rendered (a solid block when
motion is off — boolab's reduced-motion behaviour); blinks only when gated. */
.theme-boocode-classic .boocode-display::after {
content: '▮';
margin-left: 0.12em;
color: var(--bc-amber);
opacity: 1;
}
html.bc-anim-on.theme-boocode-classic .boocode-display::after {
animation: bc-caret-blink 1s steps(1, end) infinite;
}
/* ── App-wide scanline sweep ──────────────────────────────────────────────
A soft warm band that drifts down the viewport on a calm 8s cycle.
`position: fixed` keeps it viewport-locked (never creates document overflow);
mix-blend-mode: screen only lightens, so text underneath stays legible.
Defined ONLY under the gate → vanishes entirely when motion is off (the warm
palette + static rain-off state is the reduced-motion fallback).
`.h-dvh.bg-background` is unique to the AppShell root (App.tsx). */
html.bc-anim-on.theme-boocode-classic .h-dvh.bg-background::after {
content: '';
position: fixed;
inset: 0;
z-index: 2;
pointer-events: none;
background: linear-gradient(
180deg,
transparent 46%,
color-mix(in srgb, var(--bc-orange) 9%, transparent) 50%,
transparent 54%
);
mix-blend-mode: screen;
animation: bc-scanline 8s linear infinite;
will-change: transform;
}
/* ── Card hover glow + lift ───────────────────────────────────────────────
The shadcn Card primitive (`data-slot="card"`). The orange rim + drop glow
apply on hover under any condition (hover feedback is acceptable under
reduced motion); the transition timing and the 1px lift are gated so motion
off = instant, no travel. */
.theme-boocode-classic [data-slot="card"]:hover {
border-color: color-mix(in srgb, var(--bc-orange) 55%, transparent);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--bc-orange) 22%, transparent),
0 6px 18px -8px color-mix(in srgb, var(--bc-orange) 40%, transparent);
}
html.bc-anim-on.theme-boocode-classic [data-slot="card"] {
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}
html.bc-anim-on.theme-boocode-classic [data-slot="card"]:hover {
transform: translateY(-1px);
}
/* ── Terminal-frame chrome on the rails ───────────────────────────────────
Monospace + a hairline orange edge-light on the sidebar (vertical list that
already truncates — low overflow risk). Pane top-bars are intentionally left
alone: they are crowded control rows where a wider mono font risks wrapping. */
.theme-boocode-classic .bg-sidebar {
font-family: var(--font-mono);
box-shadow: inset -1px 0 0 0 color-mix(in srgb, var(--bc-orange) 8%, transparent);
}
/* Floating surfaces (menus / dialogs / popovers) are intentionally NOT given an
inset box-shadow here — that would clobber shadcn's `shadow-md` elevation
(higher specificity) and flatten menus over the rain. The warm `--popover` /
`--border` tokens already theme them. */
/* ──────────────────────────────────────────────────────────────────────────
Ported boolab design-system classes. These are faithful copies of the
/opt/boolab `.bc-*` / `.boocode-*` chrome. No component in this repo renders
these class names yet (boolab's RepoStatusBar / HUD components don't exist
here), so they are DORMANT — shipped so the Classic design system is complete
and any future HUD element (prompt line, status pill, kbd hint, breadcrumb)
lights up automatically. All scoped to the theme; animations gated.
────────────────────────────────────────────────────────────────────────── */
/* Terminal prompt line: `$ boocode @host: path (branch)` */
.theme-boocode-classic .bc-prompt-line {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
row-gap: 0.25rem;
font-family: var(--font-mono);
font-size: 0.8125rem;
color: var(--muted-foreground);
padding: 0.5rem 0.75rem;
background: var(--popover);
border-bottom: 1px solid var(--border);
}
.theme-boocode-classic .bc-prompt-host { color: var(--bc-orange); }
.theme-boocode-classic .bc-prompt-branch { color: var(--bc-orange); opacity: 0.8; }
.theme-boocode-classic .bc-prompt-dollar {
color: var(--bc-orange);
text-shadow: 0 0 6px var(--bc-orange);
margin-right: 0.25rem;
}
/* Standalone blinking block caret. */
.theme-boocode-classic .bc-caret {
display: inline-block;
width: 0.6em;
height: 1em;
background: currentColor;
vertical-align: text-bottom;
margin-left: 0.1em;
}
html.bc-anim-on.theme-boocode-classic .bc-caret {
animation: bc-caret-blink 1s steps(1, end) infinite;
}
/* IDLE / SYNCING / ERROR status pills. */
.theme-boocode-classic .bc-status-pill {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.1rem 0.5rem;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--card);
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.12em;
font-family: var(--font-mono);
}
.theme-boocode-classic .bc-status-idle { color: #7ae07a; border-color: rgba(122, 224, 122, 0.4); }
.theme-boocode-classic .bc-status-syncing {
color: var(--bc-orange);
border-color: color-mix(in srgb, var(--bc-orange) 60%, transparent);
}
.theme-boocode-classic .bc-status-error { color: #ff6b6b; border-color: rgba(255, 107, 107, 0.5); }
.theme-boocode-classic .bc-status-syncing::before {
content: '▮';
color: var(--bc-orange);
}
html.bc-anim-on.theme-boocode-classic .bc-status-syncing::before {
animation: bc-caret-blink 1s steps(1, end) infinite;
}
/* Keyboard hint chip. */
.theme-boocode-classic .bc-key-hint {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.25rem;
padding: 0 0.3rem;
height: 1.1rem;
border: 1px solid var(--border);
border-radius: 0.25rem;
font-family: var(--font-mono);
font-size: 0.625rem;
color: var(--muted-foreground);
background: var(--popover);
}
/* Uppercase orange breadcrumb / overline. */
.theme-boocode-classic .boocode-breadcrumb {
font-family: 'Orbitron', var(--font-sans);
font-size: 0.6875rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--bc-orange);
text-shadow: 0 0 8px color-mix(in srgb, var(--bc-orange) 40%, transparent);
}
/* Inset terminal-frame border. */
.theme-boocode-classic .boocode-terminal-frame {
border: 1px solid var(--border);
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--bc-orange) 5%, transparent),
0 0 0 1px #0a0604;
}
/* Card hover glow + lift utility (boolab `.bc-card`), for non-shadcn surfaces. */
.theme-boocode-classic .bc-card {
position: relative;
border: 1px solid color-mix(in srgb, var(--bc-orange) 22%, transparent);
background: var(--card);
padding: 1rem;
border-radius: 0.375rem;
overflow: hidden;
}
.theme-boocode-classic .bc-card:hover {
border-color: color-mix(in srgb, var(--bc-orange) 55%, transparent);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--bc-orange) 22%, transparent),
0 6px 18px -8px color-mix(in srgb, var(--bc-orange) 40%, transparent);
}
html.bc-anim-on.theme-boocode-classic .bc-card {
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}
html.bc-anim-on.theme-boocode-classic .bc-card:hover {
transform: translateY(-1px);
}
/* ── Keyframes (global names, referenced only under the gate above) ───────── */
@keyframes bc-caret-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
@keyframes bc-scanline {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
/* ── Reduced-motion belt-and-suspenders ───────────────────────────────────
bc-anim-on already excludes reduced-motion (ThemeFx), but if the class ever
lingered, this hard-stops every animation and the lift transform. */
@media (prefers-reduced-motion: reduce) {
.theme-boocode-classic .boocode-display::after,
.theme-boocode-classic .bc-caret,
.theme-boocode-classic .bc-status-syncing::before {
animation: none;
opacity: 1;
}
.theme-boocode-classic .h-dvh.bg-background::after {
animation: none;
display: none;
}
.theme-boocode-classic [data-slot="card"],
.theme-boocode-classic .bc-card {
transition: none;
}
.theme-boocode-classic [data-slot="card"]:hover,
.theme-boocode-classic .bc-card:hover {
transform: none;
}
}

View File

@@ -0,0 +1,288 @@
/* BooCode Override — effects layer. Full neon cyberpunk: heavy bloom/glow,
chromatic-glitch wordmark, glitch-on-hover, strong scanlines, neon border
pulses. Magenta #ff2d78 + cyan #00e5ff + violet #9b5de5 on blue-black.
Scoping contract (immutable, same as boocode-classic-fx.css):
- EVERY rule is scoped under `.theme-boocode-override` so the other 22 themes
are byte-for-byte unaffected.
- EVERY continuous keyframe animation is *additionally* scoped under
`html.bc-anim-on.theme-boocode-override`. ThemeFx sets `bc-anim-on` only
when the Animated-background toggle is ON *and* prefers-reduced-motion is
not set — so toggling either off removes the class and freezes all motion.
@keyframes names are global (CSS can't scope the at-rule itself); they are
`bco-`-prefixed and only *referenced* under the gated selector.
Static vs. gated split:
- STATIC (always, under .theme-boocode-override): the neon palette, a bloom
vignette painted on the app-shell background, static neon rims/glows on the
primary action / cards / inputs, and a neon focus ring. This is the
"static tint" fallback shown when the toggle is OFF or reduced-motion is on.
- GATED (under html.bc-anim-on.theme-boocode-override): the NeonField canvas
(mounted by ThemeFx), the scanline texture + moving sweep, the wordmark
chromatic glitch, the hover glitch jitter, and the idle neon pulse.
AA: neon is used ONLY for accents/borders/glow and the (large display)
wordmark — never body text. Body/panel text keeps the light #cde0ff /
#8aa9cd tokens over the high-alpha translucent surfaces (see
boocode-override.css). Glitch uses transform / text-shadow / clip-path only,
so it never reflows layout. */
.theme-boocode-override {
--bco-magenta: #ff2d78;
--bco-cyan: #00e5ff;
--bco-violet: #9b5de5;
--bco-glow-magenta: 0 0 22px color-mix(in srgb, var(--bco-magenta) 42%, transparent);
--bco-glow-cyan: 0 0 22px color-mix(in srgb, var(--bco-cyan) 38%, transparent);
}
/* ── Bloom vignette on the app shell ──────────────────────────────────────
Painted as background-image on the AppShell root (`.h-dvh.bg-background`,
unique to App.tsx). The background-COLOR stays the translucent --background
token so the NeonField canvas (z-0, behind the z-10 root) still shows through
the panel gaps; these radial neon pools sit over the field for an atmospheric
bloom. Static → this is part of the reduced-motion / toggle-off fallback. */
.theme-boocode-override .h-dvh.bg-background {
background-color: var(--background);
background-image:
radial-gradient(125% 90% at 50% 8%,
color-mix(in oklab, var(--bco-magenta) 12%, transparent), transparent 46%),
radial-gradient(120% 85% at 6% 102%,
color-mix(in oklab, var(--bco-cyan) 11%, transparent), transparent 50%),
radial-gradient(120% 95% at 100% 100%,
color-mix(in oklab, var(--bco-violet) 12%, transparent), transparent 52%),
radial-gradient(140% 120% at 50% 50%,
transparent 60%, rgba(2, 4, 10, 0.55) 100%);
background-repeat: no-repeat;
}
/* ── Strong scanlines (gated) ─────────────────────────────────────────────
::before = a fine static CRT scanline texture; ::after = a bright neon band
that sweeps the viewport. Both are `position: fixed` (never create document
overflow), `pointer-events: none`, and use `mix-blend-mode: screen` so they
only LIGHTEN — text underneath never loses contrast. Both live ONLY under the
gate, so motion-off / reduced-motion removes scanlines entirely (the bloom
vignette above is the static fallback). z-index:2/3 overlays the static panels
inside the shell; portaled dialogs (separate stacking context) stay clean. */
html.bc-anim-on.theme-boocode-override .h-dvh.bg-background::before {
content: '';
position: fixed;
inset: 0;
z-index: 2;
pointer-events: none;
background-image: repeating-linear-gradient(
0deg,
color-mix(in srgb, var(--bco-cyan) 9%, transparent) 0px,
color-mix(in srgb, var(--bco-cyan) 9%, transparent) 1px,
transparent 1px,
transparent 3px
);
mix-blend-mode: screen;
opacity: 0.55;
}
html.bc-anim-on.theme-boocode-override .h-dvh.bg-background::after {
content: '';
position: fixed;
inset: 0;
z-index: 3;
pointer-events: none;
background: linear-gradient(
180deg,
transparent 44%,
color-mix(in srgb, var(--bco-cyan) 10%, transparent) 49%,
color-mix(in srgb, var(--bco-magenta) 12%, transparent) 51%,
transparent 56%
);
mix-blend-mode: screen;
animation: bco-scan-sweep 7s linear infinite;
will-change: transform;
}
/* ── Neon chromatic-glitch wordmark ───────────────────────────────────────
The "BooCode" display heading (Home.tsx, additive `boocode-display` class —
the same hook BooCode Classic restyles; only one theme is ever active).
Orbitron 800 (JS-imported in main.tsx). A near-white core with stacked cyan +
magenta glow gives the neon tube look; the gated glitch jitters the colour
split + a clip slice via transform/text-shadow only → zero layout shift. */
.theme-boocode-override .boocode-display {
font-family: 'Orbitron', var(--font-sans);
font-weight: 800;
letter-spacing: 0.08em;
color: #eafaff;
text-shadow:
0 0 2px color-mix(in srgb, var(--bco-cyan) 80%, transparent),
-0.02em 0 0 color-mix(in srgb, var(--bco-magenta) 70%, transparent),
0.02em 0 0 color-mix(in srgb, var(--bco-cyan) 70%, transparent),
0 0 18px color-mix(in srgb, var(--bco-cyan) 55%, transparent),
0 0 34px color-mix(in srgb, var(--bco-magenta) 40%, transparent);
}
html.bc-anim-on.theme-boocode-override .boocode-display {
animation: bco-glitch 5.5s steps(1, end) infinite;
}
/* ── Primary action: neon rim + idle pulse + hover bloom + press squash ────
The default-variant Button (Send / primary confirm). Static magenta rim +
drop glow (always); a gentle idle pulse and a hover glitch jitter under the
gate; box-shadow / transform / filter only → never reflows. */
.theme-boocode-override [data-slot="button"][data-variant="default"] {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 55%, transparent),
0 4px 20px -6px color-mix(in srgb, var(--bco-magenta) 60%, transparent);
}
.theme-boocode-override [data-slot="button"][data-variant="default"]:not([disabled]):hover {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 85%, transparent),
0 0 16px -2px color-mix(in srgb, var(--bco-magenta) 70%, transparent),
0 8px 30px -6px color-mix(in srgb, var(--bco-cyan) 55%, transparent);
filter: brightness(1.08);
}
html.bc-anim-on.theme-boocode-override [data-slot="button"][data-variant="default"] {
transition: box-shadow 200ms ease, transform 120ms ease, filter 160ms ease;
animation: bco-pulse 3.4s ease-in-out infinite;
}
html.bc-anim-on.theme-boocode-override [data-slot="button"][data-variant="default"]:not([disabled]):hover {
animation: bco-hover-glitch 320ms steps(1, end) 1;
}
html.bc-anim-on.theme-boocode-override [data-slot="button"][data-variant="default"]:not([disabled]):active {
transform: translateY(0.5px) scale(0.985);
}
/* ── Cards: neon edge + hover glow/lift ───────────────────────────────────
shadcn Card primitive (`data-slot="card"`). Static cyan hairline; on hover a
cyan/magenta rim glow + 1px lift (lift + transition gated). */
.theme-boocode-override [data-slot="card"] {
box-shadow: 0 0 0 1px color-mix(in srgb, var(--bco-cyan) 12%, transparent);
}
.theme-boocode-override [data-slot="card"]:hover {
border-color: color-mix(in srgb, var(--bco-cyan) 55%, transparent);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--bco-cyan) 38%, transparent),
0 0 22px -6px color-mix(in srgb, var(--bco-magenta) 45%, transparent),
0 8px 24px -10px color-mix(in srgb, var(--bco-cyan) 50%, transparent);
}
html.bc-anim-on.theme-boocode-override [data-slot="card"] {
transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
}
html.bc-anim-on.theme-boocode-override [data-slot="card"]:hover {
transform: translateY(-1px);
}
/* ── Sidebar edge-light + neon focus ring ─────────────────────────────────
A static cyan inset hairline down the sidebar's trailing edge; a magenta neon
focus ring on inputs/buttons (focus-visible only, static — focus feedback is
acceptable under reduced motion). */
.theme-boocode-override .bg-sidebar {
box-shadow: inset -1px 0 0 0 color-mix(in srgb, var(--bco-cyan) 16%, transparent);
}
.theme-boocode-override [data-slot="input"]:focus-visible,
.theme-boocode-override [data-slot="button"]:focus-visible,
.theme-boocode-override [data-slot="textarea"]:focus-visible {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 70%, transparent),
0 0 14px -2px color-mix(in srgb, var(--bco-magenta) 55%, transparent);
outline: none;
}
/* ── Links / accent text glow ─────────────────────────────────────────────
Anchors inside content get a soft cyan glow on hover — accent, not body. */
.theme-boocode-override a:hover {
text-shadow: 0 0 10px color-mix(in srgb, var(--bco-cyan) 55%, transparent);
}
/* ── Keyframes (global names, referenced only under the gate above) ───────── */
/* Scanline sweep: a bright neon band drifts top→bottom. */
@keyframes bco-scan-sweep {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
/* Idle neon pulse on the primary action: the glow swells and settles. */
@keyframes bco-pulse {
0%, 100% {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 55%, transparent),
0 4px 20px -6px color-mix(in srgb, var(--bco-magenta) 55%, transparent);
}
50% {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 80%, transparent),
0 0 18px -2px color-mix(in srgb, var(--bco-magenta) 60%, transparent),
0 6px 26px -6px color-mix(in srgb, var(--bco-cyan) 45%, transparent);
}
}
/* Wordmark chromatic glitch: mostly steady, with brief jitter bursts. Only
transform / text-shadow / clip-path change → no reflow. */
@keyframes bco-glitch {
0%, 88%, 100% {
transform: translate3d(0, 0, 0);
clip-path: none;
text-shadow:
0 0 2px color-mix(in srgb, var(--bco-cyan) 80%, transparent),
-0.02em 0 0 color-mix(in srgb, var(--bco-magenta) 70%, transparent),
0.02em 0 0 color-mix(in srgb, var(--bco-cyan) 70%, transparent),
0 0 18px color-mix(in srgb, var(--bco-cyan) 55%, transparent),
0 0 34px color-mix(in srgb, var(--bco-magenta) 40%, transparent);
}
90% {
transform: translate3d(-2px, 0, 0);
clip-path: inset(8% 0 62% 0);
text-shadow:
-0.08em 0 0 color-mix(in srgb, var(--bco-magenta) 90%, transparent),
0.08em 0 0 color-mix(in srgb, var(--bco-cyan) 90%, transparent),
0 0 22px color-mix(in srgb, var(--bco-cyan) 60%, transparent);
}
92% {
transform: translate3d(2px, 0, 0);
clip-path: inset(54% 0 18% 0);
text-shadow:
0.1em 0 0 color-mix(in srgb, var(--bco-magenta) 90%, transparent),
-0.06em 0 0 color-mix(in srgb, var(--bco-cyan) 85%, transparent),
0 0 22px color-mix(in srgb, var(--bco-magenta) 60%, transparent);
}
94% {
transform: translate3d(-1px, 0, 0);
clip-path: inset(34% 0 40% 0);
text-shadow:
-0.05em 0 0 color-mix(in srgb, var(--bco-cyan) 85%, transparent),
0.05em 0 0 color-mix(in srgb, var(--bco-magenta) 85%, transparent);
}
}
/* Hover glitch on the primary button: a single quick colour-split jitter. */
@keyframes bco-hover-glitch {
0% { transform: translate3d(0, 0, 0); }
25% { transform: translate3d(-1.5px, 0, 0); }
50% { transform: translate3d(1.5px, 0, 0); }
75% { transform: translate3d(-0.5px, 0, 0); }
100% { transform: translate3d(0, 0, 0); }
}
/* ── Reduced-motion belt-and-suspenders ───────────────────────────────────
bc-anim-on already excludes reduced-motion (ThemeFx), but if the class ever
lingered, this hard-stops every animation and the lift/jitter transforms. The
static neon rims, bloom vignette and focus ring remain (the "static tint"). */
@media (prefers-reduced-motion: reduce) {
.theme-boocode-override .boocode-display,
.theme-boocode-override .h-dvh.bg-background::before,
.theme-boocode-override .h-dvh.bg-background::after,
.theme-boocode-override [data-slot="button"][data-variant="default"],
.theme-boocode-override [data-slot="button"][data-variant="default"]:hover {
animation: none;
}
.theme-boocode-override .h-dvh.bg-background::after {
display: none;
}
.theme-boocode-override .boocode-display {
transform: none;
clip-path: none;
}
.theme-boocode-override [data-slot="card"],
.theme-boocode-override [data-slot="button"][data-variant="default"] {
transition: none;
}
.theme-boocode-override [data-slot="card"]:hover,
.theme-boocode-override [data-slot="button"][data-variant="default"]:active {
transform: none;
}
}

View File

@@ -0,0 +1,205 @@
/* BooCode+ — effects layer. Tasteful sci-fi: calm, premium, Linear-grade.
EVERY rule is scoped under `.theme-boocode-plus.dark` so (a) the other 22
themes are byte-for-byte unaffected and (b) the light variant stays a clean
flat slate — dark is the priority, so the glass/gradient/glow ship dark-only.
No canvas, no animation loop, no scanlines. Just: a faint STATIC ambient
gradient on the app background, frosted glass on CHROME ONLY (rails, menus,
dialogs, popovers, the composer, pane top-bars), a restrained indigo glow on
the primary action, and spring-eased transitions on chrome state changes.
iOS / quality guardrails honored:
- backdrop-blur capped at 812px; no nested blur-on-blur (the dialog
overlay's own backdrop-blur is neutralized so only the dialog CONTENT
frosts — one layer).
- opaque fallback: every glass rule lives inside @supports(backdrop-filter),
so a browser without it keeps the plain opaque Tailwind utility
(bg-sidebar/bg-popover/bg-card = solid token). prefers-reduced-transparency
forces the same opaque path.
- spring via cubic-bezier (NOT the linear() function — Safari <17.2 gap),
confined to box-shadow / transform / color / filter — never layout props.
- chat messages, tool-call cards and code blocks (bg-muted/30·/20, bg-card
bubbles) are deliberately NOT targeted — glass never sits behind reading
content; the ambient gradient behind it stays faint & opaque-based (AA).
Glass targets are stable, verified hooks: bg-sidebar (both rails), bg-popover
(all menus/dialogs/popovers/sheets), the composer's unique
focus-within:ring-primary/15 box, [data-slot=button][data-variant=default]
(the primary action), and the compound .border-b.bg-muted/30·/20 (pane
top-bars — distinct from message bubbles, which use .border.rounded-lg). */
.theme-boocode-plus.dark {
/* Spring curves. --bcp-spring overshoots a touch (press/glow bloom);
--bcp-ease is a smooth Linear-style decel for color/opacity. */
--bcp-spring: cubic-bezier(0.34, 1.42, 0.5, 1);
--bcp-ease: cubic-bezier(0.32, 0.72, 0, 1);
--bcp-blur: 10px;
}
/* ── Static ambient gradient ─────────────────────────────────────────────
Painted on the app-shell root (.h-dvh is unique to App.tsx). Opaque base
(var(--background)) + three faint indigo radials = depth without cost. The
MessageList scroll area is transparent, so the gradient reads faintly behind
chat — but it's static and opaque-based: the brightest stop (indigo @14% over
#0f1117) still gives ~10:1 for #e3e6f1 body text. The glass rails blur it for
a premium parallax-of-light feel. No background-attachment:fixed (iOS jank);
.h-dvh doesn't scroll, so the field is already viewport-stable. */
.theme-boocode-plus.dark .h-dvh {
background-color: var(--background);
background-image:
radial-gradient(1200px 760px at 8% -12%,
color-mix(in oklab, #5e6ad2 14%, transparent), transparent 58%),
radial-gradient(1000px 680px at 102% 4%,
color-mix(in oklab, #5a73d8 10%, transparent), transparent 54%),
radial-gradient(1100px 900px at 50% 128%,
color-mix(in oklab, #2c3270 16%, transparent), transparent 60%);
background-repeat: no-repeat;
}
/* ── Frosted glass on chrome ─────────────────────────────────────────────
Progressive enhancement: only inside @supports does the surface go
translucent + blur. Without backdrop-filter support the rules vanish and the
plain opaque utility (solid token bg) shows — that IS the opaque fallback.
Each background has an rgba() line first (covers the rare browser that has
backdrop-filter but not color-mix, e.g. Safari 15) then the color-mix line. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
/* Side rails (ProjectSidebar + RightRail). Blur the ambient gradient behind
them → the signature glass plane. Kept at 80% so the dense nav text stays
crisp; the blur + saturate sells the effect, not heavy transparency. */
.theme-boocode-plus.dark .bg-sidebar {
background-color: rgba(19, 21, 29, 0.80);
background-color: color-mix(in oklab, var(--sidebar) 80%, transparent);
-webkit-backdrop-filter: blur(var(--bcp-blur)) saturate(140%);
backdrop-filter: blur(var(--bcp-blur)) saturate(140%);
}
/* Every floating surface: dialogs, dropdown / context / sub menus, the
@-mention & slash pickers, the mobile bottom sheet, the message-actions
menu. All carry bg-popover and all float over content → one clean blur. */
.theme-boocode-plus.dark .bg-popover {
background-color: rgba(26, 29, 46, 0.84);
background-color: color-mix(in oklab, var(--popover) 84%, transparent);
-webkit-backdrop-filter: blur(12px) saturate(150%);
backdrop-filter: blur(12px) saturate(150%);
}
/* The composer message box (ChatInput) — unique focus-within ring hook.
Frosts the tail of the conversation as it scrolls beneath. 82% keeps the
textarea text readable even over a bright code block underneath. */
.theme-boocode-plus.dark .focus-within\:ring-primary\/15 {
background-color: rgba(26, 29, 46, 0.82);
background-color: color-mix(in oklab, var(--card) 82%, transparent);
-webkit-backdrop-filter: blur(var(--bcp-blur)) saturate(140%);
backdrop-filter: blur(var(--bcp-blur)) saturate(140%);
}
/* Pane top-bars (Coder/Workspace/terminal-hotkey/artifact headers). The
compound .border-b.bg-muted/N selector excludes chat bubbles & tool cards
(those use .border.rounded-lg). Lightest blur (8px) + an inset top hairline
for the edge-lit premium feel. */
.theme-boocode-plus.dark .border-b.bg-muted\/30,
.theme-boocode-plus.dark .border-b.bg-muted\/20 {
background-color: rgba(28, 32, 47, 0.62);
background-color: color-mix(in oklab, var(--muted) 62%, transparent);
-webkit-backdrop-filter: blur(8px) saturate(130%);
backdrop-filter: blur(8px) saturate(130%);
box-shadow: inset 0 1px 0 0 color-mix(in oklab, #aab2ff 8%, transparent);
}
}
/* Dialog scrim: deepen the dim (the stock bg-black/10 is too light to anchor a
frosted modal) and kill its own backdrop-blur so the only blur layer is the
dialog CONTENT above — no nested blur-on-blur. Unconditional: when blur is
unsupported the stock overlay had no blur anyway, and the deeper scrim is
harmless. */
.theme-boocode-plus.dark [data-slot="dialog-overlay"] {
background-color: rgba(7, 8, 14, 0.55);
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
/* ── Restrained indigo glow on the primary action ────────────────────────
The default-variant Button only (Send, primary confirm). Resting: a hairline
indigo rim + soft drop glow. Hover: the glow blooms with the spring curve.
Press: a composited squash. box-shadow/transform/filter only — no reflow. */
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"] {
box-shadow:
0 0 0 1px color-mix(in oklab, #5e6ad2 38%, transparent),
0 4px 16px -6px color-mix(in oklab, #5e6ad2 50%, transparent);
transition:
box-shadow 0.28s var(--bcp-spring),
transform 0.2s var(--bcp-spring),
filter 0.2s var(--bcp-ease),
background-color 0.18s var(--bcp-ease);
}
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"]:not([disabled]):hover {
box-shadow:
0 0 0 1px color-mix(in oklab, #5e6ad2 60%, transparent),
0 8px 26px -6px color-mix(in oklab, #5e6ad2 68%, transparent);
filter: brightness(1.06);
}
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"]:not([disabled]):active {
transform: translateY(0.5px) scale(0.985);
}
/* ── Spring-eased state on the rest of the chrome ─────────────────────────
Smooth (non-overshoot) color/bg transitions on menu items and the composer
focus, so hovers and focus feel intentional, not instant. Color props only. */
.theme-boocode-plus.dark [data-slot="dropdown-menu-item"],
.theme-boocode-plus.dark [data-slot="context-menu-item"],
.theme-boocode-plus.dark [data-slot="dropdown-menu-sub-trigger"],
.theme-boocode-plus.dark [data-slot="context-menu-sub-trigger"] {
transition:
background-color 0.16s var(--bcp-ease),
color 0.16s var(--bcp-ease);
}
.theme-boocode-plus.dark .focus-within\:ring-primary\/15 {
transition:
border-color 0.22s var(--bcp-ease),
box-shadow 0.22s var(--bcp-spring),
background-color 0.22s var(--bcp-ease);
}
/* ── Accessibility fallbacks ──────────────────────────────────────────────
Reduced transparency: drop every glass surface to its solid token bg and
remove all blur. Declared after the @supports block so it wins. */
@media (prefers-reduced-transparency: reduce) {
.theme-boocode-plus.dark .bg-sidebar {
background-color: var(--sidebar);
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
.theme-boocode-plus.dark .bg-popover {
background-color: var(--popover);
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
.theme-boocode-plus.dark .focus-within\:ring-primary\/15 {
background-color: var(--card);
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
.theme-boocode-plus.dark .border-b.bg-muted\/30,
.theme-boocode-plus.dark .border-b.bg-muted\/20 {
background-color: var(--muted);
-webkit-backdrop-filter: none;
backdrop-filter: none;
box-shadow: none;
}
}
/* Reduced motion: no spring/transition, no press transform. The ambient
gradient is static, so it (correctly) stays. */
@media (prefers-reduced-motion: reduce) {
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"],
.theme-boocode-plus.dark [data-slot="dropdown-menu-item"],
.theme-boocode-plus.dark [data-slot="context-menu-item"],
.theme-boocode-plus.dark [data-slot="dropdown-menu-sub-trigger"],
.theme-boocode-plus.dark [data-slot="context-menu-sub-trigger"],
.theme-boocode-plus.dark .focus-within\:ring-primary\/15 {
transition: none;
}
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"]:not([disabled]):active {
transform: none;
}
}

View File

@@ -0,0 +1,153 @@
/* BooCode (Ember) polish layer — a restrained refinement of the default
theme, NOT a redesign. Keeps the orange-on-charcoal identity; just gives
the flat chrome gentle depth, the primary action a warm glow, a crisp
keyboard focus ring, and a hair tighter title tracking. Calm enough to
live in for hours — no background animation, no scanlines, no canvas.
SCOPE DISCIPLINE (the cross-theme regression trap): EVERY rule below is
scoped under `.theme-ember`, so the other 22 themes are byte-for-byte
unaffected. No global body, html, universal, or heading rule; no global
line-height or letter-spacing change (those reflow every theme). The few
`.theme-ember.dark` rules are still ember-only — both classes must be
present — so they never leak either.
This sheet is imported UNLAYERED (globals.css, after the tailwindcss
import), so these rules win over Tailwind's layered utilities. A bare
box-shadow therefore REPLACES a component's `ring-1` hairline — so every
elevation below composes the hairline back in via --ember-hairline (except
the composer, which already has a real CSS `border`).
Hooks are the same stable ones BooCode+ uses: [data-slot=card],
[data-slot=dialog-content], the menu [data-slot=*-content]s, the composer's
unique `.focus-within:ring-primary/15` box, and the primary
[data-slot=button][data-variant=default]. */
.theme-ember {
/* Hairline = the stock `ring-foreground/10` edge, re-expressed as a shadow
so it can ride alongside the elevation shadows in one box-shadow list. */
--ember-hairline: 0 0 0 1px color-mix(in oklab, var(--foreground) 9%, transparent);
/* Consistent elevation scale. Resting surfaces (cards, composer) → 1;
floating chrome (menus) → 2; the modal layer (dialog) → 3. Drop shadows
are the primary depth cue in light mode; tuned calm (Material-ish alphas),
never loud. */
--ember-shadow-1: 0 1px 2px -1px rgb(0 0 0 / 0.40), 0 3px 8px -3px rgb(0 0 0 / 0.28);
--ember-shadow-2: 0 3px 8px -3px rgb(0 0 0 / 0.44), 0 12px 28px -8px rgb(0 0 0 / 0.36);
--ember-shadow-3: 0 6px 14px -4px rgb(0 0 0 / 0.50), 0 24px 56px -14px rgb(0 0 0 / 0.44);
/* Edge-light: a no-op in light mode (drop shadows do the work there). */
--ember-edge: 0 0 transparent;
/* Restrained warm glow for the PRIMARY action only. Rest = a faint orange
rim + soft drop; hover blooms. Colored shadows read on dark charcoal where
black shadows fade, so the primary stays legible as "the" action. */
--ember-glow-rest:
0 0 0 1px color-mix(in oklab, var(--primary) 22%, transparent),
0 2px 8px -3px color-mix(in oklab, var(--primary) 32%, transparent),
0 1px 2px -1px rgb(0 0 0 / 0.40);
--ember-glow-hover:
0 0 0 1px color-mix(in oklab, var(--primary) 40%, transparent),
0 5px 18px -4px color-mix(in oklab, var(--primary) 55%, transparent),
0 1px 2px -1px rgb(0 0 0 / 0.40);
/* A gentle decel curve (no overshoot) — intentionally calmer than
BooCode+'s spring, to suit a theme meant for long sessions. */
--ember-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
/* On near-black, drop shadows barely read — a hair of warm top edge-light is
the real "lifted" cue in dark mode. Tinted to the warm foreground so it
stays on-brand and never cool. */
.theme-ember.dark {
--ember-edge: inset 0 1px 0 0 color-mix(in oklab, var(--foreground) 7%, transparent);
}
/* ── Elevation ────────────────────────────────────────────────────────────
Each surface keeps its edge (hairline, re-composed) and gains depth. */
/* Cards — resting elevation. */
.theme-ember [data-slot="card"] {
box-shadow: var(--ember-edge), var(--ember-hairline), var(--ember-shadow-1);
}
/* The composer message box (ChatInput) — its own unique focus-within hook.
It already has a real CSS `border`, so NO hairline here (that would double
the edge). Only at rest: on focus-within the stock orange ring/border takes
over, so we step aside with :not(:focus-within). */
.theme-ember .focus-within\:ring-primary\/15:not(:focus-within) {
box-shadow: var(--ember-edge), var(--ember-shadow-1);
}
/* Floating menus — dropdown / context / their submenus. (Replaces the stock
shadow-md/lg with the consistent themed scale.) */
.theme-ember [data-slot="dropdown-menu-content"],
.theme-ember [data-slot="dropdown-menu-sub-content"],
.theme-ember [data-slot="context-menu-content"],
.theme-ember [data-slot="context-menu-sub-content"] {
box-shadow: var(--ember-edge), var(--ember-hairline), var(--ember-shadow-2);
}
/* Dialog — the modal layer, strongest depth to separate it from the (light)
scrim. */
.theme-ember [data-slot="dialog-content"] {
box-shadow: var(--ember-edge), var(--ember-hairline), var(--ember-shadow-3);
}
/* ── Primary action glow ──────────────────────────────────────────────────
The default-variant Button only (Send, primary confirm). box-shadow /
transform / filter only — never a layout-triggering prop, so no shift. */
.theme-ember [data-slot="button"][data-variant="default"] {
box-shadow: var(--ember-glow-rest);
transition:
box-shadow 0.26s var(--ember-ease),
transform 0.18s var(--ember-ease),
filter 0.18s var(--ember-ease);
}
.theme-ember [data-slot="button"][data-variant="default"]:not([disabled]):hover {
box-shadow: var(--ember-glow-hover);
filter: brightness(1.04);
}
.theme-ember [data-slot="button"][data-variant="default"]:not([disabled]):active {
transform: translateY(0.5px) scale(0.99);
}
/* ── Keyboard focus ring ──────────────────────────────────────────────────
A crisp, fully-opaque 2px orange outline replaces the stock soft 3px/50%
ring — cleaner and more visible for keyboard nav. The outline follows each
control's border-radius automatically. Form fields are intentionally left
alone (the composer owns its focus-within ring; inputs keep theirs). */
.theme-ember button:focus-visible,
.theme-ember [data-slot="button"]:focus-visible,
.theme-ember a:focus-visible,
.theme-ember [role="switch"]:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
/* Drop the stock soft box-shadow ring on non-primary buttons so only the crisp
outline shows. (The primary button keeps its glow box-shadow on focus.) */
.theme-ember [data-slot="button"]:not([data-variant="default"]):focus-visible {
box-shadow: none;
}
/* ── Type rhythm ──────────────────────────────────────────────────────────
The only safe scoped tightening: a hair of negative tracking on display /
title text. NO line-height changes and nothing global — body copy, chat
text, and code keep their exact metrics, so reading rhythm and the other
themes are untouched. */
.theme-ember .boocode-display,
.theme-ember [data-slot="dialog-title"],
.theme-ember [data-slot="card-title"] {
letter-spacing: -0.01em;
}
/* ── Reduced motion ───────────────────────────────────────────────────────
The glow/shadows/outline are static; only the primary button transitions.
Disable them (and the press transform) when the user asks for less motion. */
@media (prefers-reduced-motion: reduce) {
.theme-ember [data-slot="button"][data-variant="default"] {
transition: none;
}
.theme-ember [data-slot="button"][data-variant="default"]:not([disabled]):active {
transform: none;
}
}