Skip to content

Theming System Design ​

Date: 2026-03-17 Branch: test/modern-styling-tweaks (dark theme experiment) → new branch for implementation Scope: Replace the dual color system with a unified CSS custom property architecture supporting light and dark mode, consolidate ~58 style files into ~14, and modernize visual polish.


1. Business Case ​

Why This Matters ​

The current styling architecture has accumulated significant technical debt that directly impacts development velocity, visual consistency, and our ability to ship a polished product.

The problem in numbers:

  • 2 competing color systems — _colors.scss (38 semantic variables like $background-panel) and _newcolors.scss (grey scale + 5 color families like $grey-20). Neither is used consistently.
  • ~95 Vue components contain hardcoded hex color values or references to old color tokens that compile to static values.
  • 47 global SCSS files in src/styles/ + 11 Reports-specific SCSS files in src/pages/Reports/styles/ with overlapping responsibilities (e.g., 6 files just for the data table).
  • 40+ files contain !important overrides to fight Vuetify's built-in theme--light defaults.
  • No runtime theme switching — SCSS compiles to static CSS. Dark mode required manually changing variable values and hoping nothing was hardcoded.

What we're fighting against:

Vuetify 2.3's theme system is minimal — it provides a light/dark toggle and a few color slots (primary, secondary, error). Our app doesn't use it. Instead, every component that uses a Vuetify element (v-card, v-sheet, v-list, v-data-table, v-menu, v-dialog) must individually override Vuetify's white defaults. This leads to code like:

scss
// This pattern is repeated in 40+ files
.v-card { background: $grey-20 !important; }
.v-sheet { background: $grey-20 !important; }
.v-list { background: $grey-20 !important; }

Every new component or page requires the developer to remember which Vuetify defaults need overriding, which color system to use, and where to put the styles. This is error-prone and slow.

What this unlocks:

  • Light + dark mode for end users, switchable at runtime
  • One place to change colors — adjust a primitive token, the entire app updates
  • Faster feature development — new components inherit correct colors automatically
  • Future extensibility — the architecture supports custom color themes without restructuring
  • Vuetify independence — the theme system works regardless of UI framework, easing future migration

2. Architecture ​

2.1 Three-Layer Token System ​

┌─────────────────────────────────────────────┐
│  Layer 3: Component Tokens (optional)       │
│  --table-row-alt, --input-bg                │
│  Only when a component needs an exception   │
├─────────────────────────────────────────────┤
│  Layer 2: Semantic Tokens                   │
│  --surface-raised, --text-primary           │
│  What the color MEANS — changes per theme   │
├─────────────────────────────────────────────┤
│  Layer 1: Primitive Tokens                  │
│  --primitive-grey-100, --primitive-blue-500  │
│  The actual OKLCH color values              │
└─────────────────────────────────────────────┘

Rule: Components only use semantic tokens (Layer 2) or component tokens (Layer 3). No component ever references a primitive token or a raw color value.

2.2 File Structure for Tokens ​

Critical architecture decision: CSS custom properties (:root { --var: value }) are runtime CSS, not SCSS compile-time variables. They must NOT be injected via Vite's additionalData — that would duplicate the entire :root block into every component's compiled CSS (~389 times).

Solution — two files:

  1. _theme-helpers.scss — SCSS-only file with helper mixins/functions for working with tokens. Injected via additionalData so all components have access. Contains no :root blocks, only SCSS constructs.

  2. theme.css (no underscore, plain CSS) — The :root and [data-theme] blocks defining all CSS custom properties. Imported once in main.ts. This is the single source of truth for all color values.

2.3 Primitive Tokens (OKLCH) ​

OKLCH (Oklab Lightness-Chroma-Hue) provides perceptually uniform color scaling. A lightness of 50% looks like 50% regardless of hue. This makes palette generation predictable and consistent.

Every OKLCH value includes a hex fallback for maximum browser compatibility:

css
:root {
  /* Grey scale — neutral with subtle blue undertone */
  /* Each value has a hex fallback above the OKLCH definition */
  --primitive-grey-50:  #1f2024;
  --primitive-grey-50:  oklch(15% 0.01 260);
  --primitive-grey-100: #333338;
  --primitive-grey-100: oklch(22% 0.01 260);
  --primitive-grey-150: #3e3e44;
  --primitive-grey-150: oklch(26% 0.01 260);
  --primitive-grey-200: #494950;
  --primitive-grey-200: oklch(30% 0.01 260);
  --primitive-grey-250: #54545c;
  --primitive-grey-250: oklch(34% 0.01 260);
  --primitive-grey-300: #606068;
  --primitive-grey-300: oklch(38% 0.01 260);
  --primitive-grey-400: #73737c;
  --primitive-grey-400: oklch(46% 0.01 260);
  --primitive-grey-500: #878790;
  --primitive-grey-500: oklch(54% 0.01 260);
  --primitive-grey-600: #9c9ca6;
  --primitive-grey-600: oklch(62% 0.01 260);
  --primitive-grey-700: #b5b5bf;
  --primitive-grey-700: oklch(72% 0.01 260);
  --primitive-grey-800: #cfcfd8;
  --primitive-grey-800: oklch(82% 0.01 260);
  --primitive-grey-850: #dddde6;
  --primitive-grey-850: oklch(87% 0.01 260);
  --primitive-grey-900: #ebebf2;
  --primitive-grey-900: oklch(92% 0.01 260);
  --primitive-grey-950: #f5f5fa;
  --primitive-grey-950: oklch(96% 0.01 260);
  --primitive-white:    #ffffff;
  --primitive-white:    oklch(100% 0 0);

  /* Brand colors */
  --primitive-blue-500:   oklch(58% 0.18 240);  /* primary */
  --primitive-red-500:    oklch(58% 0.20 18);    /* danger */
  --primitive-green-500:  oklch(62% 0.17 155);   /* success */
  --primitive-yellow-500: oklch(82% 0.14 85);    /* warning */

  /* Status colors (table indicators, categorical) */
  --primitive-status-blue:   #0693c7;
  --primitive-status-green:  #09d270;
  --primitive-status-purple: #7b76f5;
  --primitive-status-yellow: #fcc11f;
  --primitive-status-red:    #f50023;
  --primitive-status-pink:   #ea5391;
}

The hex fallback pattern (property: hex; property: oklch(...)) is standard progressive enhancement — browsers that don't support OKLCH use the hex value, modern browsers override with OKLCH.

The exact values will be tuned visually during implementation. The important thing is the structure — one place, easy to adjust.

2.4 Semantic Tokens ​

These map meaning to primitives. Switching theme = changing which primitives the semantic tokens point to.

css
/* Dark theme (default) */
[data-theme="dark"] {
  /* Surfaces */
  --surface-base:    var(--primitive-grey-100);  /* page background */
  --surface-raised:  var(--primitive-grey-150);  /* cards, panels */
  --surface-overlay: var(--primitive-grey-50);   /* topbar, dropdowns, modals */
  --surface-sunken:  var(--primitive-grey-50);   /* inputs, textareas */
  --surface-hover:   var(--primitive-grey-250);  /* hover states */
  --surface-active:  var(--primitive-grey-300);  /* active/pressed */

  /* Text */
  --text-primary:   var(--primitive-grey-850);  /* headings, body */
  --text-secondary: var(--primitive-grey-600);  /* labels, meta */
  --text-muted:     var(--primitive-grey-400);  /* placeholders, disabled */

  /* Borders */
  --border-default: var(--primitive-grey-250);  /* card edges, dividers */
  --border-strong:  var(--primitive-grey-400);  /* input borders */
  --border-subtle:  var(--primitive-grey-200);  /* row separators */

  /* Accents */
  --accent-primary: var(--primitive-blue-500);
  --accent-danger:  var(--primitive-red-500);
  --accent-success: var(--primitive-green-500);
  --accent-warning: var(--primitive-yellow-500);

  /* Status (categorical — same in both themes) */
  --status-blue:   var(--primitive-status-blue);
  --status-green:  var(--primitive-status-green);
  --status-purple: var(--primitive-status-purple);
  --status-yellow: var(--primitive-status-yellow);
  --status-red:    var(--primitive-status-red);
  --status-pink:   var(--primitive-status-pink);

  /* Focus */
  --focus-ring: var(--primitive-blue-500);

  /* Shadows — more visible on dark backgrounds */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
  --shadow-lg: 0 4px 24px rgba(0, 0, 0, 0.35);
}

/* Light theme */
[data-theme="light"] {
  --surface-base:    var(--primitive-grey-950);
  --surface-raised:  var(--primitive-white);
  --surface-overlay: var(--primitive-white);
  --surface-sunken:  var(--primitive-grey-900);
  --surface-hover:   var(--primitive-grey-900);
  --surface-active:  var(--primitive-grey-850);

  --text-primary:   var(--primitive-grey-100);
  --text-secondary: var(--primitive-grey-500);
  --text-muted:     var(--primitive-grey-600);

  --border-default: var(--primitive-grey-850);
  --border-strong:  var(--primitive-grey-700);
  --border-subtle:  var(--primitive-grey-900);

  --accent-primary: var(--primitive-blue-500);
  --accent-danger:  var(--primitive-red-500);
  --accent-success: var(--primitive-green-500);
  --accent-warning: var(--primitive-yellow-500);

  --status-blue:   var(--primitive-status-blue);
  --status-green:  var(--primitive-status-green);
  --status-purple: var(--primitive-status-purple);
  --status-yellow: var(--primitive-status-yellow);
  --status-red:    var(--primitive-status-red);
  --status-pink:   var(--primitive-status-pink);

  --focus-ring: var(--primitive-blue-500);

  /* Shadows — subtler on light backgrounds */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
  --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.1);
}

Note: Shadow tokens use rgba() for the shadow color component, not OKLCH. This is intentional — shadows are always black with opacity, and rgba() is universally supported. The OKLCH choice applies to visible color tokens only.

2.5 Theme Switching ​

typescript
// Runtime theme switch — one line
document.documentElement.setAttribute('data-theme', 'dark'); // or 'light'

// Persist preference
localStorage.setItem('cavai-theme', 'dark');

// Respect system preference as default
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

2.6 Vuetify Reset ​

Important: Do NOT change Vuetify's theme.dark property in main.ts. Leave Vuetify permanently in "light" mode. The _vuetify-reset.scss file neutralizes its defaults, and our data-theme attribute handles actual theme switching independently of Vuetify. Toggling Vuetify's own dark theme would re-introduce a second set of color defaults to fight against.

A single file that neutralizes all Vuetify theme--light defaults so our tokens take over:

scss
// _vuetify-reset.scss (~50 lines)
.theme--light.v-application,
.theme--light.v-sheet,
.theme--light.v-card,
.theme--light.v-list,
.theme--light.v-data-table,
.theme--light.v-menu__content,
.theme--light.v-dialog,
.theme--light.v-expansion-panels,
.theme--light.v-expansion-panel,
.theme--light.v-autocomplete__content {
  background-color: var(--surface-raised);
  color: var(--text-primary);
}

.theme--light.v-application {
  background-color: var(--surface-base);
}

// Input overrides
.theme--light .v-input input,
.theme--light .v-input textarea,
.theme--light .v-select__selection {
  color: var(--text-primary);
}

.theme--light .v-label {
  color: var(--text-secondary);
}

This one file eliminates the need for per-component Vuetify overrides scattered across 40+ files. After this reset, Vuetify components inherit our tokens automatically.


3. File Consolidation ​

Current: ~58 files ​

src/styles/ (47 files):

_animations.scss, _colors.scss, _fonts.scss, _newcolors.scss, _shadows.scss,
_variables.scss, ActionMenu.scss, Breadcrumbs.scss, BuilderPanels.scss,
BuilderTop.scss, Buttons.scss, CTooltip.scss, Chip.scss, ContentTop.scss,
DatePicker.scss, Dialog.scss, DropdownsAndSelects.scss, EditProfileForm.scss,
FormShell.scss, GlobalTypography.scss, ImageUploader.scss, Input.scss,
List.scss, LoadingCircle.scss, MainPage.scss, MainPageTiles.scss,
MainTable.scss, MainTableFooter.scss, MainTableHeader.scss,
MainTableNumeric.scss, MainTableSearch.scss, MainTableStatus.scss,
MiddlewareOp.scss, OperatorStyles.scss, OptionRowShared.scss, Preview.scss,
ReportsManagementView.scss, ReportsTable.scss, ScrollBar.scss, SignIn.scss,
Stepper.scss, Toast.scss, TopBar.scss, TreeView.scss, Wizards.scss,
user_profile.scss, vertical_tabbed_layout_variables.scss, vuetify.min.custom.css

src/pages/Reports/styles/ (11 files):

reports.scss, reports-charts.scss, reports-charts-header.scss,
reports-data-sources.scss, reports-dialogs.scss, reports-heading.scss,
reports-table-container.scss, reports-table-wrapper.scss, reports-tabs.scss,
reports-templates.scss, reports-vuetify-overwrite.scss

src/pages/Chatbots/components/CavaiFlow/flowactors/invert.scss:

Contains 130+ *-invert and *-invert-alt variables from _newcolors.scss, used for a localized light-mode flow editor. This becomes obsolete — the .invert class is replaced by applying [data-theme="light"] to the flow editor container.

Target: ~14 files ​

src/styles/
├── theme.css                      # CSS custom properties — primitives, semantic tokens,
│                                  # shadows, motion tokens. Imported ONCE in main.ts.
├── _theme-helpers.scss            # SCSS helpers/mixins for token access. In additionalData.
├── _variables.scss                # Sizing, spacing, border-radius (kept, absorbs
│                                  # vertical_tabbed_layout_variables sizing vars)
├── _fonts.scss                    # Font faces (kept)
├── _animations.scss               # Keyframes + animation definitions (modernized)
├── _vuetify-reset.scss            # Neutralize Vuetify theme--light defaults
├── _components-navigation.scss    # TopBar, Breadcrumbs, ContentTop
├── _components-tables.scss        # MainTable, header, footer, numeric, search, status
├── _components-forms.scss         # Input, DropdownsAndSelects, DatePicker, ImageUploader
├── _components-dialogs.scss       # Dialog, FormShell, Wizards
├── _components-buttons.scss       # Buttons
├── _components-builder.scss       # BuilderPanels, BuilderTop, OptionRowShared,
│                                  # OperatorStyles, Preview, MiddlewareOp
├── _components-misc.scss          # Toast, Chip, CTooltip, LoadingCircle, ScrollBar,
│                                  # ActionMenu, List, Stepper, TreeView
├── _pages.scss                    # MainPage, MainPageTiles, SignIn, EditProfileForm,
│                                  # user_profile
├── _reports.scss                  # All 11 Reports SCSS files consolidated (215 color
│                                  # declarations warrant a dedicated file)
└── vuetify.min.custom.css         # Vuetify base CSS (kept, untouched)

Deleted entirely:

  • _colors.scss — replaced by theme.css
  • _newcolors.scss — replaced by theme.css (including all *-invert / *-invert-alt variables)
  • _shadows.scss — shadow tokens move into theme.css
  • GlobalTypography.scss — base text styles handled by Vuetify reset + tokens
  • vertical_tabbed_layout_variables.scss — sizing into _variables.scss, colors into tokens
  • invert.scss — replaced by [data-theme="light"] scoping on flow editor
  • All 34 individual component SCSS files in src/styles/ (replaced by 8 consolidated files)
  • All 11 individual Reports SCSS files (replaced by _reports.scss)

Vite additionalData updated to inject:

  1. _theme-helpers.scss (SCSS helpers only — NO :root blocks)
  2. _variables.scss
  3. _fonts.scss
  4. _animations.scss

Imported once in main.ts:

  • theme.css (CSS custom properties)
  • _vuetify-reset.scss
  • All _components-*.scss and _pages.scss and _reports.scss
  • ScrollBar.scss styles (previously in additionalData, moved to entry-point import)

4. Known Issues Catalog ​

Complete inventory of visual issues discovered during the dark theme experiment. All are resolved by the token migration.

Surfaces (white backgrounds) ​

ComponentCurrent IssueToken Fix
Vuetify defaults (v-card, v-sheet, v-list, v-data-table)White from theme--light_vuetify-reset.scss → --surface-raised
Modals (Create Brand, Create Campaign, Bulk Tag Export)White modal background--surface-raised on .v-dialog .v-card
Dropdown menusHardcoded #EFEFEF hover--surface-hover
Color picker popupWhite v-card wrapper--surface-overlay
Builder preview panelWhite iframe border/frame--border-default
Dashboard cards (MainPageTiles)Were $global-white--surface-raised
Table containerNo dark background set--surface-raised
Data table rowsWhite default--surface-raised + zebra via --surface-hover
Number input spinnersWhite native WebKit controlsGlobal CSS reset (not token-related)
Outlined buttonsWhite background--surface-raised + --border-default
Export tag badgesWhite/light background--surface-active + --border-default
"+12 more" / "show less"White text/background--text-secondary
Format tags in tableBlend with background--surface-active + --text-secondary

Text (invisible on dark background) ​

ComponentCurrent IssueToken Fix
Breadcrumbs$typography-minus4 (#2C3038) — invisible--text-secondary
Page titles (ContentHeader)Inherited black--text-primary
Table numeric dataNo color set — inherited black--text-secondary
Table link/name text$typography-minus6 (#14171E)--text-primary
Search input textHardcoded #252525--text-primary
Dialog title$typography-default (medium gray)--text-primary
Dialog subtitle$typography-minus1 (dark gray)--text-secondary
Select dropdown text$grey-25 (too dark)--text-primary

Borders & Dividers ​

ComponentCurrent IssueToken Fix
Dashboard card divider$grey-85 (too light/white)--border-default
Table header border$typography-plus6 (light gray)--border-default
Table row separatorsLight mix() formula--border-subtle
Slider section dividers$grey-95 (nearly white)--border-subtle
Card section dividers$grey-95--border-subtle
Dropdown separator$grey-90--border-subtle
Form field bordersVarious light values--border-strong

Interactive States ​

ComponentCurrent IssueToken Fix
Dropdown hover$grey-95 / #EFEFEF--surface-hover
Table row hoverLight mix() formula--surface-hover
Button activeVarious light values--surface-active
Close button (dialogs)White backgroundtransparent + --text-secondary icon
Focus outline (builder blocks)White-ish outline--focus-ring (accent-primary)
Tooltip backgroundsWhite--surface-overlay
Dropdown caret$global-white background--surface-overlay
Sort arrow (table header)$global-white--text-secondary

Components Not Yet Verified ​

These were not encountered during the dark theme experiment but likely have similar issues:

  • Reports dashboard charts and tables (11 SCSS files, 215 color declarations)
  • Stepper component
  • Sign-in page
  • User profile/edit profile
  • Template management views
  • Image uploader states
  • Date picker popup
  • App.vue style block (references $background-body, $primary-font-color)

5. Animation & Polish Modernization ​

As part of the consolidation, update transitions and animations to feel more modern and consistent.

Current State ​

  • _animations.scss defines some keyframes
  • _variables.scss has transition tokens: $transition-quick (0.15s), $transition-long (0.4s), $transition-medium (0.25s)
  • Many components define their own ad-hoc transitions (e.g., transition: background-color 500ms cubic-bezier(0, 0.5, 0, 1))

New Approach ​

Standardize on a small set of motion tokens using modern easing. These are defined in theme.css alongside color tokens (single import, no duplication):

css
:root {
  /* Duration */
  --duration-fast:   100ms;
  --duration-normal: 200ms;
  --duration-slow:   350ms;

  /* Easing — snappy, modern feel */
  --ease-out:     cubic-bezier(0.16, 1, 0.3, 1);    /* quick deceleration */
  --ease-in-out:  cubic-bezier(0.45, 0, 0.55, 1);   /* balanced */
  --ease-spring:  cubic-bezier(0.34, 1.56, 0.64, 1); /* slight overshoot */

  /* Composed transitions */
  --transition-colors: var(--duration-fast) var(--ease-out);
  --transition-transform: var(--duration-normal) var(--ease-spring);
  --transition-layout: var(--duration-slow) var(--ease-in-out);
}

Usage pattern:

css
.card {
  transition: background-color var(--transition-colors),
              box-shadow var(--transition-colors);
}

Polish Targets ​

  • Hover transitions — consistent --duration-fast everywhere instead of scattered 70ms–1000ms values
  • Card hover — subtle translateY(-1px) + shadow lift instead of the current size-change approach
  • Border radius — already standardized to $border-radius-milli (6px), $border-radius-base (10px), $border-radius-kilo (14px). Keep as-is in _variables.scss.
  • Shadows — theme-aware via --shadow-sm/md/lg, stronger on dark to compensate for low-contrast backgrounds
  • Table rows — smooth color transitions on hover
  • Buttons — consistent press feedback

6. Migration Strategy ​

Approach: Single PR ​

Build the entire new system and migrate all files in one branch. No intermediate state to maintain.

Execution Order ​

Phase 1: Foundation

  1. Create theme.css with all primitive and semantic tokens (including hex fallbacks)
  2. Create _theme-helpers.scss with any SCSS helper mixins
  3. Create _vuetify-reset.scss
  4. Update vite.config.ts — change additionalData to inject _theme-helpers.scss instead of _colors.scss and _newcolors.scss
  5. Update main.ts — import theme.css and _vuetify-reset.scss
  6. Update index.html body background to use system preference
  7. Update App.vue style block — replace $background-body and .theme--light overrides with tokens

Phase 2: Global Style Migration 8. Migrate all 47 src/styles/*.scss files:

  • Replace all $grey-XX, $background-*, $border-*, $typography-*, $illustration-*, $global-white, $white-color, $black-color with semantic tokens
  • Remove all !important overrides that exist solely to fight Vuetify
  • Consolidate into the target files
  1. Migrate all 11 src/pages/Reports/styles/*.scss files into _reports.scss
  2. Delete _colors.scss, _newcolors.scss, _shadows.scss, invert.scss

Phase 3: Component Migration 11. Migrate all Vue components with <style> blocks referencing old SCSS variables or hardcoded hex values 12. Special attention to: - App.vue — root styles - Builder components (largest surface area) - Reports components (charts, tables, 215 color declarations) - Dialog/modal components - Flow editor components (replace .invert class with [data-theme="light"] scoping)

Phase 4: Theme Switching 13. Add theme toggle mechanism (localStorage + system preference detection) 14. Add theme toggle UI (location TBD — likely user menu in TopBar)

Phase 5: Verification & Polish 15. Visual QA of every page/view in both themes 16. Animation/transition modernization pass 17. Final cleanup of any remaining hardcoded values

Graduated Color Scales ​

The current _colors.scss defines graduated scales like $primary-plus1 through $primary-plus7 and $primary-minus1 through $primary-minus7 (used in 28 files, 61 occurrences). Decision for migration:

  • Accent colors that only appear at full strength (buttons, links, status indicators) → use --accent-primary directly
  • Lighter/darker accent variants (hover states, subtle backgrounds) → use opacity modifiers or OKLCH lightness adjustments in component tokens where needed, rather than pre-defining 14 variants per color
  • Status indicator colors → preserved as-is via --status-* tokens (they're categorical, not scaled)

Risk Mitigation ​

  • Large PR risk: Mitigated by systematic, file-by-file approach. Each file migration is mechanical (replace old token → new token). The creative decisions are in the token definitions, not in the migration.
  • Merge conflicts: Minimize by communicating with team about the branch, and merging promptly once complete.
  • Visual regressions: Verify each page in both light and dark mode before marking complete.
  • OKLCH browser support: Supported in all modern browsers (Chrome 111+, Firefox 113+, Safari 15.4+). Hex fallbacks included for every primitive token using the standard progressive enhancement pattern (property: hex; property: oklch(...)). Opera Mini and older iOS Safari get hex values.

7. Variable Migration Reference ​

Quick-reference mapping for the mechanical migration. Context determines which semantic token to use.

Backgrounds → Surface tokens ​

Old Variable(s)New TokenUsage
$background-body, $grey-15var(--surface-base)Page background
$background-content, $background-panel, $grey-20var(--surface-raised)Cards, panels, tables
$background-header, $grey-10var(--surface-overlay)TopBar, dropdowns, modals
$background-light, $grey-25 (as input bg)var(--surface-sunken)Inputs, textareas
$global-white, $white-color, #FFFFFFContext-dependent--surface-raised or --surface-overlay

Text → Text tokens ​

Old Variable(s)New TokenUsage
$typography-minus6, $typography-minus7, $grey-80 (as text)var(--text-primary)Headings, body text
$typography-default, $grey-60 (as text)var(--text-secondary)Labels, captions
$grey-50 (as placeholder), $global-disabledvar(--text-muted)Placeholders, disabled

Borders → Border tokens ​

Old Variable(s)New TokenUsage
$border-default, $grey-30, $grey-35var(--border-default)Card edges, dividers
$text-input-bottom-border, $grey-40, $grey-45var(--border-strong)Input borders
$grey-85, $grey-90, $grey-95 (as separator)var(--border-subtle)Row separators, sections

Interactive → Surface/accent tokens ​

Old Variable(s)New TokenUsage
$grey-25 (as hover bg)var(--surface-hover)Row/item hover
$grey-30 (as active bg)var(--surface-active)Active/pressed
$primary-defaultvar(--accent-primary)Primary buttons, links
$error-color, $secondary-defaultvar(--accent-danger)Error states
$tertiary-defaultvar(--accent-success)Success states
$warning-colorvar(--accent-warning)Warnings

8. Files Changed ​

New Files ​

  • src/styles/theme.css — CSS custom properties (primitives + semantic + shadows + motion)
  • src/styles/_theme-helpers.scss — SCSS helpers for token access
  • src/styles/_vuetify-reset.scss — Vuetify neutralization

Consolidated Files (new, replacing many) ​

  • src/styles/_components-navigation.scss
  • src/styles/_components-tables.scss
  • src/styles/_components-forms.scss
  • src/styles/_components-dialogs.scss
  • src/styles/_components-buttons.scss
  • src/styles/_components-builder.scss
  • src/styles/_components-misc.scss
  • src/styles/_pages.scss
  • src/styles/_reports.scss

Modified Files ​

  • src/styles/_variables.scss — absorb sizing vars from vertical_tabbed_layout_variables
  • src/styles/_animations.scss — modernize, remove ad-hoc transition definitions
  • vite.config.ts — update additionalData imports
  • src/main.ts — add theme.css import, theme initialization
  • src/App.vue — migrate style block to tokens
  • index.html — system preference background color
  • ~95 Vue component files — replace hardcoded colors with tokens

Deleted Files ​

  • src/styles/_colors.scss
  • src/styles/_newcolors.scss
  • src/styles/_shadows.scss
  • src/styles/GlobalTypography.scss
  • src/styles/vertical_tabbed_layout_variables.scss
  • src/pages/Chatbots/components/CavaiFlow/flowactors/invert.scss
  • All 34 individual component SCSS files in src/styles/ (replaced by 9 consolidated files)
  • All 11 Reports SCSS files in src/pages/Reports/styles/ (replaced by _reports.scss)

9. Success Criteria ​

  1. Zero hardcoded color values in any SCSS or Vue component <style> block
  2. Zero !important overrides for color/background properties (except where Vuetify specificity genuinely requires it)
  3. Light and dark mode both visually correct across all pages
  4. Runtime theme switching works via data-theme attribute
  5. ~14 style files instead of ~58
  6. One color system instead of two
  7. Build passes with no SCSS compilation errors
  8. All existing functionality preserved — no behavioral changes
  9. Hex fallbacks for all OKLCH values

10. Out of Scope ​

  • Custom user themes / brand colors — architecture supports it, but no UI for it in this PR
  • Vuetify migration (to v3 or removal) — separate project
  • Vuetify theme.dark toggle — Vuetify stays in light mode permanently; our system handles theming
  • Component library extraction — separate project
  • Responsive design changes — not part of this work
  • New features — this is purely a styling architecture change

Internal documentation