Primary Hero
+Homepage & main landing pages — sky blue tint, softer overlay
+.hero#primary and .hero#secondary. Each variant resolves its own CSS variable set per theme.--hero-primary-bg-color — Fallback background colour--hero-primary-overlay — Gradient overlay tint--hero-primary-color — Text colour--hero-secondary-bg-color — Fallback background colour--hero-secondary-overlay — Gradient overlay tint--hero-secondary-color — Text colour<div class="hero" id="primary" style="background-image:url(...)">"
+
+TPL_MOKOCASSIOPEIA_CSS_VARS_BLOCK_COLORS_LABEL="Block Color System (top-a / top-b / bottom-a / bottom-b)"
+TPL_MOKOCASSIOPEIA_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand colour palette for modules in top-a, top-b, bottom-a, and bottom-b positions. Colours assigned by :nth-child() order — no classes needed.--block-color-1 / --block-text-1 — 1st module--block-color-2 / --block-text-2 — 2nd module--block-color-3 / --block-text-3 — 3rd module--block-color-4 / --block-text-4 — 4th module--block-highlight-bg / --block-highlight-text — for #block-highlight--block-cta-bg / --block-cta-text — for #block-cta--block-alert-bg / --block-alert-text — for #block-alert!important needed — specificity handles it."
+
TPL_MOKOCASSIOPEIA_CSS_VARS_HEADER_LABEL="Header Background"
TPL_MOKOCASSIOPEIA_CSS_VARS_HEADER_DESC="Controls the background of the topbar/header area.--header-background-image — CSS background-image value (default: built-in SVG pattern)--header-background-attachment — fixed or scroll--header-background-repeat — e.g. repeat, no-repeat--header-background-size — e.g. auto, cover, contain"
diff --git a/src/language/en-US/tpl_mokocassiopeia.ini b/src/language/en-US/tpl_mokocassiopeia.ini
index b28f2df..23980c3 100644
--- a/src/language/en-US/tpl_mokocassiopeia.ini
+++ b/src/language/en-US/tpl_mokocassiopeia.ini
@@ -148,6 +148,12 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_COLORS_DESC="Named colors
TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_LABEL="Hero / Banner Overlay"
TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_DESC="Applied to the .custom-hero / .banner-overlay layout. Set on :root[data-bs-theme] so light and dark values are independent.
--hero-height — Banner height (default: 70vh)
--hero-color — Base text color
--hero-bg-repeat — Background repeat (default: no-repeat)
--hero-bg-attachment — Background attachment (default: fixed)
--hero-bg-position — Background position (default: top center)
--hero-bg-size — Background size (default: cover)
--hero-border-bottom — Bottom border (default: solid var(--accent-color-secondary))
--hero-overlay-bg — Overlay tint color (light default: hsla(0,0%,0%,0.1) / dark default: hsla(0,0%,0%,0.3))
--hero-overlay-padding — Overlay inner padding (default: 1em)
--hero-overlay-text-align — Overlay text alignment (default: center)
--hero-overlay-text-color — Overlay text color"
+TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_VARIANTS_LABEL="Hero Variants (.hero#primary / .hero#secondary)"
+TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_VARIANTS_DESC="Two-variant hero system using .hero#primary and .hero#secondary. Each variant resolves its own CSS variable set per theme.
Primary variant — homepage, main landing pages (sky blue tint, softer overlay)
--hero-primary-bg-color — Fallback background color
--hero-primary-overlay — Gradient overlay tint
--hero-primary-color — Text color
Secondary variant — inner pages, events, about (navy overlay, lighter text)
--hero-secondary-bg-color — Fallback background color
--hero-secondary-overlay — Gradient overlay tint
--hero-secondary-color — Text color
HTML usage:
<div class="hero" id="primary" style="background-image:url(...)">"
+
+TPL_MOKOCASSIOPEIA_CSS_VARS_BLOCK_COLORS_LABEL="Block Color System (top-a / top-b / bottom-a / bottom-b)"
+TPL_MOKOCASSIOPEIA_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand color palette for modules in top-a, top-b, bottom-a, and bottom-b positions. Colors assigned by :nth-child() order — no classes needed.
Slot palette
--block-color-1 / --block-text-1 — 1st module
--block-color-2 / --block-text-2 — 2nd module
--block-color-3 / --block-text-3 — 3rd module
--block-color-4 / --block-text-4 — 4th module
Named overrides (add an ID to the module HTML to bypass slot color)
--block-highlight-bg / --block-highlight-text — for #block-highlight
--block-cta-bg / --block-cta-text — for #block-cta
--block-alert-bg / --block-alert-text — for #block-alert
Priority: Named ID > Slot color. No !important needed — specificity handles it."
+
TPL_MOKOCASSIOPEIA_CSS_VARS_HEADER_LABEL="Header Background"
TPL_MOKOCASSIOPEIA_CSS_VARS_HEADER_DESC="Controls the background of the topbar/header area.
--header-background-image — CSS background-image value (default: built-in SVG pattern)
--header-background-attachment — fixed or scroll
--header-background-repeat — e.g. repeat, no-repeat
--header-background-size — e.g. auto, cover, contain"
diff --git a/src/media/css/template.css b/src/media/css/template.css
index ca078f6..8247c51 100644
--- a/src/media/css/template.css
+++ b/src/media/css/template.css
@@ -13942,6 +13942,28 @@ meter {
}
}
+/* ── HERO VARIANT BASE ── */
+.hero {
+ background-size: cover;
+ background-position: center;
+ overflow: hidden;
+ border-radius: .5rem;
+}
+
+/* ── PRIMARY VARIANT ── */
+.hero#primary {
+ background-color: var(--hero-primary-bg-color);
+ background-image: var(--hero-primary-overlay);
+ color: var(--hero-primary-color);
+}
+
+/* ── SECONDARY VARIANT ── */
+.hero#secondary {
+ background-color: var(--hero-secondary-bg-color);
+ background-image: var(--hero-secondary-overlay);
+ color: var(--hero-secondary-color);
+}
+
.footer {
margin-top: 1em;
color: var(--body-color);
@@ -14640,6 +14662,64 @@ iframe {
}
}
+/* ── BLOCK COLOR SYSTEM (nth-child slot palette) ── */
+.container-top-a > .card:nth-child(1),
+.container-top-b > .card:nth-child(1),
+.container-bottom-a > .card:nth-child(1),
+.container-bottom-b > .card:nth-child(1) {
+ background-color: var(--block-color-1);
+ color: var(--block-text-1);
+}
+
+.container-top-a > .card:nth-child(2),
+.container-top-b > .card:nth-child(2),
+.container-bottom-a > .card:nth-child(2),
+.container-bottom-b > .card:nth-child(2) {
+ background-color: var(--block-color-2);
+ color: var(--block-text-2);
+}
+
+.container-top-a > .card:nth-child(3),
+.container-top-b > .card:nth-child(3),
+.container-bottom-a > .card:nth-child(3),
+.container-bottom-b > .card:nth-child(3) {
+ background-color: var(--block-color-3);
+ color: var(--block-text-3);
+}
+
+.container-top-a > .card:nth-child(4),
+.container-top-b > .card:nth-child(4),
+.container-bottom-a > .card:nth-child(4),
+.container-bottom-b > .card:nth-child(4) {
+ background-color: var(--block-color-4);
+ color: var(--block-text-4);
+}
+
+/* ── BLOCK COLOR — Named per-module overrides ── */
+.container-top-a #block-highlight,
+.container-top-b #block-highlight,
+.container-bottom-a #block-highlight,
+.container-bottom-b #block-highlight {
+ background-color: var(--block-highlight-bg);
+ color: var(--block-highlight-text);
+}
+
+.container-top-a #block-cta,
+.container-top-b #block-cta,
+.container-bottom-a #block-cta,
+.container-bottom-b #block-cta {
+ background-color: var(--block-cta-bg);
+ color: var(--block-cta-text);
+}
+
+.container-top-a #block-alert,
+.container-top-b #block-alert,
+.container-bottom-a #block-alert,
+.container-bottom-b #block-alert {
+ background-color: var(--block-alert-bg);
+ color: var(--block-alert-text);
+}
+
.container-component nav {
position: relative;
}
diff --git a/src/sync_custom_vars.php b/src/sync_custom_vars.php
new file mode 100644
index 0000000..a71672c
--- /dev/null
+++ b/src/sync_custom_vars.php
@@ -0,0 +1,406 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ *
+ * CSS Variable Sync Utility
+ *
+ * Compares a user's custom palette file against the template starter file and
+ * injects any missing CSS variable declarations. Existing user values are
+ * never overwritten — only genuinely new variables are added.
+ *
+ * Usage (CLI):
+ * php sync_custom_vars.php
+ *
+ * Usage (from Joomla script.php or plugin):
+ * require_once __DIR__ . '/sync_custom_vars.php';
+ * MokoCssVarSync::run();
+ *
+ * The script auto-detects Joomla's root by walking up from __DIR__.
+ */
+
+defined('_JEXEC') or define('MOKO_CLI', true);
+
+final class MokoCssVarSync
+{
+ /**
+ * Template name used in Joomla's media path.
+ */
+ private const TPL = 'mokocassiopeia';
+
+ /**
+ * Palette pairs: [starter template path relative to this file, user file relative to Joomla root].
+ */
+ private const PALETTES = [
+ [
+ 'starter' => 'templates/light.custom.css',
+ 'user' => 'media/templates/site/%s/css/theme/light.custom.css',
+ ],
+ [
+ 'starter' => 'templates/dark.custom.css',
+ 'user' => 'media/templates/site/%s/css/theme/dark.custom.css',
+ ],
+ ];
+
+ /**
+ * Run the sync for all palette pairs.
+ *
+ * @param string|null $joomlaRoot Absolute path to Joomla root (auto-detected if null).
+ * @return array Results keyed by file path.
+ */
+ public static function run(?string $joomlaRoot = null): array
+ {
+ $tplDir = self::resolveTemplateDir();
+ $root = $joomlaRoot ?? self::detectJoomlaRoot();
+
+ $results = [];
+
+ foreach (self::PALETTES as $pair) {
+ $starterPath = $tplDir . '/' . $pair['starter'];
+ $userPath = $root . '/' . sprintf($pair['user'], self::TPL);
+
+ if (!is_file($starterPath)) {
+ self::log("SKIP starter not found: {$starterPath}");
+ continue;
+ }
+
+ if (!is_file($userPath)) {
+ self::log("SKIP user file not found (custom palette not deployed): {$userPath}");
+ continue;
+ }
+
+ $result = self::syncFile($starterPath, $userPath);
+ $results[$userPath] = $result;
+
+ $addedCount = count($result['added']);
+ if ($addedCount > 0) {
+ self::log("ADDED {$addedCount} variable(s) to {$userPath}");
+ foreach ($result['added'] as $var) {
+ self::log(" + {$var}");
+ }
+ } else {
+ self::log("OK {$userPath} — all variables present");
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Compare a starter file against a user file and inject missing variables.
+ *
+ * @param string $starterPath Absolute path to the starter template CSS.
+ * @param string $userPath Absolute path to the user's custom CSS.
+ * @return array{added: string[], skipped: string[]}
+ */
+ private static function syncFile(string $starterPath, string $userPath): array
+ {
+ $starterVars = self::extractVarsWithContext($starterPath);
+ $userVars = self::extractVarNames($userPath);
+
+ $missing = [];
+ foreach ($starterVars as $name => $declaration) {
+ if (!isset($userVars[$name])) {
+ $missing[$name] = $declaration;
+ }
+ }
+
+ if (empty($missing)) {
+ return ['added' => [], 'skipped' => []];
+ }
+
+ // Group missing variables by their section comment header.
+ $sections = self::groupBySection($missing, $starterPath);
+
+ // Build the injection block.
+ $injection = self::buildInjectionBlock($sections);
+
+ // Insert before the closing } of the :root rule.
+ $userCss = file_get_contents($userPath);
+ $userCss = self::injectBeforeRootClose($userCss, $injection);
+
+ // Write back (atomic: write to .tmp then rename).
+ $tmpPath = $userPath . '.tmp';
+ file_put_contents($tmpPath, $userCss);
+ rename($tmpPath, $userPath);
+
+ return ['added' => array_keys($missing), 'skipped' => []];
+ }
+
+ /**
+ * Extract CSS custom property declarations with their full text (name: value).
+ * Only extracts from the first :root block.
+ *
+ * @return array Variable name => full declaration line.
+ */
+ private static function extractVarsWithContext(string $filePath): array
+ {
+ $css = file_get_contents($filePath);
+ $vars = [];
+
+ // Match --variable-name: value (possibly spanning multiple lines until ;)
+ if (preg_match_all('/^\s*(--[\w-]+)\s*:\s*([^;]+);/m', $css, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $m) {
+ $name = trim($m[1]);
+ $value = trim($m[2]);
+ $vars[$name] = "{$name}: {$value};";
+ }
+ }
+
+ return $vars;
+ }
+
+ /**
+ * Extract just the variable names present in a CSS file.
+ *
+ * @return array
+ */
+ private static function extractVarNames(string $filePath): array
+ {
+ $css = file_get_contents($filePath);
+ $vars = [];
+
+ if (preg_match_all('/^\s*(--[\w-]+)\s*:/m', $css, $matches)) {
+ foreach ($matches[1] as $name) {
+ $vars[trim($name)] = true;
+ }
+ }
+
+ return $vars;
+ }
+
+ /**
+ * Group missing variables by the section comment they appear under in the starter file.
+ *
+ * @param array $missing Variable name => declaration.
+ * @param string $starterPath Path to starter file.
+ * @return array Section header => list of declarations.
+ */
+ private static function groupBySection(array $missing, string $starterPath): array
+ {
+ $lines = file($starterPath, FILE_IGNORE_NEW_LINES);
+ $section = 'Uncategorised';
+ $map = []; // variable name => section
+
+ foreach ($lines as $line) {
+ // Detect section comment headers like /* ===== HERO VARIANTS ===== */
+ if (preg_match('/\/\*\s*=+\s*(.+?)\s*=+\s*\*\//', $line, $m)) {
+ $section = trim($m[1]);
+ }
+ // Detect variable declaration
+ if (preg_match('/^\s*(--[\w-]+)\s*:/', $line, $m)) {
+ $name = trim($m[1]);
+ if (isset($missing[$name])) {
+ $map[$name] = $section;
+ }
+ }
+ }
+
+ // Group by section
+ $sections = [];
+ foreach ($missing as $name => $declaration) {
+ $sec = $map[$name] ?? 'Uncategorised';
+ $sections[$sec][] = $declaration;
+ }
+
+ return $sections;
+ }
+
+ /**
+ * Build a CSS block from grouped sections ready for injection.
+ */
+ private static function buildInjectionBlock(array $sections): string
+ {
+ $lines = [];
+ $lines[] = '';
+ $lines[] = '/* ===== VARIABLES ADDED BY SYNC (' . date('Y-m-d') . ') ===== */';
+
+ foreach ($sections as $sectionName => $declarations) {
+ $lines[] = '';
+ $lines[] = "/* -- {$sectionName} -- */";
+ foreach ($declarations as $decl) {
+ $lines[] = $decl;
+ }
+ }
+
+ $lines[] = '';
+
+ return implode("\n", $lines);
+ }
+
+ /**
+ * Inject a block of CSS just before the closing } of the :root[data-bs-theme] rule.
+ */
+ private static function injectBeforeRootClose(string $css, string $block): string
+ {
+ // Find the :root block's closing brace. The :root rule is the first major
+ // rule in the file; its closing } is on its own line.
+ // Strategy: find the LAST } that is preceded only by CSS variable content.
+ // More robustly: find the first } that appears on its own line (possibly
+ // with whitespace), which closes the :root block.
+
+ // Walk backwards from each } to see if it's inside the :root block.
+ // Simple approach: the :root closing } is the first bare } on its own line.
+ $pos = self::findRootClosingBrace($css);
+
+ if ($pos === false) {
+ // Fallback: append before last }
+ $pos = strrpos($css, '}');
+ }
+
+ if ($pos === false) {
+ // Last resort: append to end
+ return $css . $block;
+ }
+
+ return substr($css, 0, $pos) . $block . substr($css, $pos);
+ }
+
+ /**
+ * Find the byte position of the closing } for the :root rule.
+ */
+ private static function findRootClosingBrace(string $css): int|false
+ {
+ // Find where :root starts
+ $rootStart = preg_match('/:root\b/', $css, $m, PREG_OFFSET_CAPTURE);
+ if (!$rootStart) {
+ return false;
+ }
+
+ $offset = $m[0][1];
+ $depth = 0;
+ $len = strlen($css);
+
+ for ($i = $offset; $i < $len; $i++) {
+ if ($css[$i] === '{') {
+ $depth++;
+ } elseif ($css[$i] === '}') {
+ $depth--;
+ if ($depth === 0) {
+ return $i;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Resolve the template source directory (where this file lives).
+ */
+ private static function resolveTemplateDir(): string
+ {
+ return dirname(__FILE__);
+ }
+
+ /**
+ * Auto-detect Joomla root by walking up from template dir looking for
+ * configuration.php or the media/templates directory structure.
+ */
+ private static function detectJoomlaRoot(): string
+ {
+ $dir = dirname(__FILE__);
+
+ // Walk up max 10 levels
+ for ($i = 0; $i < 10; $i++) {
+ if (is_file($dir . '/configuration.php')) {
+ return $dir;
+ }
+ // Also check for the media/templates structure (works in dev too)
+ if (is_dir($dir . '/media/templates')) {
+ return $dir;
+ }
+ $parent = dirname($dir);
+ if ($parent === $dir) {
+ break;
+ }
+ $dir = $parent;
+ }
+
+ // Fallback for dev: if JPATH_ROOT is defined, use it
+ if (defined('JPATH_ROOT')) {
+ return JPATH_ROOT;
+ }
+
+ self::log('WARNING: Could not auto-detect Joomla root. Pass it explicitly.');
+ return dirname(__FILE__);
+ }
+
+ /**
+ * Log a message (CLI: stdout, web: Joomla enqueueMessage if available).
+ */
+ private static function log(string $message): void
+ {
+ if (defined('MOKO_CLI') || PHP_SAPI === 'cli') {
+ echo $message . PHP_EOL;
+ }
+ }
+
+ /**
+ * Dry-run mode: report what would be added without writing.
+ *
+ * @return array File path => list of missing variable names.
+ */
+ public static function dryRun(?string $joomlaRoot = null): array
+ {
+ $tplDir = self::resolveTemplateDir();
+ $root = $joomlaRoot ?? self::detectJoomlaRoot();
+ $report = [];
+
+ foreach (self::PALETTES as $pair) {
+ $starterPath = $tplDir . '/' . $pair['starter'];
+ $userPath = $root . '/' . sprintf($pair['user'], self::TPL);
+
+ if (!is_file($starterPath) || !is_file($userPath)) {
+ continue;
+ }
+
+ $starterVars = self::extractVarsWithContext($starterPath);
+ $userVars = self::extractVarNames($userPath);
+
+ $missing = [];
+ foreach ($starterVars as $name => $declaration) {
+ if (!isset($userVars[$name])) {
+ $missing[] = $name;
+ }
+ }
+
+ if (!empty($missing)) {
+ $report[$userPath] = $missing;
+ }
+ }
+
+ return $report;
+ }
+}
+
+// CLI entry point
+if (PHP_SAPI === 'cli' && realpath($argv[0] ?? '') === realpath(__FILE__)) {
+ $dryRun = in_array('--dry-run', $argv, true);
+
+ echo "MokoCassiopeia CSS Variable Sync\n";
+ echo str_repeat('─', 40) . "\n\n";
+
+ if ($dryRun) {
+ echo "DRY RUN — no files will be modified\n\n";
+ $report = MokoCssVarSync::dryRun();
+ if (empty($report)) {
+ echo "All custom palettes are up to date.\n";
+ } else {
+ foreach ($report as $file => $vars) {
+ echo "MISSING in {$file}:\n";
+ foreach ($vars as $var) {
+ echo " - {$var}\n";
+ }
+ echo "\n";
+ }
+ }
+ } else {
+ MokoCssVarSync::run();
+ }
+
+ echo "\nDone.\n";
+}
diff --git a/src/templateDetails.xml b/src/templateDetails.xml
index 3301e86..43ed88b 100644
--- a/src/templateDetails.xml
+++ b/src/templateDetails.xml
@@ -264,6 +264,8 @@
+
+
diff --git a/src/templates/dark.custom.css b/src/templates/dark.custom.css
index a5b2781..fea9660 100644
--- a/src/templates/dark.custom.css
+++ b/src/templates/dark.custom.css
@@ -512,6 +512,40 @@ color-scheme: dark;
--vm-vendor-menu-link-active: var(--primary);
--vm-vendor-menu-hover-bg: var(--tertiary-bg);
+/* ===== HERO VARIANTS ===== */
+/* Primary — deep navy, dark overlay */
+--hero-primary-bg-color: #0d1e3a;
+--hero-primary-overlay: linear-gradient(rgba(13, 30, 58, .65), rgba(13, 30, 58, .65));
+--hero-primary-color: #f1f5f9;
+
+/* Secondary — darker navy, heavier overlay */
+--hero-secondary-bg-color: #080f1e;
+--hero-secondary-overlay: linear-gradient(rgba(8, 15, 30, .80), rgba(8, 15, 30, .80));
+--hero-secondary-color: #f1f5f9;
+
+/* ===== BLOCK COLORS (top-a / top-b / bottom-a / bottom-b) ===== */
+--block-color-1: var(--secondary-bg);
+--block-text-1: var(--body-color);
+
+--block-color-2: var(--accent-color-primary);
+--block-text-2: #fff;
+
+--block-color-3: rgba(238, 194, 52, .15);
+--block-text-3: var(--body-color);
+
+--block-color-4: rgba(74, 166, 100, .15);
+--block-text-4: var(--body-color);
+
+/* ===== BLOCK COLOR OVERRIDES ===== */
+--block-highlight-bg: var(--accent-color-primary);
+--block-highlight-text: #fff;
+
+--block-cta-bg: var(--color-primary);
+--block-cta-text: #f1f5f9;
+
+--block-alert-bg: var(--danger, #c23a31);
+--block-alert-text: #fff;
+
/* ===== GABLE ===== */
--gab-blue: #4d9fff;
--gab-green: #5cb85c;
diff --git a/src/templates/light.custom.css b/src/templates/light.custom.css
index 5fe651c..378830d 100644
--- a/src/templates/light.custom.css
+++ b/src/templates/light.custom.css
@@ -511,6 +511,40 @@ color-scheme: light;
--vm-vendor-menu-link-active: var(--primary);
--vm-vendor-menu-hover-bg: var(--secondary-bg);
+/* ===== HERO VARIANTS ===== */
+/* Primary — sky blue, light overlay */
+--hero-primary-bg-color: var(--color-primary);
+--hero-primary-overlay: linear-gradient(rgba(163, 205, 226, .45), rgba(163, 205, 226, .45));
+--hero-primary-color: var(--color-primary);
+
+/* Secondary — navy, stronger overlay */
+--hero-secondary-bg-color: var(--color-primary);
+--hero-secondary-overlay: linear-gradient(rgba(17, 40, 85, .75), rgba(17, 40, 85, .75));
+--hero-secondary-color: #f1f5f9;
+
+/* ===== BLOCK COLORS (top-a / top-b / bottom-a / bottom-b) ===== */
+--block-color-1: var(--color-primary);
+--block-text-1: var(--body-color);
+
+--block-color-2: var(--accent-color-primary);
+--block-text-2: #fff;
+
+--block-color-3: var(--warning, #eec234);
+--block-text-3: var(--body-color);
+
+--block-color-4: var(--success-bg-subtle, #eef7f0);
+--block-text-4: var(--body-color);
+
+/* ===== BLOCK COLOR OVERRIDES ===== */
+--block-highlight-bg: var(--accent-color-primary);
+--block-highlight-text: #fff;
+
+--block-cta-bg: var(--color-primary);
+--block-cta-text: #fff;
+
+--block-alert-bg: var(--danger, #a51f18);
+--block-alert-text: #fff;
+
/* ===== GABLE ===== */
--gab-blue: #0066cc;
--gab-green: #28a745;
diff --git a/src/templates/theme-test.html b/src/templates/theme-test.html
new file mode 100644
index 0000000..04a248e
--- /dev/null
+++ b/src/templates/theme-test.html
@@ -0,0 +1,611 @@
+
+
+
+
+
+
+MokoCassiopeia — Theme Test Sheet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MokoCassiopeia Theme Test Sheet
+Visual reference for CSS variables, Bootstrap components, hero variants, and block color system. Toggle light/dark mode with the button in the top-right corner.
+
+
+
+
+1. Brand & Theme Colors
+
+
+
+ --color-primary
+
+
+
+ --accent-color-primary
+
+
+
+ --accent-color-secondary
+
+
+
+ --body-bg
+
+
+
+ --body-color
+
+
+
+ --secondary-bg
+
+
+
+ --tertiary-bg
+
+
+
+ --border-color
+
+
+
+
+2. Bootstrap Color Palette
+
+
+
+ --primary
+
+
+
+ --secondary
+
+
+
+ --success
+
+
+
+ --info
+
+
+
+ --warning
+
+
+
+ --danger
+
+
+
+ --light
+
+
+
+ --dark
+
+
+
+
+3. Gray Scale
+
+
+
+ --gray-100
+
+
+
+ --gray-200
+
+
+
+ --gray-300
+
+
+
+ --gray-400
+
+
+
+ --gray-500
+
+
+
+ --gray-600
+
+
+
+ --gray-700
+
+
+
+ --gray-800
+
+
+
+ --gray-900
+
+
+
+
+4. Standard Colors
+
+
+
+ --blue
+
+
+
+ --indigo
+
+
+
+ --purple
+
+
+
+ --pink
+
+
+
+ --red
+
+
+
+ --orange
+
+
+
+ --yellow
+
+
+
+ --green
+
+
+
+ --teal
+
+
+
+ --cyan
+
+
+
+
+5. Typography
+
+ Heading 1 h1
+ Heading 2 h2
+ Heading 3 h3
+ Heading 4 h4
+ Heading 5 h5
+ Heading 6 h6
+
+This is regular body text using --body-color on --body-bg. Font family: --body-font-family. Size: --body-font-size (1rem).
+Bold text. Italic text. This is a link. Inline code. Highlighted text.
+This is lead text styled with --muted-color.
+
+
+6. Link Colors
+
+ Variable Preview
+ --link-colorSample link
+ --link-hover-colorHover state
+ --color-linkcolor-link value
+ --color-hovercolor-hover value
+
+
+
+7. Buttons
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+8. Cards
+
+
+
+ Card Header
+
+ Card Title
+ Card body using --card-bg, --card-color, and --card-border-color.
+
+
+
+
+
+
+ Simple Card
+ No header, just body content. Uses the same card variables.
+
+
+
+
+
+9. Form Elements
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+10. Alerts
+
+ Primary alert. Uses --primary-bg-subtle and --primary-text-emphasis.
+
+
+ Success alert. Uses --success-bg-subtle and --success-text-emphasis.
+
+
+ Warning alert. Uses --warning-bg-subtle and --warning-text-emphasis.
+
+
+ Danger alert. Uses --danger-bg-subtle and --danger-text-emphasis.
+
+
+ Info alert. Uses --info-bg-subtle and --info-text-emphasis.
+
+
+
+11. Borders & Shadows
+
+
+ Default border: --border-width / --border-color / --border-radius
+
+
+ --box-shadow-sm
+
+
+ --box-shadow
+
+
+ --box-shadow-lg
+
+
+
+
+12. Navigation Colors
+
+
+
+ --nav-bg-color
+
+
+
+ --nav-text-color
+
+
+
+ --mainmenu-nav-link-color
+
+
+
+
+13. Container Background Variables
+
+ Container BG Color BG Image Border
+ below-topbar --container-below-topbar-bg-color--container-below-topbar-bg-image--container-below-topbar-border
+ top-a --container-top-a-bg-color--container-top-a-bg-image--container-top-a-border
+ top-b --container-top-b-bg-color--container-top-b-bg-image--container-top-b-border
+ bottom-a --container-bottom-a-bg-color--container-bottom-a-bg-image--container-bottom-a-border
+ bottom-b --container-bottom-b-bg-color--container-bottom-b-bg-image--container-bottom-b-border
+ sidebar --container-sidebar-bg-color--container-sidebar-bg-image--container-sidebar-border
+
+
+
+14. Hero Variants NEW
+The .hero#primary and .hero#secondary variants use CSS variables for background color, overlay gradient, and text color. Each adapts automatically with the active theme.
+
+Primary Variant — .hero#primary
+
+
+ Primary Hero
+ Homepage & main landing pages — sky blue tint, softer overlay
+
+
+
+Secondary Variant — .hero#secondary
+
+
+ Secondary Hero
+ Inner pages, events, about — navy overlay, lighter text
+
+
+
+Hero Variable Reference
+
+ Variable Variant Purpose
+ --hero-primary-bg-colorPrimary Fallback background color
+ --hero-primary-overlayPrimary Gradient overlay tint
+ --hero-primary-colorPrimary Text color
+ --hero-secondary-bg-colorSecondary Fallback background color
+ --hero-secondary-overlaySecondary Gradient overlay tint
+ --hero-secondary-colorSecondary Text color
+
+
+
+15. Block Color System NEW
+Modules in top-a, top-b, bottom-a, and bottom-b positions automatically receive brand colors based on their order. No classes needed — :nth-child() handles assignment.
+
+Slot Palette Preview
+
+
+ Slot 1
+ --block-color-1
+
+
+ Slot 2
+ --block-color-2
+
+
+ Slot 3
+ --block-color-3
+
+
+ Slot 4
+ --block-color-4
+
+
+
+Named Override Preview
+
+
+ #block-highlight
+ --block-highlight-bg
+
+
+ #block-cta
+ --block-cta-bg
+
+
+ #block-alert
+ --block-alert-bg
+
+
+
+Block Variable Reference
+
+ Variable Purpose
+ --block-color-1 / --block-text-11st module in position (automatic)
+ --block-color-2 / --block-text-22nd module in position (automatic)
+ --block-color-3 / --block-text-33rd module in position (automatic)
+ --block-color-4 / --block-text-44th module in position (automatic)
+ --block-highlight-bg / --block-highlight-textNamed override for #block-highlight
+ --block-cta-bg / --block-cta-textNamed override for #block-cta
+ --block-alert-bg / --block-alert-textNamed override for #block-alert
+
+
+Override Priority
+
+ Priority Method How Applied
+ 1 (highest) Named module ID (#block-highlight) ID in module HTML + named variable
+ 2 (default) Slot color (--block-color-N) Automatic by :nth-child() order
+
+
+
+16. VirtueMart Surface Colors
+
+
+
+ --vm-surface
+
+
+
+ --vm-surface-2
+
+
+
+ --vm-price-color
+
+
+
+
+17. Gable Colors
+
+
+
+ --gab-blue
+
+
+
+ --gab-green
+
+
+
+ --gab-red
+
+
+
+ --gab-orange
+
+
+
+
+18. Code & Preformatted Text
+Inline code: var(--color-primary)
+/* Example: overriding block slot 1 in colors_custom.css */
+--block-color-1: var(--accent-color-primary);
+--block-text-1: #fff;
+
+/* Hero variant usage in module HTML */
+<div class="hero" id="primary"
+ style="background-image:url('/images/hero/main.jpg')">
+ <div class="col-12 py-5 px-4 text-center">
+ ...content...
+ </div>
+</div>
+
+
+19. Opacity Scale
+
+ 5%
+ 10%
+ 15%
+ 25%
+ 50%
+ 75%
+ 100%
+
+
+
+
+ MokoCassiopeia Theme Test Sheet — v03.09.02 — © 2026 Moko Consulting
+
+
+
+
+
+
+