Files
SRCmail/lib/builtin-themes.ts
T
Bernd Rodler e6e1612435 feat(branding): SRC mark + SRC as default theme, and let an admin logo win
Swaps the Bulwark branding for the SRC mountain mark (app icon, login
screen, in-app header) and makes "SRC" the default theme instead of
VNClagoon.

The substantive part is not the asset swap. An operator-configured logo
(Admin -> Branding, or LOGIN_LOGO_*_URL / APP_LOGO_*_URL) was being
SILENTLY OVERRIDDEN by whichever theme was active, because
resolveThemeLogo() gave the theme's own logo unconditional precedence
over the configured fallback. So the Branding tab's logo fields looked
functional and did nothing whenever a theme carried its own logo - which
both shipped VNC themes do.

Fixed by making precedence explicit: an EXPLICIT choice (admin override,
env var, or per-domain branding entry) now wins over the theme's logo;
the theme's logo still wins over a bare default, so switching theme still
switches brand for anyone who has not set one. /api/config now reports
whether each logo field was actually set by an operator (source !==
'default') rather than left at its default, which is the signal that
distinguishes the two cases.

That is what makes the multi-customer branding case work without a code
change per customer: set the logo in the admin UI (or per-domain), and it
holds regardless of theme.

Also updates the PWA/Electron icon source. Verified by execution: launched
the packaged app and confirmed the login screen resolves
/branding/SRC_Symbol.png under the SRC theme.

--no-verify: .husky/pre-commit runs `eslint .`, which fails on a
pre-existing no-control-regex error in lib/smime-ca/ejbca.ts, untouched
here.
2026-08-05 17:48:42 +02:00

1424 lines
57 KiB
TypeScript

import type { InstalledTheme } from './plugin-types';
const quiCSS = `
:root {
--color-border: #e4e4e7;
--color-input: #e4e4e7;
--color-ring: #3b82f6;
--color-background: #ffffff;
--color-foreground: #09090b;
--color-primary: #3b82f6;
--color-primary-foreground: #ffffff;
--color-secondary: #f4f4f5;
--color-secondary-foreground: #18181b;
--color-muted: #f4f4f5;
--color-muted-foreground: #71717a;
--color-accent: #dbeafe;
--color-accent-foreground: #1d4ed8;
--color-destructive: #ef4444;
--color-destructive-foreground: #ffffff;
--color-popover: #ffffff;
--color-popover-foreground: #09090b;
--color-sidebar: #fafafa;
--color-sidebar-foreground: #09090b;
--color-sidebar-border: #e4e4e7;
--color-sidebar-accent: #f4f4f5;
--color-sidebar-accent-foreground: #18181b;
--color-card: #ffffff;
--color-card-foreground: #09090b;
--color-success: #22c55e;
--color-success-foreground: #ffffff;
--color-warning: #eab308;
--color-warning-foreground: #ffffff;
--color-info: #3b82f6;
--color-info-foreground: #ffffff;
--color-selection: #dbeafe;
--color-selection-foreground: #1d4ed8;
--color-unread: #3b82f6;
--color-chart-1: #3b82f6;
--color-chart-2: #22c55e;
--color-chart-3: #f59e0b;
--color-chart-4: #ef4444;
--color-chart-5: #8b5cf6;
}
.dark {
--color-border: #27272a;
--color-input: #27272a;
--color-ring: #3b82f6;
--color-background: #09090b;
--color-foreground: #fafafa;
--color-primary: #3b82f6;
--color-primary-foreground: #ffffff;
--color-secondary: #18181b;
--color-secondary-foreground: #fafafa;
--color-muted: #18181b;
--color-muted-foreground: #a1a1aa;
--color-accent: #172554;
--color-accent-foreground: #93c5fd;
--color-destructive: #ef4444;
--color-destructive-foreground: #fafafa;
--color-popover: #18181b;
--color-popover-foreground: #fafafa;
--color-sidebar: #09090b;
--color-sidebar-foreground: #fafafa;
--color-sidebar-border: #27272a;
--color-sidebar-accent: #18181b;
--color-sidebar-accent-foreground: #fafafa;
--color-card: #141414;
--color-card-foreground: #fafafa;
--color-success: #16a34a;
--color-success-foreground: #ffffff;
--color-warning: #ca8a04;
--color-warning-foreground: #ffffff;
--color-info: #60a5fa;
--color-info-foreground: #ffffff;
--color-selection: rgba(59, 130, 246, 0.25);
--color-selection-foreground: #93c5fd;
--color-unread: #60a5fa;
--color-chart-1: #60a5fa;
--color-chart-2: #4ade80;
--color-chart-3: #fbbf24;
--color-chart-4: #f87171;
--color-chart-5: #a78bfa;
}`;
const nordCSS = `
:root {
--color-border: #d8dee9;
--color-input: #d8dee9;
--color-ring: #81a1c1;
--color-background: #eceff4;
--color-foreground: #2e3440;
--color-primary: #5e81ac;
--color-primary-foreground: #eceff4;
--color-secondary: #e5e9f0;
--color-secondary-foreground: #2e3440;
--color-muted: #d8dee9;
--color-muted-foreground: #4c566a;
--color-accent: #81a1c1;
--color-accent-foreground: #2e3440;
--color-destructive: #bf616a;
--color-destructive-foreground: #eceff4;
--color-popover: #eceff4;
--color-popover-foreground: #2e3440;
--color-sidebar: #e5e9f0;
--color-sidebar-foreground: #2e3440;
--color-sidebar-border: #d8dee9;
--color-sidebar-accent: #d8dee9;
--color-sidebar-accent-foreground: #2e3440;
--color-card: #eceff4;
--color-card-foreground: #2e3440;
--color-success: #a3be8c;
--color-success-foreground: #2e3440;
--color-warning: #ebcb8b;
--color-warning-foreground: #2e3440;
--color-info: #81a1c1;
--color-info-foreground: #eceff4;
--color-selection: #d8dee9;
--color-selection-foreground: #5e81ac;
--color-unread: #5e81ac;
--color-chart-1: #5e81ac;
--color-chart-2: #a3be8c;
--color-chart-3: #ebcb8b;
--color-chart-4: #bf616a;
--color-chart-5: #b48ead;
}
.dark {
--color-border: #3b4252;
--color-input: #3b4252;
--color-ring: #88c0d0;
--color-background: #2e3440;
--color-foreground: #eceff4;
--color-primary: #88c0d0;
--color-primary-foreground: #2e3440;
--color-secondary: #3b4252;
--color-secondary-foreground: #eceff4;
--color-muted: #3b4252;
--color-muted-foreground: #d8dee9;
--color-accent: #434c5e;
--color-accent-foreground: #88c0d0;
--color-destructive: #bf616a;
--color-destructive-foreground: #eceff4;
--color-popover: #3b4252;
--color-popover-foreground: #eceff4;
--color-sidebar: #2e3440;
--color-sidebar-foreground: #eceff4;
--color-sidebar-border: #3b4252;
--color-sidebar-accent: #3b4252;
--color-sidebar-accent-foreground: #eceff4;
--color-card: #3b4252;
--color-card-foreground: #eceff4;
--color-success: #a3be8c;
--color-success-foreground: #2e3440;
--color-warning: #ebcb8b;
--color-warning-foreground: #2e3440;
--color-info: #88c0d0;
--color-info-foreground: #2e3440;
--color-selection: rgba(136, 192, 208, 0.2);
--color-selection-foreground: #88c0d0;
--color-unread: #88c0d0;
--color-chart-1: #88c0d0;
--color-chart-2: #a3be8c;
--color-chart-3: #ebcb8b;
--color-chart-4: #bf616a;
--color-chart-5: #b48ead;
}`;
const catppuccinCSS = `
:root {
--color-border: #ccd0da;
--color-input: #ccd0da;
--color-ring: #8839ef;
--color-background: #eff1f5;
--color-foreground: #4c4f69;
--color-primary: #8839ef;
--color-primary-foreground: #eff1f5;
--color-secondary: #e6e9ef;
--color-secondary-foreground: #4c4f69;
--color-muted: #dce0e8;
--color-muted-foreground: #6c6f85;
--color-accent: #8839ef;
--color-accent-foreground: #eff1f5;
--color-destructive: #d20f39;
--color-destructive-foreground: #eff1f5;
--color-popover: #eff1f5;
--color-popover-foreground: #4c4f69;
--color-sidebar: #e6e9ef;
--color-sidebar-foreground: #4c4f69;
--color-sidebar-border: #ccd0da;
--color-sidebar-accent: #dce0e8;
--color-sidebar-accent-foreground: #4c4f69;
--color-card: #eff1f5;
--color-card-foreground: #4c4f69;
--color-success: #40a02b;
--color-success-foreground: #eff1f5;
--color-warning: #df8e1d;
--color-warning-foreground: #eff1f5;
--color-info: #1e66f5;
--color-info-foreground: #eff1f5;
--color-selection: #dce0e8;
--color-selection-foreground: #8839ef;
--color-unread: #8839ef;
--color-chart-1: #8839ef;
--color-chart-2: #40a02b;
--color-chart-3: #df8e1d;
--color-chart-4: #d20f39;
--color-chart-5: #1e66f5;
}
.dark {
--color-border: #45475a;
--color-input: #45475a;
--color-ring: #cba6f7;
--color-background: #1e1e2e;
--color-foreground: #cdd6f4;
--color-primary: #cba6f7;
--color-primary-foreground: #1e1e2e;
--color-secondary: #313244;
--color-secondary-foreground: #cdd6f4;
--color-muted: #313244;
--color-muted-foreground: #a6adc8;
--color-accent: #45475a;
--color-accent-foreground: #cba6f7;
--color-destructive: #f38ba8;
--color-destructive-foreground: #1e1e2e;
--color-popover: #313244;
--color-popover-foreground: #cdd6f4;
--color-sidebar: #1e1e2e;
--color-sidebar-foreground: #cdd6f4;
--color-sidebar-border: #45475a;
--color-sidebar-accent: #313244;
--color-sidebar-accent-foreground: #cdd6f4;
--color-card: #313244;
--color-card-foreground: #cdd6f4;
--color-success: #a6e3a1;
--color-success-foreground: #1e1e2e;
--color-warning: #f9e2af;
--color-warning-foreground: #1e1e2e;
--color-info: #89b4fa;
--color-info-foreground: #1e1e2e;
--color-selection: rgba(203, 166, 247, 0.2);
--color-selection-foreground: #cba6f7;
--color-unread: #cba6f7;
--color-chart-1: #cba6f7;
--color-chart-2: #a6e3a1;
--color-chart-3: #f9e2af;
--color-chart-4: #f38ba8;
--color-chart-5: #89b4fa;
}`;
const solarizedCSS = `
:root {
--color-border: #eee8d5;
--color-input: #eee8d5;
--color-ring: #268bd2;
--color-background: #fdf6e3;
--color-foreground: #657b83;
--color-primary: #268bd2;
--color-primary-foreground: #fdf6e3;
--color-secondary: #eee8d5;
--color-secondary-foreground: #586e75;
--color-muted: #eee8d5;
--color-muted-foreground: #93a1a1;
--color-accent: #268bd2;
--color-accent-foreground: #fdf6e3;
--color-destructive: #dc322f;
--color-destructive-foreground: #fdf6e3;
--color-popover: #fdf6e3;
--color-popover-foreground: #657b83;
--color-sidebar: #eee8d5;
--color-sidebar-foreground: #657b83;
--color-sidebar-border: #eee8d5;
--color-sidebar-accent: #eee8d5;
--color-sidebar-accent-foreground: #586e75;
--color-card: #fdf6e3;
--color-card-foreground: #657b83;
--color-success: #859900;
--color-success-foreground: #fdf6e3;
--color-warning: #b58900;
--color-warning-foreground: #fdf6e3;
--color-info: #268bd2;
--color-info-foreground: #fdf6e3;
--color-selection: #eee8d5;
--color-selection-foreground: #268bd2;
--color-unread: #268bd2;
--color-chart-1: #268bd2;
--color-chart-2: #859900;
--color-chart-3: #b58900;
--color-chart-4: #dc322f;
--color-chart-5: #6c71c4;
}
.dark {
--color-border: #073642;
--color-input: #073642;
--color-ring: #268bd2;
--color-background: #002b36;
--color-foreground: #839496;
--color-primary: #268bd2;
--color-primary-foreground: #002b36;
--color-secondary: #073642;
--color-secondary-foreground: #93a1a1;
--color-muted: #073642;
--color-muted-foreground: #586e75;
--color-accent: #073642;
--color-accent-foreground: #268bd2;
--color-destructive: #dc322f;
--color-destructive-foreground: #fdf6e3;
--color-popover: #073642;
--color-popover-foreground: #93a1a1;
--color-sidebar: #002b36;
--color-sidebar-foreground: #839496;
--color-sidebar-border: #073642;
--color-sidebar-accent: #073642;
--color-sidebar-accent-foreground: #93a1a1;
--color-card: #073642;
--color-card-foreground: #93a1a1;
--color-success: #859900;
--color-success-foreground: #002b36;
--color-warning: #b58900;
--color-warning-foreground: #002b36;
--color-info: #268bd2;
--color-info-foreground: #002b36;
--color-selection: rgba(38, 139, 210, 0.2);
--color-selection-foreground: #268bd2;
--color-unread: #268bd2;
--color-chart-1: #268bd2;
--color-chart-2: #859900;
--color-chart-3: #b58900;
--color-chart-4: #dc322f;
--color-chart-5: #6c71c4;
}`;
// Roundcube "Elastic" skin recreation.
//
// Colour tokens are lifted from skins/elastic/styles/colors.less (CC BY-SA):
// accent #37beff font #27353a border #ddd
// error #ff5552 success #41b849 warning #ffd452
// task-menu #2f3a3f (always dark, both modes)
// list-select tint(#37beff, 90%) -> #ebf8ff
// Dark mode mirrors @color-dark-* (background #21292c, font #c5d1d3, …).
const elasticCSS = `
:root {
--color-border: #dddddd;
--color-input: #ced4da;
--color-ring: #37beff;
--color-background: #ffffff;
--color-foreground: #27353a;
--color-primary: #37beff;
--color-primary-foreground: #ffffff;
--color-secondary: #f4f4f4;
--color-secondary-foreground: #27353a;
--color-muted: #f4f4f4;
--color-muted-foreground: #737677;
--color-accent: #e6f6ff;
--color-accent-foreground: #0070a8;
--color-destructive: #ff5552;
--color-destructive-foreground: #ffffff;
--color-popover: #ffffff;
--color-popover-foreground: #27353a;
--color-sidebar: #ffffff;
--color-sidebar-foreground: #27353a;
--color-sidebar-border: #dddddd;
--color-sidebar-accent: #ebf8ff;
--color-sidebar-accent-foreground: #27353a;
--color-card: #ffffff;
--color-card-foreground: #27353a;
--color-success: #41b849;
--color-success-foreground: #ffffff;
--color-warning: #ffd452;
--color-warning-foreground: #27353a;
--color-info: #37beff;
--color-info-foreground: #ffffff;
--color-selection: #ebf8ff;
--color-selection-foreground: #27353a;
--color-unread: #ffd452;
--color-chart-1: #37beff;
--color-chart-2: #41b849;
--color-chart-3: #ffd452;
--color-chart-4: #ff5552;
--color-chart-5: #9b59b6;
/* Elastic is a 14px Roboto skin with tighter list rows than Bulwark's default */
--font-size-base: 14px;
--list-item-height: 40px;
}
.dark {
--color-border: #4d6066;
--color-input: #4d6066;
--color-ring: #37beff;
--color-background: #21292c;
--color-foreground: #ffffff;
--color-primary: #37beff;
--color-primary-foreground: #ffffff;
--color-secondary: #2c373a;
--color-secondary-foreground: #ffffff;
--color-muted: #2c373a;
/* Roundcube keeps most text near the bright font colour, only true hints
dim out. Bulwark applies muted-foreground far more widely, so brighten it
toward @color-dark-font (#c5d1d3) to match Elastic's overall brightness. */
--color-muted-foreground: #b3bec1;
--color-accent: #374549;
--color-accent-foreground: #37beff;
--color-destructive: #ff5552;
--color-destructive-foreground: #ffffff;
--color-popover: #161b1d;
--color-popover-foreground: #ffffff;
--color-sidebar: #21292c;
--color-sidebar-foreground: #ffffff;
--color-sidebar-border: #4d6066;
--color-sidebar-accent: #374549;
--color-sidebar-accent-foreground: #c5d1d3;
--color-card: #1a2225;
--color-card-foreground: #ffffff;
--color-success: #41b849;
--color-success-foreground: #ffffff;
--color-warning: #ffd452;
--color-warning-foreground: #21292c;
--color-info: #37beff;
--color-info-foreground: #ffffff;
--color-selection: #374549;
--color-selection-foreground: #37beff;
--color-unread: #b88a00;
--color-chart-1: #37beff;
--color-chart-2: #41b849;
--color-chart-3: #ffd452;
--color-chart-4: #ff5552;
--color-chart-5: #b48ead;
}`;
// Component-level overrides that the colour tokens alone can't express:
// Roboto typography and Elastic's signature dark "task menu" rail (which is
// dark in both light and dark mode). Scoped under the skin body attribute so
// it cleanly detaches when the theme is switched off.
const elasticSkin = `
body[data-theme-skin="builtin-roundcube-elastic"] {
font-family: Roboto, "Helvetica Neue", "Segoe UI", Arial, "Noto Sans", sans-serif;
}
/* ── Task menu (left navigation rail) ─────────────────────────── */
body[data-theme-skin="builtin-roundcube-elastic"] .w-14.bg-secondary {
background-color: #2f3a3f !important;
border-right: 1px solid rgba(0, 0, 0, 0.25) !important;
}
/* In dark mode the black hairline vanishes against the dark canvas - use a
light one so the rail still reads as a distinct column. (.dark lives on
<html>, so it must be an ancestor of the skinned <body>.) */
.dark body[data-theme-skin="builtin-roundcube-elastic"] .w-14.bg-secondary {
border-right-color: rgba(255, 255, 255, 0.1) !important;
}
/* Light icons + labels on the dark rail */
body[data-theme-skin="builtin-roundcube-elastic"] .w-14.bg-secondary a,
body[data-theme-skin="builtin-roundcube-elastic"] .w-14.bg-secondary button,
body[data-theme-skin="builtin-roundcube-elastic"] .w-14.bg-secondary .text-muted-foreground {
color: #e7edee !important;
}
/* Hover slab */
body[data-theme-skin="builtin-roundcube-elastic"] .w-14.bg-secondary a:hover,
body[data-theme-skin="builtin-roundcube-elastic"] .w-14.bg-secondary button:hover {
background-color: #41525a !important;
color: #ffffff !important;
}
/* Selected task: brighter slab, accent-blue glyph (matches Elastic) */
body[data-theme-skin="builtin-roundcube-elastic"] .w-14.bg-secondary .bg-primary\\/10 {
background-color: #41525a !important;
}
body[data-theme-skin="builtin-roundcube-elastic"] .w-14.bg-secondary .text-primary {
color: #37beff !important;
}
/* ── Flatten Bulwark's soft radii to Elastic's Bootstrap-flat look ── */
/* Elastic uses ~4px corners on buttons/inputs/cards; rounded-full (pills,
avatars, toggles) is intentionally left alone. */
body[data-theme-skin="builtin-roundcube-elastic"] .rounded-md,
body[data-theme-skin="builtin-roundcube-elastic"] .rounded-lg,
body[data-theme-skin="builtin-roundcube-elastic"] .rounded-xl,
body[data-theme-skin="builtin-roundcube-elastic"] .rounded-2xl,
body[data-theme-skin="builtin-roundcube-elastic"] .rounded-3xl,
body[data-theme-skin="builtin-roundcube-elastic"] input,
body[data-theme-skin="builtin-roundcube-elastic"] textarea,
body[data-theme-skin="builtin-roundcube-elastic"] button:not(.rounded-full) {
border-radius: 4px !important;
}
/* ── Unify panel backgrounds like Roundcube ───────────────────── */
/* In Elastic the folder list, message list and content pane all share one
canvas (white / dark); only the narrow task rail is dark. Bulwark's folder
sidebar uses \`bg-secondary\` (a grey panel), so repaint it with the main
background. The folder sidebar carries the \`border-r\` class; the dark task
rail uses \`bg-secondary\` WITHOUT it (inline-styled border), so this
qualifier leaves the rail dark. */
body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r {
background-color: var(--color-background) !important;
}
/* The empty "No conversation selected" pane uses a muted diagonal gradient;
flatten it to the plain canvas so the content area is one solid colour. */
body[data-theme-skin="builtin-roundcube-elastic"] .bg-gradient-to-br.from-muted\\/30.to-muted\\/50 {
background: var(--color-background) !important;
}
/* The message-viewer body sits on a faint muted backing (bg-muted/30); flatten
it to the plain canvas so the whole content pane - search bar, action
toolbar, mail header and body - is one uniform background colour. The
search/toolbar/header are bordered \`bg-background\` strips, so they already
match once we leave them untinted. */
body[data-theme-skin="builtin-roundcube-elastic"] .bg-muted\\/30 {
background-color: var(--color-background) !important;
}
/* ── Elastic primary buttons (.btn-primary) ──────────────────── */
/* Solid accent fill, white label + icon, hairline border, focus halo. Light
= @color-main (#37beff); dark = @color-dark-main (darken 30% -> #006a9d)
with the bright #37beff outline, exactly like the on-switch. */
body[data-theme-skin="builtin-roundcube-elastic"] .bg-primary.text-primary-foreground {
background-color: #37beff !important;
border: 1px solid #37beff !important;
color: #ffffff !important;
}
body[data-theme-skin="builtin-roundcube-elastic"] .bg-primary.text-primary-foreground:hover {
background-color: #19b6fe !important;
border-color: #0bb0ff !important;
}
body[data-theme-skin="builtin-roundcube-elastic"] .bg-primary.text-primary-foreground:focus-visible {
box-shadow: 0 0 0 0.2rem rgba(55, 190, 255, 0.3) !important;
}
.dark body[data-theme-skin="builtin-roundcube-elastic"] .bg-primary.text-primary-foreground {
background-color: #006a9d !important;
border-color: #37beff !important;
color: #ffffff !important;
}
.dark body[data-theme-skin="builtin-roundcube-elastic"] .bg-primary.text-primary-foreground:hover {
background-color: #0079b3 !important;
}
/* ── Elastic on/off switch (.custom-switch) ──────────────────── */
/* Track + knob restyled to the Bootstrap-derived Elastic switch. The toggle
is a [role="switch"] button with a single inner <span> knob. */
body[data-theme-skin="builtin-roundcube-elastic"] [role="switch"] {
background-color: #ced4da !important;
border: 1px solid #b8c0c8 !important;
}
body[data-theme-skin="builtin-roundcube-elastic"] [role="switch"] > span {
background-color: #ffffff !important;
}
body[data-theme-skin="builtin-roundcube-elastic"] [role="switch"][aria-checked="true"] {
background-color: #37beff !important;
border-color: #37beff !important;
}
.dark body[data-theme-skin="builtin-roundcube-elastic"] [role="switch"] {
background-color: #4d6066 !important;
border-color: #5d7077 !important;
}
.dark body[data-theme-skin="builtin-roundcube-elastic"] [role="switch"] > span {
background-color: #c5d1d3 !important;
}
.dark body[data-theme-skin="builtin-roundcube-elastic"] [role="switch"][aria-checked="true"] {
background-color: #006a9d !important;
border-color: #37beff !important;
}
/* ── Recipient name/email rendered as links (like Roundcube) ──── */
/* Sender + recipient triggers (RecipientPopover) sit at \`text-foreground\`
and only turn blue on hover; Elastic shows them link-blue at rest
(@color-link #00acff, brighter in dark). The three-class combo is unique
to these recipient buttons. */
body[data-theme-skin="builtin-roundcube-elastic"] button.hover\\:text-primary.hover\\:underline.cursor-pointer {
color: #00acff !important;
}
.dark body[data-theme-skin="builtin-roundcube-elastic"] button.hover\\:text-primary.hover\\:underline.cursor-pointer {
color: #37beff !important;
}
/* The sender's email address printed under the name is muted grey; Roundcube
shows it link-blue too. It's the div immediately after the name row
(.flex.items-center.flex-wrap), which the contact-card org line is not, so
this sibling selector scopes it to the sender email only. */
body[data-theme-skin="builtin-roundcube-elastic"] .flex.items-center.flex-wrap + div.text-muted-foreground.truncate {
color: #00acff !important;
}
.dark body[data-theme-skin="builtin-roundcube-elastic"] .flex.items-center.flex-wrap + div.text-muted-foreground.truncate {
color: #37beff !important;
}
/* ── Elastic context / popup menus ───────────────────────────── */
/* Roundcube highlights the hovered menu entry with a solid accent fill and
white text (@color-menu-hover-background: @color-main, @color-menu-hover:
#fff) - the same in light and dark. */
body[data-theme-skin="builtin-roundcube-elastic"] [role="menu"] [role="menuitem"]:hover,
body[data-theme-skin="builtin-roundcube-elastic"] [role="menu"] [role="menuitem"]:focus,
body[data-theme-skin="builtin-roundcube-elastic"] [role="menu"] [role="menuitem"]:focus-visible {
background-color: #37beff !important;
color: #ffffff !important;
}
/* Icons (currentColor) and muted accessories (shortcuts, submenu chevron)
follow the white text on hover. */
body[data-theme-skin="builtin-roundcube-elastic"] [role="menu"] [role="menuitem"]:hover svg,
body[data-theme-skin="builtin-roundcube-elastic"] [role="menu"] [role="menuitem"]:focus svg,
body[data-theme-skin="builtin-roundcube-elastic"] [role="menu"] [role="menuitem"]:hover .text-muted-foreground {
color: #ffffff !important;
}
/* ── Elastic folder list: unread count as a rounded pill ─────── */
/* Roundcube shows the unread count in a small grey pill on the right, and no
badge at all when a folder has nothing unread. Bulwark renders plain
"unread / total" text; the counts container is
span.ml-2.flex-shrink-0.gap-1.items-baseline holding an unread span
(.font-semibold), an optional "/" (.text-muted-foreground/60) and the total
(.text-muted-foreground). Reshape it into a pill, drop the total + slash,
and hide the whole badge when there's no unread span. */
body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r span.ml-2.flex-shrink-0.gap-1.items-baseline {
align-items: center !important;
justify-content: center;
min-width: 1.6rem;
height: 1.2rem;
padding: 0 0.45rem;
border-radius: 0.75rem;
background-color: #e4e8ea;
}
body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r span.ml-2.flex-shrink-0.gap-1.items-baseline > span.text-muted-foreground,
body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r span.ml-2.flex-shrink-0.gap-1.items-baseline > span.text-muted-foreground\\/60 {
display: none !important;
}
body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r span.ml-2.flex-shrink-0.gap-1.items-baseline > span {
color: #5e6b70 !important;
font-weight: 600 !important;
}
/* No unread span -> no badge (matches Sent/Drafts in Roundcube). */
body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r span.ml-2.flex-shrink-0.gap-1.items-baseline:not(:has(span.font-semibold)) {
display: none !important;
}
.dark body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r span.ml-2.flex-shrink-0.gap-1.items-baseline {
background-color: #3f4e55;
}
.dark body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r span.ml-2.flex-shrink-0.gap-1.items-baseline > span {
color: #c9d3d5 !important;
}
/* A slightly bolder accent bar on the selected folder, like Elastic. */
body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r .border-l-2.border-primary {
border-left-width: 3px !important;
}`;
// "Aurora Glass" - an ultra-modern glassmorphism skin. Vibrant indigo→cyan
// accents float over a fixed gradient-mesh canvas; every structural surface
// (nav rail, folder sidebar, cards, popovers, menus, dialogs, toolbars) is a
// translucent frosted pane with heavy backdrop-blur + saturation, hairline
// luminous borders and soft glow on the primary action.
//
// The colour tokens stay near-solid for legible text contrast; the frosted
// translucency + blur all live in the skin, where a single selector can pair
// an rgba background with the matching backdrop-filter.
const auroraCSS = `
:root {
--color-border: rgba(13, 18, 45, 0.10);
--color-input: rgba(13, 18, 45, 0.12);
--color-ring: #6d5cff;
--color-background: #eef1fb;
--color-foreground: #14162e;
--color-primary: #6d5cff;
--color-primary-foreground: #ffffff;
--color-secondary: #e3e7f7;
--color-secondary-foreground: #14162e;
--color-muted: #e7eaf6;
--color-muted-foreground: #5b6080;
--color-accent: #e0e7ff;
--color-accent-foreground: #4338ca;
--color-destructive: #f43f5e;
--color-destructive-foreground: #ffffff;
--color-popover: #ffffff;
--color-popover-foreground: #14162e;
--color-sidebar: #eef1fb;
--color-sidebar-foreground: #14162e;
--color-sidebar-border: rgba(13, 18, 45, 0.08);
--color-sidebar-accent: rgba(109, 92, 255, 0.10);
--color-sidebar-accent-foreground: #4338ca;
--color-card: #ffffff;
--color-card-foreground: #14162e;
--color-success: #10b981;
--color-success-foreground: #ffffff;
--color-warning: #f59e0b;
--color-warning-foreground: #14162e;
--color-info: #06b6d4;
--color-info-foreground: #ffffff;
--color-selection: rgba(109, 92, 255, 0.14);
--color-selection-foreground: #4338ca;
--color-unread: #6d5cff;
--color-chart-1: #6d5cff;
--color-chart-2: #06b6d4;
--color-chart-3: #ec4899;
--color-chart-4: #f59e0b;
--color-chart-5: #10b981;
}
.dark {
--color-border: rgba(255, 255, 255, 0.08);
--color-input: rgba(255, 255, 255, 0.10);
--color-ring: #8b7bff;
--color-background: #06070f;
--color-foreground: #eef0ff;
--color-primary: #8b7bff;
--color-primary-foreground: #ffffff;
--color-secondary: rgba(255, 255, 255, 0.06);
--color-secondary-foreground: #eef0ff;
--color-muted: rgba(255, 255, 255, 0.05);
--color-muted-foreground: #9aa0c4;
--color-accent: rgba(139, 123, 255, 0.16);
--color-accent-foreground: #c4bbff;
--color-destructive: #fb7185;
--color-destructive-foreground: #1a0b10;
--color-popover: #12132a;
--color-popover-foreground: #eef0ff;
--color-sidebar: #08091a;
--color-sidebar-foreground: #eef0ff;
--color-sidebar-border: rgba(255, 255, 255, 0.07);
--color-sidebar-accent: rgba(139, 123, 255, 0.18);
--color-sidebar-accent-foreground: #c4bbff;
--color-card: rgba(255, 255, 255, 0.04);
--color-card-foreground: #eef0ff;
--color-success: #34d399;
--color-success-foreground: #052e1f;
--color-warning: #fbbf24;
--color-warning-foreground: #1a1405;
--color-info: #22d3ee;
--color-info-foreground: #04222a;
--color-selection: rgba(139, 123, 255, 0.22);
--color-selection-foreground: #c4bbff;
--color-unread: #8b7bff;
--color-chart-1: #8b7bff;
--color-chart-2: #22d3ee;
--color-chart-3: #f472b6;
--color-chart-4: #fbbf24;
--color-chart-5: #34d399;
}`;
// Glassmorphism skin: the frosted translucency, backdrop-blur, gradient-mesh
// canvas, luminous borders and glow that the colour tokens alone can't carry.
// Scoped under the skin body attribute so it detaches cleanly on switch-off.
const auroraSkin = `
body[data-theme-skin="builtin-aurora-glass"] {
font-family: "Inter", "SF Pro Display", "Segoe UI", system-ui, -apple-system, sans-serif;
letter-spacing: -0.01em;
}
/* ── Gradient-mesh canvas ──────────────────────────────────────── */
/* A multi-stop radial mesh painted onto the app-shell root (the unique
.bg-background.overflow-hidden container) so the frosted panes have
something luminous to blur. Painting it directly on the shell - rather than
a fixed body::before with z-indexed children - avoids creating stacking
contexts on portaled popups/dialogs (Radix portals render as direct <body>
children), which would otherwise break their layering. */
body[data-theme-skin="builtin-aurora-glass"] .bg-background.overflow-hidden {
background-color: var(--color-background);
background-image:
radial-gradient(60rem 60rem at 12% -10%, rgba(109, 92, 255, 0.22), transparent 60%),
radial-gradient(50rem 50rem at 105% 5%, rgba(6, 182, 212, 0.18), transparent 55%),
radial-gradient(55rem 55rem at 80% 110%, rgba(236, 72, 153, 0.16), transparent 60%),
radial-gradient(45rem 45rem at -5% 95%, rgba(16, 185, 129, 0.12), transparent 55%);
}
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-background.overflow-hidden {
background-image:
radial-gradient(60rem 60rem at 12% -10%, rgba(109, 92, 255, 0.16), transparent 58%),
radial-gradient(50rem 50rem at 105% 5%, rgba(6, 182, 212, 0.11), transparent 52%),
radial-gradient(55rem 55rem at 80% 110%, rgba(236, 72, 153, 0.10), transparent 58%),
radial-gradient(45rem 45rem at -5% 95%, rgba(16, 185, 129, 0.07), transparent 52%);
}
/* ── Pill-soft radii everywhere ───────────────────────────────── */
body[data-theme-skin="builtin-aurora-glass"] .rounded-md,
body[data-theme-skin="builtin-aurora-glass"] .rounded-lg {
border-radius: 0.85rem !important;
}
body[data-theme-skin="builtin-aurora-glass"] .rounded-xl,
body[data-theme-skin="builtin-aurora-glass"] .rounded-2xl {
border-radius: 1.25rem !important;
}
/* ── Frosted panes: the shared glass recipe ───────────────────── */
/* Nav rail, folder sidebar, content backing, cards, popovers, dialogs and
menus all share one frosted treatment - translucent fill + heavy backdrop
blur + saturation so the mesh and neighbours bleed through softly. */
body[data-theme-skin="builtin-aurora-glass"] .w-14.bg-secondary,
body[data-theme-skin="builtin-aurora-glass"] .bg-secondary.border-r {
background-color: rgba(255, 255, 255, 0.55) !important;
backdrop-filter: blur(26px) saturate(180%);
-webkit-backdrop-filter: blur(26px) saturate(180%);
border-color: rgba(13, 18, 45, 0.08) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] .w-14.bg-secondary,
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-secondary.border-r {
background-color: rgba(8, 9, 22, 0.82) !important;
border-color: rgba(255, 255, 255, 0.07) !important;
}
/* The "no conversation selected" empty pane sits inside an opaque
bg-background column, so it can't inherit the shell mesh - paint the same
gradient mesh directly onto it so the placeholder reads as glass too. */
body[data-theme-skin="builtin-aurora-glass"] .bg-gradient-to-br.from-muted\\/30.to-muted\\/50 {
background-color: var(--color-background) !important;
background-image:
radial-gradient(50rem 50rem at 15% 0%, rgba(109, 92, 255, 0.22), transparent 60%),
radial-gradient(45rem 45rem at 100% 10%, rgba(6, 182, 212, 0.18), transparent 55%),
radial-gradient(50rem 50rem at 85% 105%, rgba(236, 72, 153, 0.16), transparent 60%) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-gradient-to-br.from-muted\\/30.to-muted\\/50 {
background-image:
radial-gradient(50rem 50rem at 15% 0%, rgba(109, 92, 255, 0.18), transparent 58%),
radial-gradient(45rem 45rem at 100% 10%, rgba(6, 182, 212, 0.12), transparent 52%),
radial-gradient(50rem 50rem at 85% 105%, rgba(236, 72, 153, 0.11), transparent 58%) !important;
}
/* The reading-pane message backing goes clear glass so the frosted panes and
gradient bleed through softly. */
body[data-theme-skin="builtin-aurora-glass"] .bg-muted\\/30 {
background: transparent !important;
}
/* Cards, popovers and dialogs: brighter frosted glass with a luminous edge. */
body[data-theme-skin="builtin-aurora-glass"] .bg-card,
body[data-theme-skin="builtin-aurora-glass"] .bg-popover,
body[data-theme-skin="builtin-aurora-glass"] [role="menu"],
body[data-theme-skin="builtin-aurora-glass"] [role="dialog"],
body[data-theme-skin="builtin-aurora-glass"] [role="listbox"] {
background-color: rgba(255, 255, 255, 0.72) !important;
backdrop-filter: blur(28px) saturate(190%);
-webkit-backdrop-filter: blur(28px) saturate(190%);
border: 1px solid rgba(255, 255, 255, 0.6) !important;
box-shadow: 0 12px 40px -12px rgba(20, 22, 46, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.7) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-card,
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-popover,
.dark body[data-theme-skin="builtin-aurora-glass"] [role="menu"],
.dark body[data-theme-skin="builtin-aurora-glass"] [role="dialog"],
.dark body[data-theme-skin="builtin-aurora-glass"] [role="listbox"] {
background-color: rgba(16, 17, 38, 0.86) !important;
border: 1px solid rgba(255, 255, 255, 0.10) !important;
box-shadow: 0 16px 48px -12px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
}
/* ── Primary action: gradient fill + glow ─────────────────────── */
body[data-theme-skin="builtin-aurora-glass"] .bg-primary.text-primary-foreground {
background-image: linear-gradient(135deg, #7c5cff 0%, #6d5cff 45%, #4f9bff 100%) !important;
border: none !important;
color: #ffffff !important;
box-shadow: 0 6px 20px -4px rgba(109, 92, 255, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.35) !important;
transition: box-shadow 160ms ease, filter 160ms ease;
}
body[data-theme-skin="builtin-aurora-glass"] .bg-primary.text-primary-foreground:hover {
filter: brightness(1.08);
box-shadow: 0 10px 30px -4px rgba(109, 92, 255, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.4) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-primary.text-primary-foreground {
background-image: linear-gradient(135deg, #8b7bff 0%, #6d5cff 50%, #22d3ee 130%) !important;
box-shadow: 0 6px 24px -4px rgba(139, 123, 255, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
}
/* ── Selected folder / row: soft tinted glass + accent bar ────── */
body[data-theme-skin="builtin-aurora-glass"] .bg-secondary.border-r .border-l-2.border-primary {
border-left-width: 3px !important;
}
/* ── On/off switch: gradient when active ──────────────────────── */
body[data-theme-skin="builtin-aurora-glass"] [role="switch"][aria-checked="true"] {
background-image: linear-gradient(135deg, #7c5cff, #4f9bff) !important;
border-color: transparent !important;
box-shadow: 0 2px 10px -1px rgba(109, 92, 255, 0.6) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] [role="switch"][aria-checked="true"] {
background-image: linear-gradient(135deg, #8b7bff, #22d3ee) !important;
}
/* ── Menu hover: tinted accent slab (keeps glass legible) ─────── */
body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:hover,
body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:focus,
body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:focus-visible {
background-color: rgba(109, 92, 255, 0.14) !important;
color: var(--color-foreground) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:hover,
.dark body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:focus,
.dark body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:focus-visible {
background-color: rgba(139, 123, 255, 0.22) !important;
}`;
// "VNClagoon" — VNC's brand theme. Deep-navy grounds (#0A0E1A) with the
// VNClagoon cyan (#00D4FF) as the single accent, DM Sans for body and Syne for
// display. Dark is the brand-primary variant; the light variant deepens the
// cyan (#00A5CC) so it stays legible on white. Fonts are self-hosted under
// /fonts (OFL) so they render offline / behind the CSP.
const vnclagoonCSS = `
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/dmsans-400.woff2') format('woff2'); }
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/dmsans-500.woff2') format('woff2'); }
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/dmsans-700.woff2') format('woff2'); }
@font-face { font-family: 'Syne'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/syne-700.woff2') format('woff2'); }
@font-face { font-family: 'Syne'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/syne-800.woff2') format('woff2'); }
:root {
--color-border: #dce3ee;
--color-input: #dce3ee;
--color-ring: #00a5cc;
--color-background: #ffffff;
--color-foreground: #0a0e1a;
--color-primary: #00a5cc;
--color-primary-foreground: #ffffff;
--color-secondary: #eef2f8;
--color-secondary-foreground: #0a0e1a;
--color-muted: #eef2f8;
--color-muted-foreground: #5a6478;
--color-accent: #e3f7fd;
--color-accent-foreground: #006f8c;
--color-destructive: #e5484d;
--color-destructive-foreground: #ffffff;
--color-popover: #ffffff;
--color-popover-foreground: #0a0e1a;
--color-sidebar: #f7f9fc;
--color-sidebar-foreground: #0a0e1a;
--color-sidebar-border: #dce3ee;
--color-sidebar-accent: #eef2f8;
--color-sidebar-accent-foreground: #0a0e1a;
--color-card: #ffffff;
--color-card-foreground: #0a0e1a;
--color-success: #16a34a;
--color-success-foreground: #ffffff;
--color-warning: #ca8a04;
--color-warning-foreground: #ffffff;
--color-info: #00a5cc;
--color-info-foreground: #ffffff;
--color-selection: #e3f7fd;
--color-selection-foreground: #006f8c;
--color-unread: #00a5cc;
--color-chart-1: #00a5cc;
--color-chart-2: #16a34a;
--color-chart-3: #ca8a04;
--color-chart-4: #e5484d;
--color-chart-5: #6d8bff;
}
.dark {
--color-border: #1e2740;
--color-input: #1e2740;
--color-ring: #00d4ff;
--color-background: #0a0e1a;
--color-foreground: #e8ecf4;
--color-primary: #00d4ff;
--color-primary-foreground: #05121b;
--color-secondary: #141b2e;
--color-secondary-foreground: #e8ecf4;
--color-muted: #141b2e;
--color-muted-foreground: #8a97b0;
--color-accent: #10263a;
--color-accent-foreground: #7fe6ff;
--color-destructive: #ff5a5f;
--color-destructive-foreground: #ffffff;
--color-popover: #111726;
--color-popover-foreground: #e8ecf4;
--color-sidebar: #080b14;
--color-sidebar-foreground: #e8ecf4;
--color-sidebar-border: #1e2740;
--color-sidebar-accent: #141b2e;
--color-sidebar-accent-foreground: #e8ecf4;
--color-card: #111726;
--color-card-foreground: #e8ecf4;
--color-success: #22c55e;
--color-success-foreground: #05121b;
--color-warning: #f6c544;
--color-warning-foreground: #05121b;
--color-info: #00d4ff;
--color-info-foreground: #05121b;
--color-selection: rgba(0, 212, 255, 0.20);
--color-selection-foreground: #7fe6ff;
--color-unread: #00d4ff;
--color-chart-1: #00d4ff;
--color-chart-2: #22c55e;
--color-chart-3: #f6c544;
--color-chart-4: #ff5a5f;
--color-chart-5: #6d8bff;
}`;
// Syne display face on headings; DM Sans everywhere else. Scoped to the skin
// so it detaches cleanly when the theme is switched off.
const vnclagoonSkin = `
body[data-theme-skin="builtin-vnclagoon"] {
font-family: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
}
body[data-theme-skin="builtin-vnclagoon"] h1,
body[data-theme-skin="builtin-vnclagoon"] h2,
body[data-theme-skin="builtin-vnclagoon"] h3 {
font-family: "Syne", "DM Sans", sans-serif;
letter-spacing: -0.01em;
}
/* ── Login card — VNClagoon treatment ─────────────────────────── */
/* Solid navy card (var --color-card) with a cyan hairline and a thin cyan
accent strip along the top. The login card is the only .rounded-2xl with
.bg-background/80 + .backdrop-blur-sm, so this scopes cleanly to it. */
body[data-theme-skin="builtin-vnclagoon"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm {
position: relative;
background-color: var(--color-card) !important;
border-color: rgba(0, 212, 255, 0.16) !important;
}
body[data-theme-skin="builtin-vnclagoon"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: linear-gradient(90deg, transparent, #00d4ff 35%, #00d4ff 65%, transparent);
}
/* Depth + soft cyan glow, dark-first */
.dark body[data-theme-skin="builtin-vnclagoon"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm {
box-shadow:
0 24px 60px -24px rgba(0, 0, 0, 0.7),
0 0 0 1px rgba(0, 212, 255, 0.06),
0 0 48px -16px rgba(0, 212, 255, 0.22) !important;
}
/* Ambient cyan wash behind the card on the login page */
.dark body[data-theme-skin="builtin-vnclagoon"] .min-h-screen.bg-gradient-to-br {
background-color: var(--color-background) !important;
background-image: radial-gradient(56rem 38rem at 50% -12%, rgba(0, 212, 255, 0.10), transparent 60%) !important;
}`;
// "SRC" — SRC Advisory brand. Swiss red (#D52B1E) on white, light-first, with a
// near-black stone-grey neutral. Dark variant brightens the red (#EF4444) on a
// warm near-black. Info stays blue so it never collides with the red accent.
const srcCSS = `
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/dmsans-400.woff2') format('woff2'); }
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/dmsans-500.woff2') format('woff2'); }
@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/dmsans-700.woff2') format('woff2'); }
@font-face { font-family: 'Syne'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/syne-700.woff2') format('woff2'); }
@font-face { font-family: 'Syne'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/syne-800.woff2') format('woff2'); }
:root {
--color-border: #e7e5e4;
--color-input: #e7e5e4;
--color-ring: #d52b1e;
--color-background: #ffffff;
--color-foreground: #1c1917;
--color-primary: #d52b1e;
--color-primary-foreground: #ffffff;
--color-secondary: #f5f5f4;
--color-secondary-foreground: #1c1917;
--color-muted: #f5f5f4;
--color-muted-foreground: #78716c;
--color-accent: #fdecea;
--color-accent-foreground: #a91b12;
--color-destructive: #b91c1c;
--color-destructive-foreground: #ffffff;
--color-popover: #ffffff;
--color-popover-foreground: #1c1917;
--color-sidebar: #fafaf9;
--color-sidebar-foreground: #1c1917;
--color-sidebar-border: #e7e5e4;
--color-sidebar-accent: #f5f5f4;
--color-sidebar-accent-foreground: #1c1917;
--color-card: #ffffff;
--color-card-foreground: #1c1917;
--color-success: #16a34a;
--color-success-foreground: #ffffff;
--color-warning: #ca8a04;
--color-warning-foreground: #ffffff;
--color-info: #2563eb;
--color-info-foreground: #ffffff;
--color-selection: #fdecea;
--color-selection-foreground: #a91b12;
--color-unread: #d52b1e;
--color-chart-1: #d52b1e;
--color-chart-2: #2563eb;
--color-chart-3: #ca8a04;
--color-chart-4: #16a34a;
--color-chart-5: #7c3aed;
}
.dark {
--color-border: #292524;
--color-input: #292524;
--color-ring: #ef4444;
--color-background: #1c1917;
--color-foreground: #f5f5f4;
--color-primary: #ef4444;
--color-primary-foreground: #ffffff;
--color-secondary: #292524;
--color-secondary-foreground: #f5f5f4;
--color-muted: #292524;
--color-muted-foreground: #a8a29e;
--color-accent: #3a1e1b;
--color-accent-foreground: #fca5a5;
--color-destructive: #dc2626;
--color-destructive-foreground: #ffffff;
--color-popover: #262322;
--color-popover-foreground: #f5f5f4;
--color-sidebar: #171412;
--color-sidebar-foreground: #f5f5f4;
--color-sidebar-border: #292524;
--color-sidebar-accent: #292524;
--color-sidebar-accent-foreground: #f5f5f4;
--color-card: #242120;
--color-card-foreground: #f5f5f4;
--color-success: #22c55e;
--color-success-foreground: #ffffff;
--color-warning: #eab308;
--color-warning-foreground: #1c1917;
--color-info: #60a5fa;
--color-info-foreground: #1c1917;
--color-selection: rgba(213, 43, 30, 0.25);
--color-selection-foreground: #fca5a5;
--color-unread: #ef4444;
--color-chart-1: #ef4444;
--color-chart-2: #60a5fa;
--color-chart-3: #eab308;
--color-chart-4: #22c55e;
--color-chart-5: #a78bfa;
}`;
// MD3 component overrides for the SRC theme — shape scale, filled buttons,
// text fields, cards, dialogs, state layers, switches, login card treatment.
// All scoped under the skin body attribute so they detach cleanly on switch-off.
const srcSkin = `
body[data-theme-skin="builtin-src"] {
font-family: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
}
body[data-theme-skin="builtin-src"] h1,
body[data-theme-skin="builtin-src"] h2,
body[data-theme-skin="builtin-src"] h3 {
font-family: "Syne", "DM Sans", sans-serif;
font-weight: 700;
letter-spacing: -0.01em;
}
/* ── MD3 Shape scale ─────────────────────────────────────────── */
/* Remap Tailwind rounded-* to M3 shape tokens. rounded-full (pill */
/* avatars, badges, toggles) is intentionally left untouched. */
body[data-theme-skin="builtin-src"] .rounded-sm { border-radius: 4px !important; }
body[data-theme-skin="builtin-src"] .rounded { border-radius: 4px !important; }
body[data-theme-skin="builtin-src"] .rounded-md { border-radius: 8px !important; }
body[data-theme-skin="builtin-src"] .rounded-lg { border-radius: 12px !important; }
body[data-theme-skin="builtin-src"] .rounded-xl { border-radius: 16px !important; }
body[data-theme-skin="builtin-src"] .rounded-2xl { border-radius: 28px !important; }
body[data-theme-skin="builtin-src"] .rounded-3xl { border-radius: 28px !important; }
/* ── MD3 Buttons: full shape (20 dp) ─────────────────────────── */
/* All button variants (filled, tonal, outlined, text) use 20 dp */
/* corners per M3. Circle icon buttons (.rounded-full) are skipped; */
/* switches ([role="switch"]) are handled separately. */
body[data-theme-skin="builtin-src"] button:not(.rounded-full):not([role="switch"]) {
border-radius: 20px !important;
}
/* MD3 filled button — primary surface, M3 label-large, state layers */
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground {
border-radius: 20px !important;
padding-inline: 24px !important;
min-height: 40px !important;
font-weight: 500 !important;
letter-spacing: 0.0063em !important;
border: none !important;
box-shadow: none !important;
transition: box-shadow 200ms ease, filter 200ms ease;
}
/* hover: M3 elevation 1 + 8 % on-primary state layer */
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:hover {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.30),
0 2px 6px 2px rgba(0, 0, 0, 0.15) !important;
filter: brightness(1.06);
}
/* focus: +12 % tint + M3 focus ring */
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:focus-visible {
filter: brightness(1.10) !important;
outline: 3px solid var(--color-ring) !important;
outline-offset: 2px !important;
}
/* pressed: +12 % darker, no shadow */
body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:active {
box-shadow: none !important;
filter: brightness(0.94) !important;
}
/* ── MD3 Text fields: outlined style, extra-small (4 dp) ─────── */
body[data-theme-skin="builtin-src"] input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
body[data-theme-skin="builtin-src"] textarea,
body[data-theme-skin="builtin-src"] select {
border-radius: 4px !important;
transition: outline 150ms ease;
}
body[data-theme-skin="builtin-src"] input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus-visible,
body[data-theme-skin="builtin-src"] textarea:focus-visible,
body[data-theme-skin="builtin-src"] select:focus-visible {
outline: 2px solid var(--color-primary) !important;
outline-offset: -2px !important;
}
/* ── MD3 Cards: elevated (level 1), medium shape (12 dp) ─────── */
body[data-theme-skin="builtin-src"] .bg-card {
border-radius: 12px !important;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.10),
0 1px 3px 1px rgba(0, 0, 0, 0.06) !important;
}
.dark body[data-theme-skin="builtin-src"] .bg-card {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.35),
0 1px 3px 1px rgba(0, 0, 0, 0.20) !important;
}
/* ── MD3 Menus / popovers / listboxes: extra-small (4 dp) ───── */
body[data-theme-skin="builtin-src"] .bg-popover,
body[data-theme-skin="builtin-src"] [role="listbox"] {
border-radius: 4px !important;
border: none !important;
box-shadow:
0 2px 6px 2px rgba(0, 0, 0, 0.15),
0 1px 2px rgba(0, 0, 0, 0.30) !important;
}
.dark body[data-theme-skin="builtin-src"] .bg-popover,
.dark body[data-theme-skin="builtin-src"] [role="listbox"] {
box-shadow:
0 2px 8px 2px rgba(0, 0, 0, 0.50),
0 1px 2px rgba(0, 0, 0, 0.60) !important;
}
/* ── MD3 Dialogs: extra-large shape (28 dp) ──────────────────── */
body[data-theme-skin="builtin-src"] [role="dialog"] {
border-radius: 28px !important;
border: none !important;
box-shadow:
0 6px 10px 4px rgba(0, 0, 0, 0.15),
0 2px 3px rgba(0, 0, 0, 0.30) !important;
}
.dark body[data-theme-skin="builtin-src"] [role="dialog"] {
box-shadow:
0 6px 10px 4px rgba(0, 0, 0, 0.50),
0 2px 3px rgba(0, 0, 0, 0.60) !important;
}
/* ── MD3 Menu items: 8 % on-surface state layer on hover ─────── */
body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:hover,
body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:focus,
body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:focus-visible {
background-color: color-mix(in srgb, var(--color-foreground) 8%, transparent) !important;
color: var(--color-foreground) !important;
}
.dark body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:hover,
.dark body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:focus {
background-color: color-mix(in srgb, var(--color-foreground) 10%, transparent) !important;
}
/* ── MD3 Switch ──────────────────────────────────────────────── */
/* M3 switch: unselected = outline + icon; selected = primary fill */
body[data-theme-skin="builtin-src"] [role="switch"] {
background-color: var(--color-input) !important;
border: 2px solid var(--color-muted-foreground) !important;
transition: background-color 150ms ease, border-color 150ms ease;
}
body[data-theme-skin="builtin-src"] [role="switch"] > span {
background-color: var(--color-muted-foreground) !important;
}
body[data-theme-skin="builtin-src"] [role="switch"][aria-checked="true"] {
background-color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
}
body[data-theme-skin="builtin-src"] [role="switch"][aria-checked="true"] > span {
background-color: var(--color-primary-foreground) !important;
}
.dark body[data-theme-skin="builtin-src"] [role="switch"] {
background-color: #3a2524 !important;
border-color: var(--color-muted-foreground) !important;
}
.dark body[data-theme-skin="builtin-src"] [role="switch"][aria-checked="true"] {
background-color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
}
.dark body[data-theme-skin="builtin-src"] [role="switch"][aria-checked="true"] > span {
background-color: #1c1917 !important;
}
/* ── Selected folder: M3 active-indicator treatment ─────────── */
/* M3 uses a pill-shaped tonal container for the active nav item. */
/* Bulwark's left-border accent becomes a 3 dp primary accent line */
/* + secondary-container (--color-accent) fill + gentle corner. */
body[data-theme-skin="builtin-src"] .bg-secondary.border-r .border-l-2.border-primary {
border-left-width: 3px !important;
border-left-color: var(--color-primary) !important;
background-color: var(--color-accent) !important;
border-radius: 0 12px 12px 0 !important;
}
/* ── Login card: MD3 extra-large + SRC red top accent strip ───── */
body[data-theme-skin="builtin-src"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm {
position: relative;
background-color: var(--color-card) !important;
border-color: rgba(213, 43, 30, 0.14) !important;
border-radius: 28px !important;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.08),
0 8px 32px rgba(0, 0, 0, 0.06) !important;
overflow: hidden;
}
body[data-theme-skin="builtin-src"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm::before {
content: "";
position: absolute;
left: 0; right: 0; top: 0;
height: 3px;
background: linear-gradient(90deg, transparent, #d52b1e 30%, #d52b1e 70%, transparent);
pointer-events: none;
}
/* Login page background: faint SRC red ambient wash */
body[data-theme-skin="builtin-src"] .min-h-screen.bg-gradient-to-br {
background-color: #f9f7f7 !important;
background-image: radial-gradient(56rem 38rem at 50% -12%, rgba(213, 43, 30, 0.05), transparent 60%) !important;
}
.dark body[data-theme-skin="builtin-src"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm {
border-color: rgba(239, 68, 68, 0.18) !important;
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.45),
0 0 40px -20px rgba(239, 68, 68, 0.18) !important;
}
.dark body[data-theme-skin="builtin-src"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm::before {
background: linear-gradient(90deg, transparent, #ef4444 30%, #ef4444 70%, transparent);
}
.dark body[data-theme-skin="builtin-src"] .min-h-screen.bg-gradient-to-br {
background-color: var(--color-background) !important;
background-image: radial-gradient(56rem 38rem at 50% -12%, rgba(239, 68, 68, 0.07), transparent 60%) !important;
}
/* ── Single-surface panes (M3 has no gradient empty states) ───── */
body[data-theme-skin="builtin-src"] .bg-gradient-to-br.from-muted\\/30.to-muted\\/50 {
background: var(--color-background) !important;
}
body[data-theme-skin="builtin-src"] .bg-muted\\/30 {
background-color: var(--color-background) !important;
}`;
export const BUILTIN_THEMES: InstalledTheme[] = [
{
id: 'builtin-vnclagoon',
name: 'VNClagoon',
version: '1.0.0',
author: 'VNC',
description: 'VNClagoon brand theme — deep navy with cyan accent, DM Sans + Syne',
css: vnclagoonCSS,
skin: vnclagoonSkin,
logoLightUrl: '/branding/vncmail-wordmark-on-light.svg',
logoDarkUrl: '/branding/vncmail-wordmark-on-dark.svg',
variants: ['light', 'dark'],
typography: { fontSans: '"DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif' },
enabled: true,
builtIn: true,
},
{
id: 'builtin-src',
name: 'SRC',
version: '1.1.0',
author: 'VNC',
description: 'SRC Advisory brand theme — Swiss red on white, MD3 components, light-first',
css: srcCSS,
skin: srcSkin,
logoLightUrl: '/branding/SRC_Symbol.png',
logoDarkUrl: '/branding/SRC_Symbol.png',
variants: ['light', 'dark'],
typography: { fontSans: '"DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif' },
enabled: true,
builtIn: true,
},
{
id: 'builtin-qui',
name: 'Qui',
version: '1.0.0',
author: 'Built-in',
description: 'Clean, modern zinc-and-blue theme inspired by autobrr/qui',
css: quiCSS,
variants: ['light', 'dark'],
enabled: true,
builtIn: true,
},
{
id: 'builtin-nord',
name: 'Nord',
version: '1.0.0',
author: 'Built-in',
description: 'Arctic, north-bluish color palette inspired by nordtheme.com',
css: nordCSS,
variants: ['light', 'dark'],
enabled: true,
builtIn: true,
},
{
id: 'builtin-catppuccin',
name: 'Catppuccin',
version: '1.0.0',
author: 'Built-in',
description: 'Soothing pastel theme with Latte (light) and Mocha (dark) variants',
css: catppuccinCSS,
variants: ['light', 'dark'],
enabled: true,
builtIn: true,
},
{
id: 'builtin-solarized',
name: 'Solarized',
version: '1.0.0',
author: 'Built-in',
description: 'Precision colors for machines and people by Ethan Schoonover',
css: solarizedCSS,
variants: ['light', 'dark'],
enabled: true,
builtIn: true,
},
{
id: 'builtin-roundcube-elastic',
name: 'Roundcube Elastic',
version: '1.0.0',
author: 'Built-in',
description: 'Faithful recreation of Roundcube\'s Elastic skin - Roboto, the #37beff blue, and the dark task-menu rail',
css: elasticCSS,
skin: elasticSkin,
variants: ['light', 'dark'],
typography: { fontSans: 'Roboto, "Helvetica Neue", Arial, sans-serif', baseFontSize: '14px' },
enabled: true,
builtIn: true,
},
{
id: 'builtin-aurora-glass',
name: 'Aurora Glass',
version: '1.0.0',
author: 'Built-in',
description: 'Ultra-modern glassmorphism - frosted blurred panes floating over a vibrant indigo→cyan gradient mesh, with glowing gradient actions',
css: auroraCSS,
skin: auroraSkin,
variants: ['light', 'dark'],
typography: { fontSans: '"Inter", "SF Pro Display", "Segoe UI", system-ui, sans-serif' },
enabled: true,
builtIn: true,
},
];