From 1f99eb0496cac6207dea35240a24dc1bde717bc7 Mon Sep 17 00:00:00 2001 From: Taras Romaniv Date: Tue, 31 Mar 2026 02:49:05 +0200 Subject: [PATCH 1/2] fix: preserve local custom module sources during quick update (#2172) * fix: preserve local custom module sources during quick update Keep customModules in the generated main manifest so local custom module source paths survive update runs. Load those preserved source paths during stock quick update before falling back to the custom cache directory. This fixes the case where BMAD would drop customModules, lose the original source path for a local module, and then skip the module or try to re-cache from _bmad/_config/custom/, which could fail with ENOENT after the cache directory was removed. Also adds an installation component regression test to verify customModules and sourcePath are preserved in manifest generation. Fixes #1582 * fix: ensure consistent formatting * refactor: extract quick update custom source assembly Move quick-update custom module source collection out of Installer and into CustomModules as assembleQuickUpdateSources(). This keeps discoverPaths() focused on consuming prepared install inputs while making the quick-update source assembly step explicit and easier to evolve. Also: - preserve customModules metadata in manifest regeneration for installed modules - drop stale customModules entries when modules are no longer installed - cover manifest preservation and manifest-backed quick-update sources in tests --- test/test-installation-components.js | 153 +++++++++++++++++++++ tools/installer/core/installer.js | 59 +------- tools/installer/core/manifest-generator.js | 9 ++ tools/installer/modules/custom-modules.js | 105 ++++++++++++++ 4 files changed, 273 insertions(+), 53 deletions(-) diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 4e5fa7282..b548cbabe 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -14,7 +14,9 @@ const path = require('node:path'); const os = require('node:os'); const fs = require('fs-extra'); +const { Installer } = require('../tools/installer/core/installer'); const { ManifestGenerator } = require('../tools/installer/core/manifest-generator'); +const { OfficialModules } = require('../tools/installer/modules/official-modules'); const { IdeManager } = require('../tools/installer/ide/manager'); const { clearCache, loadPlatformCodes } = require('../tools/installer/ide/platform-codes'); @@ -126,6 +128,56 @@ async function createSkillCollisionFixture() { return { root: fixtureRoot, bmadDir: fixtureDir }; } +async function createCustomModuleManifestFixture() { + const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-custom-manifest-')); + const bmadDir = path.join(fixtureRoot, '_bmad'); + const configDir = path.join(bmadDir, '_config'); + const moduleSourceDir = path.join(fixtureRoot, 'test-module-source'); + await fs.ensureDir(configDir); + await fs.ensureDir(moduleSourceDir); + + const minimalAgent = 'p'; + await fs.ensureDir(path.join(bmadDir, 'core', 'agents')); + await fs.writeFile(path.join(bmadDir, 'core', 'agents', 'test.md'), minimalAgent); + await fs.ensureDir(path.join(bmadDir, 'test-module', 'agents')); + await fs.writeFile(path.join(bmadDir, 'test-module', 'agents', 'test.md'), minimalAgent); + await fs.writeFile(path.join(moduleSourceDir, 'module.yaml'), ['code: test-module', 'name: Test Module', ''].join('\n')); + + await fs.writeFile( + path.join(configDir, 'manifest.yaml'), + [ + 'installation:', + ' version: 6.2.2', + ' installDate: 2026-03-30T00:00:00.000Z', + ' lastUpdated: 2026-03-30T00:00:00.000Z', + 'modules:', + ' - name: core', + ' version: 6.2.2', + ' installDate: 2026-03-30T00:00:00.000Z', + ' lastUpdated: 2026-03-30T00:00:00.000Z', + ' source: built-in', + ' npmPackage: null', + ' repoUrl: null', + ' - name: test-module', + ' version: null', + ' installDate: 2026-03-30T00:00:00.000Z', + ' lastUpdated: 2026-03-30T00:00:00.000Z', + ' source: custom', + ' npmPackage: null', + ' repoUrl: null', + 'customModules:', + ' - id: test-module', + ' name: "Test Module"', + ` sourcePath: ${JSON.stringify(moduleSourceDir)}`, + 'ides:', + ' - codex', + '', + ].join('\n'), + ); + + return { root: fixtureRoot, bmadDir, manifestPath: path.join(configDir, 'manifest.yaml'), moduleSourceDir }; +} + /** * Test Suite */ @@ -1713,6 +1765,107 @@ async function runTests() { console.log(''); + // ============================================================ + // Suite 33: Main manifest preserves active customModules only + // ============================================================ + console.log(`${colors.yellow}Test Suite 33: Preserve active customModules in main manifest${colors.reset}\n`); + + let customManifestFixture = null; + try { + customManifestFixture = await createCustomModuleManifestFixture(); + const yaml = require('yaml'); + const originalManifest = yaml.parse(await fs.readFile(customManifestFixture.manifestPath, 'utf8')); + originalManifest.customModules.push({ + id: 'removed-module', + name: 'Removed Module', + sourcePath: path.join(customManifestFixture.root, 'removed-module-source'), + }); + await fs.writeFile(customManifestFixture.manifestPath, yaml.stringify(originalManifest), 'utf8'); + + const generator33 = new ManifestGenerator(); + await generator33.generateManifests(customManifestFixture.bmadDir, ['core', 'test-module'], [], { ides: ['codex'] }); + + const updatedManifest = yaml.parse(await fs.readFile(customManifestFixture.manifestPath, 'utf8')); + const customModule = updatedManifest.customModules?.find((entry) => entry.id === 'test-module'); + + assert(Array.isArray(updatedManifest.customModules), 'Main manifest keeps customModules array'); + assert(customModule !== undefined, 'Main manifest preserves existing custom module entry'); + assert( + customModule && customModule.sourcePath === customManifestFixture.moduleSourceDir, + 'Main manifest preserves custom module sourcePath', + ); + assert( + !updatedManifest.customModules?.some((entry) => entry.id === 'removed-module'), + 'Main manifest drops stale custom module entries', + ); + } catch (error) { + assert(false, 'Main manifest preserves customModules test succeeds', error.message); + } finally { + if (customManifestFixture?.root) await fs.remove(customManifestFixture.root).catch(() => {}); + } + + console.log(''); + + // ============================================================ + // Suite 34: Quick update uses manifest-backed custom sources + // ============================================================ + console.log(`${colors.yellow}Test Suite 34: Quick update uses manifest-backed custom module sources${colors.reset}\n`); + + let quickUpdateFixture = null; + const originalListAvailable34 = OfficialModules.prototype.listAvailable; + const originalLoadExistingConfig34 = OfficialModules.prototype.loadExistingConfig; + const originalCollectModuleConfigQuick34 = OfficialModules.prototype.collectModuleConfigQuick; + try { + quickUpdateFixture = await createCustomModuleManifestFixture(); + const installer34 = new Installer(); + installer34.externalModuleManager.hasModule = async () => false; + installer34.externalModuleManager.listAvailable = async () => []; + + let capturedInstallConfig34 = null; + installer34.install = async (config) => { + capturedInstallConfig34 = config; + return { success: true }; + }; + + OfficialModules.prototype.listAvailable = async function () { + return { modules: [], customModules: [] }; + }; + OfficialModules.prototype.loadExistingConfig = async function () { + this.collectedConfig = this.collectedConfig || {}; + }; + OfficialModules.prototype.collectModuleConfigQuick = async function (moduleName) { + this.collectedConfig = this.collectedConfig || {}; + if (!this.collectedConfig[moduleName]) { + this.collectedConfig[moduleName] = {}; + } + return false; + }; + + await installer34.quickUpdate({ + directory: quickUpdateFixture.root, + skipPrompts: true, + }); + + const customModule34 = capturedInstallConfig34?._customModuleSources?.get('test-module'); + + assert(capturedInstallConfig34 !== null, 'Quick update forwards config to install'); + assert(customModule34 !== undefined, 'Quick update keeps manifest-backed custom module updateable'); + assert(customModule34 && customModule34.cached === false, 'Quick update uses manifest-backed source before cache'); + assert( + customModule34 && customModule34.sourcePath === quickUpdateFixture.moduleSourceDir, + 'Quick update uses preserved manifest sourcePath for custom modules', + ); + } catch (error) { + assert(false, 'Quick update manifest-backed custom source test succeeds', error.message); + } finally { + OfficialModules.prototype.listAvailable = originalListAvailable34; + OfficialModules.prototype.loadExistingConfig = originalLoadExistingConfig34; + OfficialModules.prototype.collectModuleConfigQuick = originalCollectModuleConfigQuick34; + if (quickUpdateFixture?.root) await fs.remove(quickUpdateFixture.root).catch(() => {}); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 111c88b54..a0ea9a66e 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -1144,59 +1144,12 @@ class Installer { const configuredIdes = existingInstall.ides; const projectRoot = path.dirname(bmadDir); - // Get custom module sources: first from --custom-content (re-cache from source), then from cache - const customModuleSources = new Map(); - if (config.customContent?.sources?.length > 0) { - for (const source of config.customContent.sources) { - if (source.id && source.path && (await fs.pathExists(source.path))) { - customModuleSources.set(source.id, { - id: source.id, - name: source.name || source.id, - sourcePath: source.path, - cached: false, // From CLI, will be re-cached - }); - } - } - } - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); - - for (const cachedModule of cachedModules) { - const moduleId = cachedModule.name; - const cachedPath = path.join(cacheDir, moduleId); - - // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT - if (!(await fs.pathExists(cachedPath))) { - continue; - } - if (!cachedModule.isDirectory()) { - continue; - } - - // Skip if we already have this module from manifest - if (customModuleSources.has(moduleId)) { - continue; - } - - // Check if this is an external official module - skip cache for those - const isExternal = await this.externalModuleManager.hasModule(moduleId); - if (isExternal) { - continue; - } - - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - customModuleSources.set(moduleId, { - id: moduleId, - name: moduleId, - sourcePath: cachedPath, - cached: true, - }); - } - } - } + const customModuleSources = await this.customModules.assembleQuickUpdateSources( + config, + existingInstall, + bmadDir, + this.externalModuleManager, + ); // Get available modules (what we have source for) const availableModulesData = await new OfficialModules().listAvailable(); diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index 65e0f4ed3..bef6f2d23 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -377,10 +377,12 @@ class ManifestGenerator { */ async writeMainManifest(cfgDir) { const manifestPath = path.join(cfgDir, 'manifest.yaml'); + const installedModuleSet = new Set(this.modules); // Read existing manifest to preserve install date let existingInstallDate = null; const existingModulesMap = new Map(); + let existingCustomModules = []; if (await fs.pathExists(manifestPath)) { try { @@ -402,6 +404,12 @@ class ManifestGenerator { } } } + + if (existingManifest.customModules && Array.isArray(existingManifest.customModules)) { + // We filter here so manifest regeneration preserves source metadata only for custom modules that + // are still installed. Without that, customModules can retain stale entries for modules that were removed. + existingCustomModules = existingManifest.customModules.filter((customModule) => installedModuleSet.has(customModule?.id)); + } } catch { // If we can't read existing manifest, continue with defaults } @@ -437,6 +445,7 @@ class ManifestGenerator { lastUpdated: new Date().toISOString(), }, modules: updatedModules, + customModules: existingCustomModules, ides: this.selectedIdes, }; diff --git a/tools/installer/modules/custom-modules.js b/tools/installer/modules/custom-modules.js index b41bf47b1..3f8b793be 100644 --- a/tools/installer/modules/custom-modules.js +++ b/tools/installer/modules/custom-modules.js @@ -192,6 +192,111 @@ class CustomModules { return this.paths; } + + /** + * Assemble quick-update source candidates before install() hands them to discoverPaths(). + * This exists because discoverPaths() consumes already-prepared quick-update sources, + * while quickUpdate() still has to build that source map from manifest, explicit inputs, + * and cache conventions. + * Precedence: manifest-backed paths, explicit sources override them, then cached modules. + * @param {Object} config - Quick update configuration + * @param {Object} existingInstall - Existing installation snapshot + * @param {string} bmadDir - BMAD directory + * @param {Object} externalModuleManager - External module manager + * @returns {Promise>} Map of custom module ID to source info + */ + async assembleQuickUpdateSources(config, existingInstall, bmadDir, externalModuleManager) { + const projectRoot = path.dirname(bmadDir); + const customModuleSources = new Map(); + + if (existingInstall.customModules) { + for (const customModule of existingInstall.customModules) { + // Skip if no ID - can't reliably track or re-cache without it + if (!customModule?.id) continue; + + let sourcePath = customModule.sourcePath; + if (sourcePath && sourcePath.startsWith('_config')) { + // Paths are relative to BMAD dir, but we want absolute paths for install + sourcePath = path.join(bmadDir, sourcePath); + } else if (!sourcePath && customModule.relativePath) { + // Fall back to relativePath + sourcePath = path.resolve(projectRoot, customModule.relativePath); + } else if (sourcePath && !path.isAbsolute(sourcePath)) { + // If we have a sourcePath but it's not absolute, resolve it relative to project root + sourcePath = path.resolve(projectRoot, sourcePath); + } + + // If we still don't have a valid source path, skip this module + if (!sourcePath || !(await fs.pathExists(sourcePath))) { + continue; + } + + customModuleSources.set(customModule.id, { + id: customModule.id, + name: customModule.name || customModule.id, + sourcePath, + relativePath: customModule.relativePath, + cached: false, + }); + } + } + + if (config.customContent?.sources?.length > 0) { + for (const source of config.customContent.sources) { + if (source.id && source.path) { + customModuleSources.set(source.id, { + id: source.id, + name: source.name || source.id, + sourcePath: source.path, + cached: false, // From CLI, will be re-cached + }); + } + } + } + + const cacheDir = path.join(bmadDir, '_config', 'custom'); + if (!(await fs.pathExists(cacheDir))) { + return customModuleSources; + } + + const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); + for (const cachedModule of cachedModules) { + const moduleId = cachedModule.name; + const cachedPath = path.join(cacheDir, moduleId); + + // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT + if (!(await fs.pathExists(cachedPath))) { + continue; + } + if (!cachedModule.isDirectory()) { + continue; + } + + // Skip if we already have this module from manifest + if (customModuleSources.has(moduleId)) { + continue; + } + + // Check if this is an external official module - skip cache for those + const isExternal = await externalModuleManager.hasModule(moduleId); + if (isExternal) { + continue; + } + + // Check if this is actually a custom module (has module.yaml) + const moduleYamlPath = path.join(cachedPath, 'module.yaml'); + if (await fs.pathExists(moduleYamlPath)) { + customModuleSources.set(moduleId, { + id: moduleId, + name: moduleId, + sourcePath: cachedPath, + cached: true, + }); + } + } + + return customModuleSources; + } } module.exports = { CustomModules }; From 2c5436f67291235321955e3daa443e479cffd01e Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 1 Apr 2026 01:12:40 -0500 Subject: [PATCH 2/2] style: update docs theme to match bmadcode.com Ghost blog (#2176) Replace purple/electric blue accent with Ghost blog design tokens: - Background #0a0a0a, surface #1a1a1a, borders #262626 - Accent blue #3b82f6, text #fafafa/#a1a1a1/#666666 - Inter body, Space Grotesk headings, JetBrains Mono code - Remove logo images, use text title --- website/astro.config.mjs | 6 -- website/src/components/Banner.astro | 13 +-- website/src/styles/custom.css | 121 ++++++++++++++++------------ 3 files changed, 76 insertions(+), 64 deletions(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 9d7efd99e..1ec2cb310 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -50,12 +50,6 @@ export default defineConfig({ defaultLocale: 'root', locales, - logo: { - light: './public/img/bmad-light.png', - dark: './public/img/bmad-dark.png', - alt: 'BMAD Method', - replacesTitle: true, - }, favicon: '/favicon.ico', // Social links diff --git a/website/src/components/Banner.astro b/website/src/components/Banner.astro index 00944d669..2b607f621 100644 --- a/website/src/components/Banner.astro +++ b/website/src/components/Banner.astro @@ -12,16 +12,16 @@ const llmsFullUrl = `${getSiteUrl()}/llms-full.txt`; .ai-banner { width: 100%; height: var(--ai-banner-height, 2.75rem); - background: #334155; - color: #cbd5e1; + background: #1a1a1a; + color: #a1a1a1; padding: 0.5rem 1rem; font-size: 0.875rem; - border-bottom: 1px solid rgba(140, 140, 255, 0.15); + border-bottom: 1px solid #262626; display: flex; align-items: center; justify-content: center; box-sizing: border-box; - font-family: system-ui, sans-serif; + font-family: 'Inter', system-ui, sans-serif; } /* Truncate text on narrow screens */ @@ -32,15 +32,16 @@ const llmsFullUrl = `${getSiteUrl()}/llms-full.txt`; max-width: 100%; } .ai-banner a { - color: #B9B9FF; + color: #3b82f6; text-decoration: none; font-weight: 600; } .ai-banner a:hover { + color: #fafafa; text-decoration: underline; } .ai-banner a:focus-visible { - outline: 2px solid #B9B9FF; + outline: 2px solid #3b82f6; outline-offset: 2px; border-radius: 2px; } diff --git a/website/src/styles/custom.css b/website/src/styles/custom.css index 3c1c6d742..6ab5b2ee5 100644 --- a/website/src/styles/custom.css +++ b/website/src/styles/custom.css @@ -1,14 +1,15 @@ /** * BMAD Method Documentation - Custom Styles for Starlight - * Electric Blue theme optimized for dark mode + * Dark theme matching bmadcode.com Ghost blog * - * CSS Variable Mapping: - * Docusaurus → Starlight - * --ifm-color-primary → --sl-color-accent - * --ifm-background-color → --sl-color-bg - * --ifm-font-color-base → --sl-color-text + * Design tokens from Ghost theme: + * Background: #0a0a0a | Surface: #1a1a1a | Border: #262626 + * Accent: #3b82f6 | Gold: #d4a853 | Text: #fafafa/#a1a1a1/#666666 */ +/* Google Fonts - match Ghost blog typography */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); + /* ============================================ COLOR PALETTE - Light Mode ============================================ */ @@ -19,10 +20,10 @@ /* Full-width content - override Starlight's default 45rem/67.5rem */ --sl-content-width: 65rem; - /* Primary accent colors - purple to match Docusaurus */ - --sl-color-accent-low: #e0e0ff; - --sl-color-accent: #5E5ED0; - --sl-color-accent-high: #3333CC; + /* Primary accent colors - blue to match Ghost blog */ + --sl-color-accent-low: #dbeafe; + --sl-color-accent: #2563eb; + --sl-color-accent-high: #1d4ed8; /* Text colors */ --sl-color-white: #1e293b; @@ -35,13 +36,14 @@ --sl-color-black: #f8fafc; /* Font settings */ - --sl-font: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', + --sl-font: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --sl-font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace; --sl-text-base: 1rem; --sl-line-height: 1.7; /* Code highlighting */ - --sl-color-bg-inline-code: rgba(94, 94, 208, 0.1); + --sl-color-bg-inline-code: rgba(59, 130, 246, 0.08); } /* ============================================ @@ -51,35 +53,49 @@ /* Full-width content - override Starlight's default */ --sl-content-width: 65rem; - /* Primary accent colors - purple to match Docusaurus */ - --sl-color-accent-low: #2a2a5a; - --sl-color-accent: #8C8CFF; - --sl-color-accent-high: #B9B9FF; + /* Primary accent colors - blue to match Ghost blog */ + --sl-color-accent-low: rgba(59, 130, 246, 0.12); + --sl-color-accent: #3b82f6; + --sl-color-accent-high: #60a5fa; - /* Background colors */ - --sl-color-bg: #1b1b1d; - --sl-color-bg-nav: #1b1b1d; - --sl-color-bg-sidebar: #1b1b1d; - --sl-color-hairline-light: rgba(140, 140, 255, 0.1); - --sl-color-hairline: rgba(140, 140, 255, 0.15); + /* Background colors - match Ghost blog */ + --sl-color-bg: #0a0a0a; + --sl-color-bg-nav: #0a0a0a; + --sl-color-bg-sidebar: #0a0a0a; + --sl-color-hairline-light: rgba(255, 255, 255, 0.06); + --sl-color-hairline: #262626; - /* Text colors */ - --sl-color-white: #f8fafc; + /* Text colors - match Ghost blog */ + --sl-color-white: #fafafa; --sl-color-gray-1: #e2e8f0; - --sl-color-gray-2: #cbd5e1; + --sl-color-gray-2: #a1a1a1; --sl-color-gray-3: #94a3b8; - --sl-color-gray-4: #64748b; + --sl-color-gray-4: #666666; --sl-color-gray-5: #475569; - --sl-color-gray-6: #334155; - --sl-color-black: #1b1b1d; + --sl-color-gray-6: #262626; + --sl-color-black: #0a0a0a; /* Code highlighting */ - --sl-color-bg-inline-code: rgba(140, 140, 255, 0.15); + --sl-color-bg-inline-code: rgba(59, 130, 246, 0.15); } /* ============================================ TYPOGRAPHY ============================================ */ + +/* Space Grotesk for all headings - match Ghost blog */ +.sl-markdown-content h1, +.sl-markdown-content h2, +.sl-markdown-content h3, +.sl-markdown-content h4, +.sl-markdown-content h5, +.sl-markdown-content h6, +.site-title, +starlight-toc h2 { + font-family: 'Space Grotesk', 'Inter', system-ui, sans-serif; + letter-spacing: -0.02em; +} + .sl-markdown-content h1 { margin-bottom: 1.5rem; } @@ -138,14 +154,14 @@ /* Active state - thin left accent bar */ .sidebar-content a[aria-current='page'] { - background-color: rgba(94, 94, 208, 0.08); + background-color: rgba(59, 130, 246, 0.08); color: var(--sl-color-accent); border-left-color: var(--sl-color-accent); font-weight: 600; } :root[data-theme='dark'] .sidebar-content a[aria-current='page'] { - background-color: rgba(140, 140, 255, 0.1); + background-color: rgba(59, 130, 246, 0.1); color: var(--sl-color-accent-high); border-left-color: var(--sl-color-accent); } @@ -232,7 +248,8 @@ header.header .header.sl-flex { } :root[data-theme='dark'] header.header { - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + box-shadow: none; + border-bottom: 1px solid #262626; } .site-title { @@ -281,20 +298,20 @@ header.header .header.sl-flex { .card:hover { transform: translateY(-3px); border-color: var(--sl-color-accent); - box-shadow: 0 8px 24px rgba(94, 94, 208, 0.15); + box-shadow: 0 8px 24px rgba(59, 130, 246, 0.15); } :root[data-theme='dark'] .card { - background: linear-gradient(145deg, rgba(30, 41, 59, 0.6), rgba(15, 23, 42, 0.8)); - border-color: rgba(140, 140, 255, 0.2); + background: #1a1a1a; + border-color: #262626; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } :root[data-theme='dark'] .card:hover { - border-color: rgba(140, 140, 255, 0.5); + border-color: #3b82f6; box-shadow: - 0 8px 32px rgba(140, 140, 255, 0.2), - 0 0 0 1px rgba(140, 140, 255, 0.1); + 0 8px 32px rgba(59, 130, 246, 0.15), + 0 0 0 1px rgba(59, 130, 246, 0.1); } /* Starlight card grid */ @@ -313,11 +330,11 @@ header.header .header.sl-flex { } :root[data-theme='dark'] .sl-link-card { - border-color: rgba(140, 140, 255, 0.2); + border-color: #262626; } :root[data-theme='dark'] .sl-link-card:hover { - border-color: rgba(140, 140, 255, 0.5); + border-color: #3b82f6; } /* ============================================ @@ -372,21 +389,21 @@ table { } :root[data-theme='dark'] table { - border-color: rgba(140, 140, 255, 0.1); + border-color: #262626; } :root[data-theme='dark'] table th { - background-color: rgba(140, 140, 255, 0.05); + background-color: rgba(59, 130, 246, 0.05); } :root[data-theme='dark'] table tr:nth-child(2n) { - background-color: rgba(140, 140, 255, 0.02); + background-color: rgba(255, 255, 255, 0.02); } /* Blockquotes */ blockquote { border-left-color: var(--sl-color-accent); - background-color: rgba(94, 94, 208, 0.05); + background-color: rgba(59, 130, 246, 0.05); border-radius: 0 8px 8px 0; padding: 1rem 1.25rem; } @@ -423,19 +440,19 @@ blockquote { /* Note aside */ .starlight-aside--note { - background-color: rgba(94, 94, 208, 0.08); + background-color: rgba(59, 130, 246, 0.08); } .starlight-aside--note .starlight-aside__title { - color: #5C5CCC; + color: #2563eb; } :root[data-theme='dark'] .starlight-aside--note { - background-color: rgba(140, 140, 255, 0.12); + background-color: rgba(59, 130, 246, 0.12); } :root[data-theme='dark'] .starlight-aside--note .starlight-aside__title { - color: #8C8CFF; + color: #3b82f6; } /* Caution aside */ @@ -512,7 +529,7 @@ blockquote { ROADMAP STYLES ============================================ */ .roadmap-container { - --color-planned: #6366f1; + --color-planned: #3b82f6; --color-in-progress: #10b981; --color-exploring: #f59e0b; --color-bg-card: rgba(255, 255, 255, 0.03); @@ -663,8 +680,8 @@ blockquote { } .roadmap-badge.planned { - background: rgba(99, 102, 241, 0.15); - color: #6366f1; + background: rgba(59, 130, 246, 0.15); + color: #3b82f6; } .roadmap-badge.exploring { @@ -735,7 +752,7 @@ blockquote { .roadmap-future-card { padding: 1.5rem; border-radius: 12px; - background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(245, 158, 11, 0.05)); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.08), rgba(212, 168, 83, 0.05)); border: 1px solid var(--color-border); transition: transform 0.2s ease; display: flex;