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 }; 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;