feat: hero variants, block colors, theme preview tab, CSS var sync on upgrade

- Add .hero#primary / .hero#secondary CSS variant system to template.css
- Add :nth-child() block color palette for top-a/b, bottom-a/b positions
- Add named per-module overrides (#block-highlight, #block-cta, #block-alert)
- Add all hero + block CSS variables to light/dark custom palette templates
- Add theme-test.html with full visual CSS variable reference + color test
- Add "Theme Preview" tab to admin config with embedded iframe of test sheet
- Add script.php install/update script (Joomla 5/6 compatible)
- Add sync_custom_vars.php — detects missing vars in user palettes on upgrade
- Add en-GB and en-US language strings for new admin config fields
- Update CSS_VARIABLES.md and CHANGELOG.md
- Bump version to 03.09.02

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:44:46 -05:00
parent 4237b303dc
commit b25a8f58d8
10 changed files with 1440 additions and 1 deletions

View File

@@ -19,6 +19,34 @@ All notable changes to the MokoCassiopeia Joomla template are documented in this
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [03.09.02] - 2026-03-26
### Added - Hero Variant System & Block Color System
#### Hero Variants
- **`.hero#primary`** and **`.hero#secondary`** CSS variant system for visually distinct hero treatments
- Shared `.hero` base class with `background-size: cover`, `border-radius: .5rem`, and `overflow: hidden`
- Six new CSS variables (`--hero-primary-bg-color`, `--hero-primary-overlay`, `--hero-primary-color`, and secondary equivalents)
- Light and dark mode defaults in custom palette templates
#### Block Color System
- Automatic `:nth-child()` slot palette for `top-a`, `top-b`, `bottom-a`, `bottom-b` module positions
- Four color slots (`--block-color-1` through `--block-color-4`) with matching text variables
- Named per-module overrides: `#block-highlight`, `#block-cta`, `#block-alert`
- ID specificity wins over `:nth-child()` — no `!important` needed
#### Files Modified
- `src/media/css/template.css` — hero variant rules, block color `:nth-child()` rules, named override rules
- `src/templates/light.custom.css` — hero and block color variables (light mode)
- `src/templates/dark.custom.css` — hero and block color variables (dark mode)
- `docs/CSS_VARIABLES.md` — full variable reference for both systems
- `CHANGELOG.md` — this entry
#### Files Added
- `src/templates/theme-test.html` — Bootstrap-style test page showing all CSS variables and new features
---
## [03.08.03] - 2026-02-27
### Added - Main Menu Collapsible Dropdown Override

View File

@@ -38,6 +38,8 @@ This document provides a complete reference of all CSS variables available in th
- [Responsive Tokens & Breakpoints](#responsive-tokens--breakpoints)
- [VirtueMart Variables](#virtuemart-variables)
- [Gable Variables](#gable-variables)
- [Hero Variant Variables](#hero-variant-variables)
- [Block Color Variables](#block-color-variables)
---
@@ -1357,6 +1359,81 @@ These ensure optimal readability for links within alert boxes.
---
## Hero Variant Variables
### `--hero-primary-bg-color`
- **Description**: Fallback background color for the primary hero variant
- **Light Mode Default**: `var(--color-primary)`
- **Dark Mode Default**: `#0d1e3a`
- **Usage**: `.hero#primary` background when no image loads
### `--hero-primary-overlay`
- **Description**: Gradient overlay tint for primary hero
- **Light Mode Default**: `linear-gradient(rgba(163, 205, 226, .45), rgba(163, 205, 226, .45))`
- **Dark Mode Default**: `linear-gradient(rgba(13, 30, 58, .65), rgba(13, 30, 58, .65))`
- **Usage**: Semi-transparent color wash over hero background image
### `--hero-primary-color`
- **Description**: Text color for primary hero content
- **Light Mode Default**: `var(--color-primary)`
- **Dark Mode Default**: `#f1f5f9`
- **Usage**: Headings and body text inside `.hero#primary`
### `--hero-secondary-bg-color`
- **Description**: Fallback background color for the secondary hero variant
- **Light Mode Default**: `var(--color-primary)`
- **Dark Mode Default**: `#080f1e`
- **Usage**: `.hero#secondary` background when no image loads
### `--hero-secondary-overlay`
- **Description**: Gradient overlay tint for secondary hero
- **Light Mode Default**: `linear-gradient(rgba(17, 40, 85, .75), rgba(17, 40, 85, .75))`
- **Dark Mode Default**: `linear-gradient(rgba(8, 15, 30, .80), rgba(8, 15, 30, .80))`
- **Usage**: Stronger overlay for inner-page heroes
### `--hero-secondary-color`
- **Description**: Text color for secondary hero content
- **Light Mode Default**: `#f1f5f9`
- **Dark Mode Default**: `#f1f5f9`
- **Usage**: Headings and body text inside `.hero#secondary`
---
## Block Color Variables
### Slot Palette (automatic by position order)
| Variable | Purpose | Light Default | Dark Default |
|---|---|---|---|
| `--block-color-1` | Background for 1st module | `var(--color-primary)` | `var(--secondary-bg)` |
| `--block-text-1` | Text for 1st module | `var(--body-color)` | `var(--body-color)` |
| `--block-color-2` | Background for 2nd module | `var(--accent-color-primary)` | `var(--accent-color-primary)` |
| `--block-text-2` | Text for 2nd module | `#fff` | `#fff` |
| `--block-color-3` | Background for 3rd module | `var(--warning, #eec234)` | `rgba(238, 194, 52, .15)` |
| `--block-text-3` | Text for 3rd module | `var(--body-color)` | `var(--body-color)` |
| `--block-color-4` | Background for 4th module | `var(--success-bg-subtle, #eef7f0)` | `rgba(74, 166, 100, .15)` |
| `--block-text-4` | Text for 4th module | `var(--body-color)` | `var(--body-color)` |
### Named Per-Module Overrides
| Variable | Purpose |
|---|---|
| `--block-highlight-bg` | Background for `#block-highlight` module |
| `--block-highlight-text` | Text color for `#block-highlight` module |
| `--block-cta-bg` | Background for `#block-cta` module |
| `--block-cta-text` | Text color for `#block-cta` module |
| `--block-alert-bg` | Background for `#block-alert` module |
| `--block-alert-text` | Text color for `#block-alert` module |
### Override Priority
| Priority | Method | How applied |
|---|---|---|
| 1 (highest) | Named module ID (`#block-highlight`) | ID in module HTML, named variable in palette |
| 2 | Slot color (`--block-color-1` etc.) | Automatic by `:nth-child()` order |
---
## Metadata
* Document: docs/CSS_VARIABLES.md
@@ -1372,5 +1449,6 @@ These ensure optimal readability for links within alert boxes.
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------- | --------------- |
| 2026-03-26 | Added hero variant and block color variable docs | Claude |
| 2026-02-07 | Added missing CSS variable documentation | GitHub Copilot |
| 2026-01-30 | Initial CSS variables reference documentation created | GitHub Copilot |

View File

@@ -238,6 +238,11 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC="<strong>Surfaces &amp; text</strong><br><co
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable"
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Colour tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>"
; ===== Theme Preview tab =====
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colours, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"

View File

@@ -148,6 +148,12 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_COLORS_DESC="<strong>Named colors</strong><br><code>
TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_LABEL="Hero / Banner Overlay"
TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_DESC="Applied to the <code>.custom-hero</code> / <code>.banner-overlay</code> layout. Set on <code>:root[data-bs-theme]</code> so light and dark values are independent.<br><code>--hero-height</code> — Banner height (default: <code>70vh</code>)<br><code>--hero-color</code> — Base text color<br><code>--hero-bg-repeat</code> — Background repeat (default: <code>no-repeat</code>)<br><code>--hero-bg-attachment</code> — Background attachment (default: <code>fixed</code>)<br><code>--hero-bg-position</code> — Background position (default: <code>top center</code>)<br><code>--hero-bg-size</code> — Background size (default: <code>cover</code>)<br><code>--hero-border-bottom</code> — Bottom border (default: <code>solid var(--accent-color-secondary)</code>)<br><code>--hero-overlay-bg</code> — Overlay tint color (light default: <code>hsla(0,0%,0%,0.1)</code> / dark default: <code>hsla(0,0%,0%,0.3)</code>)<br><code>--hero-overlay-padding</code> — Overlay inner padding (default: <code>1em</code>)<br><code>--hero-overlay-text-align</code> — Overlay text alignment (default: <code>center</code>)<br><code>--hero-overlay-text-color</code> — 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 <code>.hero#primary</code> and <code>.hero#secondary</code>. Each variant resolves its own CSS variable set per theme.<br><br><strong>Primary variant</strong> — homepage, main landing pages (sky blue tint, softer overlay)<br><code>--hero-primary-bg-color</code> — Fallback background color<br><code>--hero-primary-overlay</code> — Gradient overlay tint<br><code>--hero-primary-color</code> — Text color<br><br><strong>Secondary variant</strong> — inner pages, events, about (navy overlay, lighter text)<br><code>--hero-secondary-bg-color</code> — Fallback background color<br><code>--hero-secondary-overlay</code> — Gradient overlay tint<br><code>--hero-secondary-color</code> — Text color<br><br><strong>HTML usage:</strong><br><code>&lt;div class=&quot;hero&quot; id=&quot;primary&quot; style=&quot;background-image:url(...)&quot;&gt;</code>"
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 <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions. Colors assigned by <code>:nth-child()</code> order — no classes needed.<br><br><strong>Slot palette</strong><br><code>--block-color-1</code> / <code>--block-text-1</code> — 1st module<br><code>--block-color-2</code> / <code>--block-text-2</code> — 2nd module<br><code>--block-color-3</code> / <code>--block-text-3</code> — 3rd module<br><code>--block-color-4</code> / <code>--block-text-4</code> — 4th module<br><br><strong>Named overrides</strong> (add an ID to the module HTML to bypass slot color)<br><code>--block-highlight-bg</code> / <code>--block-highlight-text</code> — for <code>#block-highlight</code><br><code>--block-cta-bg</code> / <code>--block-cta-text</code> — for <code>#block-cta</code><br><code>--block-alert-bg</code> / <code>--block-alert-text</code> — for <code>#block-alert</code><br><br><strong>Priority:</strong> Named ID &gt; Slot color. No <code>!important</code> 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.<br><code>--header-background-image</code> — CSS <code>background-image</code> value (default: built-in SVG pattern)<br><code>--header-background-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--header-background-repeat</code> — e.g. <code>repeat</code>, <code>no-repeat</code><br><code>--header-background-size</code> — e.g. <code>auto</code>, <code>cover</code>, <code>contain</code>"
@@ -232,6 +238,11 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC="<strong>Surfaces &amp; text</strong><br><co
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable"
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Color tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>"
; ===== Theme Preview tab =====
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colors, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"

221
src/script.php Normal file
View File

@@ -0,0 +1,221 @@
<?php
/**
* @package Joomla.Site
* @subpackage Templates.MokoCassiopeia
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Template install/update/uninstall script.
*
* Joomla calls the methods in this class automatically during template
* install, update, and uninstall via the <scriptfile> element in
* templateDetails.xml.
*
* Joomla 5 and 6 compatible — uses the InstallerScriptInterface when
* available, falls back to the legacy class-based approach otherwise.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Log\Log;
class Tpl_MokocassiopeiaInstallerScript
{
/**
* Minimum PHP version required by this template.
*/
private const MIN_PHP = '8.1.0';
/**
* Minimum Joomla version required by this template.
*/
private const MIN_JOOMLA = '4.4.0';
/**
* Called before install/update/uninstall.
*
* @param string $type install, update, discover_install, or uninstall.
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool True to proceed, false to abort.
*/
public function preflight(string $type, InstallerAdapter $parent): bool
{
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
Factory::getApplication()->enqueueMessage(
sprintf(
'MokoCassiopeia requires PHP %s or later. You are running PHP %s.',
self::MIN_PHP,
PHP_VERSION
),
'error'
);
return false;
}
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
Factory::getApplication()->enqueueMessage(
sprintf(
'MokoCassiopeia requires Joomla %s or later. You are running Joomla %s.',
self::MIN_JOOMLA,
JVERSION
),
'error'
);
return false;
}
return true;
}
/**
* Called after a successful install.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function install(InstallerAdapter $parent): bool
{
$this->logMessage('MokoCassiopeia template installed.');
return true;
}
/**
* Called after a successful update.
*
* This is where the CSS variable sync runs — it detects variables that
* were added in the new version and injects them into the user's custom
* palette files without overwriting existing values.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function update(InstallerAdapter $parent): bool
{
$this->logMessage('MokoCassiopeia template updated.');
// Run CSS variable sync to inject any new variables into user's custom palettes.
$synced = $this->syncCustomVariables($parent);
if ($synced > 0) {
Factory::getApplication()->enqueueMessage(
sprintf(
'MokoCassiopeia: %d new CSS variable(s) were added to your custom palette files. '
. 'Review them in your light.custom.css and/or dark.custom.css to customise the new defaults.',
$synced
),
'notice'
);
}
return true;
}
/**
* Called after a successful uninstall.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function uninstall(InstallerAdapter $parent): bool
{
$this->logMessage('MokoCassiopeia template uninstalled.');
return true;
}
/**
* Called after install/update completes (regardless of type).
*
* @param string $type install, update, or discover_install.
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function postflight(string $type, InstallerAdapter $parent): bool
{
return true;
}
/**
* Run the CSS variable sync utility.
*
* Loads sync_custom_vars.php from the template directory and calls
* MokoCssVarSync::run() to detect and inject missing variables.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return int Number of variables added across all files.
*/
private function syncCustomVariables(InstallerAdapter $parent): int
{
$templateDir = $parent->getParent()->getPath('source');
// The sync script lives alongside this script in the template root.
$syncScript = $templateDir . '/sync_custom_vars.php';
if (!is_file($syncScript)) {
$this->logMessage('CSS variable sync script not found at: ' . $syncScript, 'warning');
return 0;
}
require_once $syncScript;
if (!class_exists('MokoCssVarSync')) {
$this->logMessage('MokoCssVarSync class not found after loading script.', 'warning');
return 0;
}
try {
$joomlaRoot = JPATH_ROOT;
$results = MokoCssVarSync::run($joomlaRoot);
$totalAdded = 0;
foreach ($results as $filePath => $result) {
$totalAdded += count($result['added']);
if (!empty($result['added'])) {
$this->logMessage(
sprintf(
'CSS sync: added %d variable(s) to %s',
count($result['added']),
basename($filePath)
)
);
}
}
return $totalAdded;
} catch (\Throwable $e) {
$this->logMessage('CSS variable sync failed: ' . $e->getMessage(), 'error');
return 0;
}
}
/**
* Log a message to Joomla's log system.
*
* @param string $message The log message.
* @param string $priority Log priority (info, warning, error).
*/
private function logMessage(string $message, string $priority = 'info'): void
{
$priorities = [
'info' => Log::INFO,
'warning' => Log::WARNING,
'error' => Log::ERROR,
];
Log::addLogger(
['text_file' => 'mokocassiopeia.log.php'],
Log::ALL,
['mokocassiopeia']
);
Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia');
}
}

406
src/sync_custom_vars.php Normal file
View File

@@ -0,0 +1,406 @@
<?php
/**
* @package Joomla.Site
* @subpackage Templates.MokoCassiopeia
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @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<string, array{added: string[], skipped: string[]}> 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<string, string> 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<string, true>
*/
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<string, string> $missing Variable name => declaration.
* @param string $starterPath Path to starter file.
* @return array<string, string[]> 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<string, string[]> 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";
}

View File

@@ -36,7 +36,8 @@
</server>
</updateservers>
<name>MokoCassiopeia</name>
<version>03.09.01</version>
<version>03.09.02</version>
<scriptfile>script.php</scriptfile>
<creationDate>2026-03-08</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -49,6 +50,8 @@
<filename>index.php</filename>
<filename>joomla.asset.json</filename>
<filename>offline.php</filename>
<filename>script.php</filename>
<filename>sync_custom_vars.php</filename>
<filename>templateDetails.xml</filename>
<folder>html</folder>
<folder>language</folder>
@@ -264,6 +267,8 @@
<field name="css_vars_alert_list" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_ALERT_LIST_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_ALERT_LIST_DESC" class="alert alert-light" />
<field name="css_vars_colors" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_COLORS_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_COLORS_DESC" class="alert alert-light" />
<field name="css_vars_hero" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_DESC" class="alert alert-light" />
<field name="css_vars_hero_variants" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_VARIANTS_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_VARIANTS_DESC" class="alert alert-info" />
<field name="css_vars_block_colors" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_BLOCK_COLORS_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_BLOCK_COLORS_DESC" class="alert alert-info" />
<field name="css_vars_header" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_HEADER_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_HEADER_DESC" class="alert alert-light" />
<field name="css_vars_containers" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_CONTAINERS_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_CONTAINERS_DESC" class="alert alert-light" />
<field name="css_vars_borders" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_BORDERS_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_BORDERS_DESC" class="alert alert-light" />
@@ -293,6 +298,12 @@
<field name="css_vars_virtuemart" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC" class="alert alert-light" />
<field name="css_vars_gable" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC" class="alert alert-light" />
</fieldset>
<!-- Theme Preview tab — embedded test sheet -->
<fieldset name="theme_preview" label="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL">
<field name="theme_preview_intro" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO" />
<field name="theme_preview_frame" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME" />
</fieldset>
</fields>
</config>
</extension>

View File

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

View File

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

View File

@@ -0,0 +1,611 @@
<!DOCTYPE html>
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokaCassiopeia.Testing
REPO: mokoconsulting-tech/MokoCassiopeia
VERSION: 03.09.02
PATH: ./src/templates/theme-test.html
BRIEF: Bootstrap-style test page for MokoCassiopeia CSS variables, hero variants, and block color system
-->
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MokoCassiopeia — Theme Test Sheet</title>
<!-- Load the template CSS -->
<link rel="stylesheet" href="../media/css/template.css">
<!-- Load the light custom palette (swap to dark.custom.css for dark mode testing) -->
<link rel="stylesheet" href="light.custom.css">
<style>
/* ── Test Page Layout ── */
body { font-family: var(--body-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif); color: var(--body-color, #22262a); background: var(--body-bg, #fff); margin: 0; padding: 0; }
.test-container { max-width: 1200px; margin: 0 auto; padding: 1rem 1.5rem; }
h1, h2, h3, h4, h5, h6 { color: var(--heading-color, inherit); }
h1 { font-size: 2.25rem; margin-bottom: .25rem; }
h2 { font-size: 1.75rem; margin-top: 2.5rem; border-bottom: 2px solid var(--border-color, #dfe3e7); padding-bottom: .5rem; }
h3 { font-size: 1.25rem; margin-top: 1.5rem; }
p.lead { font-size: 1.15rem; color: var(--muted-color, #6d757e); }
hr { border: 0; border-top: 1px solid var(--border-color, #dfe3e7); margin: 2rem 0; }
a { color: var(--link-color, #224faa); text-decoration: var(--link-decoration, underline); }
a:hover { color: var(--link-hover-color, #424077); }
code { color: var(--code-color, #e93f8e); background: var(--secondary-bg, #eaedf0); padding: .15em .4em; border-radius: .2rem; font-size: .875em; }
pre { background: var(--secondary-bg, #eaedf0); color: var(--body-color); padding: 1rem; border-radius: var(--border-radius, .25rem); overflow-x: auto; font-size: .875rem; }
/* ── Swatch Grid ── */
.swatch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: .75rem; margin: 1rem 0; }
.swatch { border-radius: var(--border-radius, .25rem); border: 1px solid var(--border-color, #dfe3e7); overflow: hidden; }
.swatch-color { height: 60px; }
.swatch-label { padding: .4rem .6rem; font-size: .75rem; background: var(--body-bg, #fff); }
.swatch-label code { font-size: .7rem; }
/* ── Variable Table ── */
.var-table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: .875rem; }
.var-table th, .var-table td { padding: .5rem .75rem; border: 1px solid var(--border-color, #dfe3e7); text-align: left; }
.var-table th { background: var(--secondary-bg, #eaedf0); font-weight: 600; }
.var-table tr:nth-child(even) td { background: var(--tertiary-bg, #f9fafb); }
/* ── Flex row helper ── */
.row { display: flex; flex-wrap: wrap; gap: 1rem; }
.col { flex: 1; min-width: 200px; }
/* ── Theme Toggle ── */
.theme-toggle { position: fixed; top: 1rem; right: 1.5rem; z-index: 1000; }
.theme-toggle button { padding: .5rem 1rem; border: 1px solid var(--border-color); border-radius: var(--border-radius); background: var(--body-bg); color: var(--body-color); cursor: pointer; font-size: .875rem; }
/* ── Block Color Demo ── */
.block-demo { display: flex; gap: .75rem; flex-wrap: wrap; margin: 1rem 0; }
.block-demo .card { flex: 1; min-width: 180px; padding: 1.25rem; border-radius: var(--border-radius, .25rem); border: 1px solid var(--border-color, #dfe3e7); }
</style>
</head>
<body>
<div class="theme-toggle">
<button onclick="toggleTheme()">Toggle Light / Dark</button>
</div>
<div class="test-container">
<!-- ══════════════════════════════════════════════
HEADER
══════════════════════════════════════════════ -->
<h1>MokoCassiopeia Theme Test Sheet</h1>
<p class="lead">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.</p>
<hr>
<!-- ══════════════════════════════════════════════
1. BRAND COLORS
══════════════════════════════════════════════ -->
<h2>1. Brand &amp; Theme Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--color-primary)"></div>
<div class="swatch-label"><code>--color-primary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--accent-color-primary)"></div>
<div class="swatch-label"><code>--accent-color-primary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--accent-color-secondary)"></div>
<div class="swatch-label"><code>--accent-color-secondary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--body-bg)"></div>
<div class="swatch-label"><code>--body-bg</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--body-color)"></div>
<div class="swatch-label"><code>--body-color</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--secondary-bg)"></div>
<div class="swatch-label"><code>--secondary-bg</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--tertiary-bg)"></div>
<div class="swatch-label"><code>--tertiary-bg</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--border-color)"></div>
<div class="swatch-label"><code>--border-color</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
2. BOOTSTRAP PALETTE
══════════════════════════════════════════════ -->
<h2>2. Bootstrap Color Palette</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--primary)"></div>
<div class="swatch-label"><code>--primary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--secondary)"></div>
<div class="swatch-label"><code>--secondary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--success)"></div>
<div class="swatch-label"><code>--success</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--info)"></div>
<div class="swatch-label"><code>--info</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--warning)"></div>
<div class="swatch-label"><code>--warning</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--danger)"></div>
<div class="swatch-label"><code>--danger</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--light)"></div>
<div class="swatch-label"><code>--light</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--dark)"></div>
<div class="swatch-label"><code>--dark</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
3. GRAY SCALE
══════════════════════════════════════════════ -->
<h2>3. Gray Scale</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-100)"></div>
<div class="swatch-label"><code>--gray-100</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-200)"></div>
<div class="swatch-label"><code>--gray-200</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-300)"></div>
<div class="swatch-label"><code>--gray-300</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-400)"></div>
<div class="swatch-label"><code>--gray-400</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-500)"></div>
<div class="swatch-label"><code>--gray-500</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-600)"></div>
<div class="swatch-label"><code>--gray-600</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-700)"></div>
<div class="swatch-label"><code>--gray-700</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-800)"></div>
<div class="swatch-label"><code>--gray-800</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-900)"></div>
<div class="swatch-label"><code>--gray-900</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
4. STANDARD COLORS
══════════════════════════════════════════════ -->
<h2>4. Standard Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--blue)"></div>
<div class="swatch-label"><code>--blue</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--indigo)"></div>
<div class="swatch-label"><code>--indigo</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--purple)"></div>
<div class="swatch-label"><code>--purple</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--pink)"></div>
<div class="swatch-label"><code>--pink</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--red)"></div>
<div class="swatch-label"><code>--red</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--orange)"></div>
<div class="swatch-label"><code>--orange</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--yellow)"></div>
<div class="swatch-label"><code>--yellow</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--green)"></div>
<div class="swatch-label"><code>--green</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--teal)"></div>
<div class="swatch-label"><code>--teal</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--cyan)"></div>
<div class="swatch-label"><code>--cyan</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
5. TYPOGRAPHY
══════════════════════════════════════════════ -->
<h2>5. Typography</h2>
<div>
<h1>Heading 1 <small style="color: var(--muted-color); font-size: .5em;">h1</small></h1>
<h2 style="border: none; padding: 0; margin-top: .5rem;">Heading 2 <small style="color: var(--muted-color); font-size: .5em;">h2</small></h2>
<h3>Heading 3 <small style="color: var(--muted-color); font-size: .6em;">h3</small></h3>
<h4>Heading 4 <small style="color: var(--muted-color); font-size: .6em;">h4</small></h4>
<h5>Heading 5 <small style="color: var(--muted-color); font-size: .6em;">h5</small></h5>
<h6>Heading 6 <small style="color: var(--muted-color); font-size: .6em;">h6</small></h6>
</div>
<p>This is regular body text using <code>--body-color</code> on <code>--body-bg</code>. Font family: <code>--body-font-family</code>. Size: <code>--body-font-size</code> (1rem).</p>
<p><strong>Bold text.</strong> <em>Italic text.</em> <a href="#">This is a link</a>. <code>Inline code</code>. <mark style="background: var(--highlight-bg); color: var(--highlight-color);">Highlighted text</mark>.</p>
<p class="lead">This is lead text styled with <code>--muted-color</code>.</p>
<!-- ══════════════════════════════════════════════
6. LINKS
══════════════════════════════════════════════ -->
<h2>6. Link Colors</h2>
<table class="var-table">
<tr><th>Variable</th><th>Preview</th></tr>
<tr><td><code>--link-color</code></td><td><a href="#" style="color: var(--link-color)">Sample link</a></td></tr>
<tr><td><code>--link-hover-color</code></td><td><span style="color: var(--link-hover-color); text-decoration: underline; cursor: pointer;">Hover state</span></td></tr>
<tr><td><code>--color-link</code></td><td><span style="color: var(--color-link)">color-link value</span></td></tr>
<tr><td><code>--color-hover</code></td><td><span style="color: var(--color-hover)">color-hover value</span></td></tr>
</table>
<!-- ══════════════════════════════════════════════
7. BUTTONS (Bootstrap-style)
══════════════════════════════════════════════ -->
<h2>7. Buttons</h2>
<div style="display: flex; flex-wrap: wrap; gap: .5rem; margin: 1rem 0;">
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-success">Success</button>
<button class="btn btn-danger">Danger</button>
<button class="btn btn-warning">Warning</button>
<button class="btn btn-info">Info</button>
<button class="btn btn-light">Light</button>
<button class="btn btn-dark">Dark</button>
</div>
<div style="display: flex; flex-wrap: wrap; gap: .5rem; margin: 1rem 0;">
<button class="btn btn-outline-primary">Outline Primary</button>
<button class="btn btn-outline-secondary">Outline Secondary</button>
<button class="btn btn-outline-success">Outline Success</button>
<button class="btn btn-outline-danger">Outline Danger</button>
</div>
<!-- ══════════════════════════════════════════════
8. CARDS
══════════════════════════════════════════════ -->
<h2>8. Cards</h2>
<div class="row">
<div class="col">
<div class="card" style="background: var(--card-bg); border: var(--card-border-width) solid var(--card-border-color); border-radius: var(--card-border-radius); padding: 0;">
<div style="padding: var(--card-cap-padding-y) var(--card-cap-padding-x); background: var(--card-cap-bg); color: var(--card-cap-color); border-bottom: 1px solid var(--card-border-color); font-weight: 600;">Card Header</div>
<div style="padding: var(--card-spacer-y) var(--card-spacer-x); color: var(--card-color);">
<h5 style="margin-top: 0;">Card Title</h5>
<p style="margin-bottom: .5rem;">Card body using <code>--card-bg</code>, <code>--card-color</code>, and <code>--card-border-color</code>.</p>
<button class="btn btn-primary" style="font-size: .875rem;">Action</button>
</div>
</div>
</div>
<div class="col">
<div class="card" style="background: var(--card-bg); border: var(--card-border-width) solid var(--card-border-color); border-radius: var(--card-border-radius); padding: var(--card-spacer-y) var(--card-spacer-x); color: var(--card-color);">
<h5 style="margin-top: 0;">Simple Card</h5>
<p>No header, just body content. Uses the same card variables.</p>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════
9. FORMS
══════════════════════════════════════════════ -->
<h2>9. Form Elements</h2>
<div style="max-width: 480px;">
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: .25rem; font-weight: 500;">Text Input</label>
<input type="text" placeholder="Placeholder text" style="width: 100%; padding: .375rem .75rem; border: 1px solid var(--input-border-color, #ced4da); border-radius: var(--border-radius); background: var(--input-bg, #fff); color: var(--input-color, #22262a); font-size: 1rem;">
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: .25rem; font-weight: 500;">Select</label>
<select style="width: 100%; padding: .375rem .75rem; border: 1px solid var(--input-border-color, #ced4da); border-radius: var(--border-radius); background: var(--input-bg, #fff); color: var(--input-color, #22262a); font-size: 1rem;">
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: .25rem; font-weight: 500;">Textarea</label>
<textarea rows="3" style="width: 100%; padding: .375rem .75rem; border: 1px solid var(--input-border-color, #ced4da); border-radius: var(--border-radius); background: var(--input-bg, #fff); color: var(--input-color, #22262a); font-size: 1rem;">Sample text content</textarea>
</div>
</div>
<!-- ══════════════════════════════════════════════
10. ALERTS (Bootstrap-style)
══════════════════════════════════════════════ -->
<h2>10. Alerts</h2>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--primary-bg-subtle); color: var(--primary-text-emphasis); border: 1px solid var(--primary-border-subtle);">
<strong>Primary alert.</strong> Uses <code>--primary-bg-subtle</code> and <code>--primary-text-emphasis</code>.
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--success-bg-subtle); color: var(--success-text-emphasis); border: 1px solid var(--success-border-subtle);">
<strong>Success alert.</strong> Uses <code>--success-bg-subtle</code> and <code>--success-text-emphasis</code>.
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--warning-bg-subtle); color: var(--warning-text-emphasis); border: 1px solid var(--warning-border-subtle);">
<strong>Warning alert.</strong> Uses <code>--warning-bg-subtle</code> and <code>--warning-text-emphasis</code>.
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--danger-bg-subtle); color: var(--danger-text-emphasis); border: 1px solid var(--danger-border-subtle);">
<strong>Danger alert.</strong> Uses <code>--danger-bg-subtle</code> and <code>--danger-text-emphasis</code>.
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--info-bg-subtle); color: var(--info-text-emphasis); border: 1px solid var(--info-border-subtle);">
<strong>Info alert.</strong> Uses <code>--info-bg-subtle</code> and <code>--info-text-emphasis</code>.
</div>
<!-- ══════════════════════════════════════════════
11. BORDERS & SHADOWS
══════════════════════════════════════════════ -->
<h2>11. Borders &amp; Shadows</h2>
<div class="row">
<div class="col" style="padding: 1.5rem; border: var(--border-width) var(--border-style) var(--border-color); border-radius: var(--border-radius); margin-bottom: 1rem;">
Default border: <code>--border-width</code> / <code>--border-color</code> / <code>--border-radius</code>
</div>
<div class="col" style="padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--box-shadow-sm); margin-bottom: 1rem;">
<code>--box-shadow-sm</code>
</div>
<div class="col" style="padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--box-shadow); margin-bottom: 1rem;">
<code>--box-shadow</code>
</div>
<div class="col" style="padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--box-shadow-lg); margin-bottom: 1rem;">
<code>--box-shadow-lg</code>
</div>
</div>
<!-- ══════════════════════════════════════════════
12. NAVIGATION COLORS
══════════════════════════════════════════════ -->
<h2>12. Navigation Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--nav-bg-color)"></div>
<div class="swatch-label"><code>--nav-bg-color</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--nav-text-color); border: 1px solid var(--border-color)"></div>
<div class="swatch-label"><code>--nav-text-color</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--mainmenu-nav-link-color); border: 1px solid var(--border-color)"></div>
<div class="swatch-label"><code>--mainmenu-nav-link-color</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
13. CONTAINER BACKGROUNDS
══════════════════════════════════════════════ -->
<h2>13. Container Background Variables</h2>
<table class="var-table">
<tr><th>Container</th><th>BG Color</th><th>BG Image</th><th>Border</th></tr>
<tr><td>below-topbar</td><td><code>--container-below-topbar-bg-color</code></td><td><code>--container-below-topbar-bg-image</code></td><td><code>--container-below-topbar-border</code></td></tr>
<tr><td>top-a</td><td><code>--container-top-a-bg-color</code></td><td><code>--container-top-a-bg-image</code></td><td><code>--container-top-a-border</code></td></tr>
<tr><td>top-b</td><td><code>--container-top-b-bg-color</code></td><td><code>--container-top-b-bg-image</code></td><td><code>--container-top-b-border</code></td></tr>
<tr><td>bottom-a</td><td><code>--container-bottom-a-bg-color</code></td><td><code>--container-bottom-a-bg-image</code></td><td><code>--container-bottom-a-border</code></td></tr>
<tr><td>bottom-b</td><td><code>--container-bottom-b-bg-color</code></td><td><code>--container-bottom-b-bg-image</code></td><td><code>--container-bottom-b-border</code></td></tr>
<tr><td>sidebar</td><td><code>--container-sidebar-bg-color</code></td><td><code>--container-sidebar-bg-image</code></td><td><code>--container-sidebar-border</code></td></tr>
</table>
<!-- ══════════════════════════════════════════════
14. HERO VARIANTS (NEW)
══════════════════════════════════════════════ -->
<h2>14. Hero Variants <span style="font-size: .65em; color: var(--success); font-weight: normal;">NEW</span></h2>
<p>The <code>.hero#primary</code> and <code>.hero#secondary</code> variants use CSS variables for background color, overlay gradient, and text color. Each adapts automatically with the active theme.</p>
<h3>Primary Variant — <code>.hero#primary</code></h3>
<div class="hero" id="primary" style="background-image: url('data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22400%22 height=%22200%22><rect fill=%22%23a3cde2%22 width=%22400%22 height=%22200%22/><text x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 font-family=%22sans-serif%22 font-size=%2216%22 fill=%22%23112855%22>Hero Background Image Area</text></svg>'); padding: 0;">
<div style="padding: 3rem 2rem; text-align: center;">
<h2 style="border: none; padding: 0; margin: 0 0 .5rem 0; font-size: 2rem;">Primary Hero</h2>
<p style="margin: 0; font-size: 1.1rem;">Homepage &amp; main landing pages — sky blue tint, softer overlay</p>
</div>
</div>
<h3>Secondary Variant — <code>.hero#secondary</code></h3>
<div class="hero" id="secondary" style="background-image: url('data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22400%22 height=%22200%22><rect fill=%22%23112855%22 width=%22400%22 height=%22200%22/><text x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 font-family=%22sans-serif%22 font-size=%2216%22 fill=%22%23f1f5f9%22>Hero Background Image Area</text></svg>'); padding: 0;">
<div style="padding: 3rem 2rem; text-align: center;">
<h2 style="border: none; padding: 0; margin: 0 0 .5rem 0; font-size: 2rem;">Secondary Hero</h2>
<p style="margin: 0; font-size: 1.1rem;">Inner pages, events, about — navy overlay, lighter text</p>
</div>
</div>
<h3>Hero Variable Reference</h3>
<table class="var-table">
<tr><th>Variable</th><th>Variant</th><th>Purpose</th></tr>
<tr><td><code>--hero-primary-bg-color</code></td><td>Primary</td><td>Fallback background color</td></tr>
<tr><td><code>--hero-primary-overlay</code></td><td>Primary</td><td>Gradient overlay tint</td></tr>
<tr><td><code>--hero-primary-color</code></td><td>Primary</td><td>Text color</td></tr>
<tr><td><code>--hero-secondary-bg-color</code></td><td>Secondary</td><td>Fallback background color</td></tr>
<tr><td><code>--hero-secondary-overlay</code></td><td>Secondary</td><td>Gradient overlay tint</td></tr>
<tr><td><code>--hero-secondary-color</code></td><td>Secondary</td><td>Text color</td></tr>
</table>
<!-- ══════════════════════════════════════════════
15. BLOCK COLOR SYSTEM (NEW)
══════════════════════════════════════════════ -->
<h2>15. Block Color System <span style="font-size: .65em; color: var(--success); font-weight: normal;">NEW</span></h2>
<p>Modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions automatically receive brand colors based on their order. No classes needed — <code>:nth-child()</code> handles assignment.</p>
<h3>Slot Palette Preview</h3>
<div class="block-demo">
<div class="card" style="background-color: var(--block-color-1); color: var(--block-text-1);">
<strong>Slot 1</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-color-1</code>
</div>
<div class="card" style="background-color: var(--block-color-2); color: var(--block-text-2);">
<strong>Slot 2</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-color-2</code>
</div>
<div class="card" style="background-color: var(--block-color-3); color: var(--block-text-3);">
<strong>Slot 3</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-color-3</code>
</div>
<div class="card" style="background-color: var(--block-color-4); color: var(--block-text-4);">
<strong>Slot 4</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-color-4</code>
</div>
</div>
<h3>Named Override Preview</h3>
<div class="block-demo">
<div class="card" style="background-color: var(--block-highlight-bg); color: var(--block-highlight-text);">
<strong>#block-highlight</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-highlight-bg</code>
</div>
<div class="card" style="background-color: var(--block-cta-bg); color: var(--block-cta-text);">
<strong>#block-cta</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-cta-bg</code>
</div>
<div class="card" style="background-color: var(--block-alert-bg); color: var(--block-alert-text);">
<strong>#block-alert</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-alert-bg</code>
</div>
</div>
<h3>Block Variable Reference</h3>
<table class="var-table">
<tr><th>Variable</th><th>Purpose</th></tr>
<tr><td><code>--block-color-1</code> / <code>--block-text-1</code></td><td>1st module in position (automatic)</td></tr>
<tr><td><code>--block-color-2</code> / <code>--block-text-2</code></td><td>2nd module in position (automatic)</td></tr>
<tr><td><code>--block-color-3</code> / <code>--block-text-3</code></td><td>3rd module in position (automatic)</td></tr>
<tr><td><code>--block-color-4</code> / <code>--block-text-4</code></td><td>4th module in position (automatic)</td></tr>
<tr><td><code>--block-highlight-bg</code> / <code>--block-highlight-text</code></td><td>Named override for <code>#block-highlight</code></td></tr>
<tr><td><code>--block-cta-bg</code> / <code>--block-cta-text</code></td><td>Named override for <code>#block-cta</code></td></tr>
<tr><td><code>--block-alert-bg</code> / <code>--block-alert-text</code></td><td>Named override for <code>#block-alert</code></td></tr>
</table>
<h3>Override Priority</h3>
<table class="var-table">
<tr><th>Priority</th><th>Method</th><th>How Applied</th></tr>
<tr><td>1 (highest)</td><td>Named module ID (<code>#block-highlight</code>)</td><td>ID in module HTML + named variable</td></tr>
<tr><td>2 (default)</td><td>Slot color (<code>--block-color-N</code>)</td><td>Automatic by <code>:nth-child()</code> order</td></tr>
</table>
<!-- ══════════════════════════════════════════════
16. VIRTUEMART COLORS
══════════════════════════════════════════════ -->
<h2>16. VirtueMart Surface Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--vm-surface, #fff)"></div>
<div class="swatch-label"><code>--vm-surface</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--vm-surface-2, #f8f9fa)"></div>
<div class="swatch-label"><code>--vm-surface-2</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--vm-price-color, #448344)"></div>
<div class="swatch-label"><code>--vm-price-color</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
17. GABLE COLORS
══════════════════════════════════════════════ -->
<h2>17. Gable Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--gab-blue)"></div>
<div class="swatch-label"><code>--gab-blue</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gab-green)"></div>
<div class="swatch-label"><code>--gab-green</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gab-red)"></div>
<div class="swatch-label"><code>--gab-red</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gab-orange)"></div>
<div class="swatch-label"><code>--gab-orange</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
18. CODE SAMPLES
══════════════════════════════════════════════ -->
<h2>18. Code &amp; Preformatted Text</h2>
<p>Inline code: <code>var(--color-primary)</code></p>
<pre>/* 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 */
&lt;div class="hero" id="primary"
style="background-image:url('/images/hero/main.jpg')"&gt;
&lt;div class="col-12 py-5 px-4 text-center"&gt;
...content...
&lt;/div&gt;
&lt;/div&gt;</pre>
<!-- ══════════════════════════════════════════════
19. OPACITY UTILITIES
══════════════════════════════════════════════ -->
<h2>19. Opacity Scale</h2>
<div style="display: flex; gap: .5rem; flex-wrap: wrap;">
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-5); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem;">5%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-10); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem;">10%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-15); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem;">15%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-25); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem; color: #fff;">25%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-50); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem; color: #fff;">50%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-75); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem; color: #fff;">75%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-100); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem; color: #fff;">100%</div>
</div>
<hr>
<p style="color: var(--muted-color); font-size: .8rem; text-align: center; margin: 2rem 0;">
MokoCassiopeia Theme Test Sheet &mdash; v03.09.02 &mdash; &copy; 2026 Moko Consulting
</p>
</div><!-- /.test-container -->
<script>
function toggleTheme() {
const html = document.documentElement;
const current = html.getAttribute('data-bs-theme');
const next = current === 'light' ? 'dark' : 'light';
html.setAttribute('data-bs-theme', next);
// Swap stylesheet
const links = document.querySelectorAll('link[rel="stylesheet"]');
links.forEach(link => {
if (link.href.includes('light.custom.css')) {
link.href = link.href.replace('light.custom.css', 'dark.custom.css');
} else if (link.href.includes('dark.custom.css')) {
link.href = link.href.replace('dark.custom.css', 'light.custom.css');
}
});
}
</script>
</body>
</html>