From 00dd6661979bf170a276db69016a32f9fed77cb2 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Tue, 7 Apr 2026 23:19:39 -0500
Subject: [PATCH 01/18] ci: sync [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.github/CODEOWNERS | 2 +-
.github/workflows/auto-assign.yml | 2 +-
.github/workflows/auto-dev-issue.yml | 2 +-
.github/workflows/auto-release.yml | 4 ++--
.github/workflows/changelog-validation.yml | 2 +-
.github/workflows/ci-joomla.yml | 2 +-
.github/workflows/deploy-manual.yml | 2 +-
.github/workflows/enterprise-firewall-setup.yml | 2 +-
.github/workflows/repo_health.yml | 2 +-
.github/workflows/repository-cleanup.yml | 6 +++++-
.github/workflows/standards-compliance.yml | 2 +-
.github/workflows/sync-version-on-merge.yml | 2 +-
.github/workflows/update-server.yml | 4 ++--
13 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index c70eb15..0108cc2 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -11,7 +11,7 @@
# ── Synced workflows (managed by MokoStandards — do not edit manually) ────
/.github/workflows/deploy-dev.yml @jmiller-moko
/.github/workflows/deploy-demo.yml @jmiller-moko
-/.github/workflows/deploy-rs.yml @jmiller-moko
+/.github/workflows/deploy-manual.yml @jmiller-moko
/.github/workflows/auto-release.yml @jmiller-moko
/.github/workflows/auto-dev-issue.yml @jmiller-moko
/.github/workflows/auto-assign.yml @jmiller-moko
diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml
index 08d0dab..d0b70f6 100644
--- a/.github/workflows/auto-assign.yml
+++ b/.github/workflows/auto-assign.yml
@@ -6,7 +6,7 @@
# INGROUP: MokoStandards.Workflows.Shared
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/workflows/auto-assign.yml
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes
name: Auto-Assign Issues & PRs
diff --git a/.github/workflows/auto-dev-issue.yml b/.github/workflows/auto-dev-issue.yml
index 75c1dae..9b5fbe2 100644
--- a/.github/workflows/auto-dev-issue.yml
+++ b/.github/workflows/auto-dev-issue.yml
@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/auto-dev-issue.yml.template
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow
# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos.
diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml
index 52c01c1..4b4706a 100644
--- a/.github/workflows/auto-release.yml
+++ b/.github/workflows/auto-release.yml
@@ -7,7 +7,7 @@
# INGROUP: MokoStandards.Release
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/auto-release.yml.template
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
#
# +========================================================================+
@@ -477,7 +477,7 @@ jobs:
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; }
cd "$SOURCE_DIR"
- zip -r "/tmp/${PACKAGE_NAME}" . -x '.ftpignore'
+ zip -r "/tmp/${PACKAGE_NAME}" . -x '.ftpignore' 'sftp-config*' '*.ppk' '*.pem' '*.key' '.env*'
cd ..
FILESIZE=$(stat -c%s "/tmp/${PACKAGE_NAME}" 2>/dev/null || stat -f%z "/tmp/${PACKAGE_NAME}" 2>/dev/null || echo "unknown")
diff --git a/.github/workflows/changelog-validation.yml b/.github/workflows/changelog-validation.yml
index 67dfc76..e2ec667 100644
--- a/.github/workflows/changelog-validation.yml
+++ b/.github/workflows/changelog-validation.yml
@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.CI
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/changelog-validation.yml.template
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Validates CHANGELOG.md format and version consistency
# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos.
diff --git a/.github/workflows/ci-joomla.yml b/.github/workflows/ci-joomla.yml
index 861770c..01e1edb 100644
--- a/.github/workflows/ci-joomla.yml
+++ b/.github/workflows/ci-joomla.yml
@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.CI
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos.
diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml
index de5143c..e127f0e 100644
--- a/.github/workflows/deploy-manual.yml
+++ b/.github/workflows/deploy-manual.yml
@@ -7,7 +7,7 @@
# INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
# NOTE: Joomla repos use update.xml for distribution. This is for manual
# dev server testing only — triggered via workflow_dispatch.
diff --git a/.github/workflows/enterprise-firewall-setup.yml b/.github/workflows/enterprise-firewall-setup.yml
index 8f8d13c..46ef7d2 100644
--- a/.github/workflows/enterprise-firewall-setup.yml
+++ b/.github/workflows/enterprise-firewall-setup.yml
@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Firewall
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server
# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules.
diff --git a/.github/workflows/repo_health.yml b/.github/workflows/repo_health.yml
index ffd52dd..73308be 100644
--- a/.github/workflows/repo_health.yml
+++ b/.github/workflows/repo_health.yml
@@ -10,7 +10,7 @@
# INGROUP: MokoStandards.Validation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/workflows/repo_health.yml
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# NOTE: Field is user-managed.
# ============================================================================
diff --git a/.github/workflows/repository-cleanup.yml b/.github/workflows/repository-cleanup.yml
index ea4c07b..ea9219d 100644
--- a/.github/workflows/repository-cleanup.yml
+++ b/.github/workflows/repository-cleanup.yml
@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Maintenance
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/repository-cleanup.yml.template
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes
# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos.
# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch.
@@ -154,6 +154,10 @@ jobs:
".github/workflows/auto-version-branch.yml"
".github/workflows/publish-to-mokodolibarr.yml"
".github/workflows/ci.yml"
+ ".github/workflows/deploy-rs.yml"
+ "sftp-config.json"
+ "sftp-config.json.template"
+ "scripts/sftp-config"
)
DELETED=0
diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml
index 24ab00d..79aaedd 100644
--- a/.github/workflows/standards-compliance.yml
+++ b/.github/workflows/standards-compliance.yml
@@ -5,7 +5,7 @@
# INGROUP: MokoStandards.Compliance
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/workflows/standards-compliance.yml
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: MokoStandards compliance validation workflow
# NOTE: Validates repository structure, documentation, and coding standards
diff --git a/.github/workflows/sync-version-on-merge.yml b/.github/workflows/sync-version-on-merge.yml
index 59acc9e..60715f6 100644
--- a/.github/workflows/sync-version-on-merge.yml
+++ b/.github/workflows/sync-version-on-merge.yml
@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers
# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos.
# README.md is the single source of truth for the repository version.
diff --git a/.github/workflows/update-server.yml b/.github/workflows/update-server.yml
index c0becfa..90482e5 100644
--- a/.github/workflows/update-server.yml
+++ b/.github/workflows/update-server.yml
@@ -7,7 +7,7 @@
# INGROUP: MokoStandards.Joomla
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/update-server.yml.template
-# VERSION: 03.09.03
+# VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple entries:
@@ -165,7 +165,7 @@ jobs:
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then
cd "$SOURCE_DIR"
- zip -r "/tmp/${PACKAGE_NAME}" . -x '.ftpignore'
+ zip -r "/tmp/${PACKAGE_NAME}" . -x '.ftpignore' 'sftp-config*' '*.ppk' '*.pem' '*.key' '.env*'
cd ..
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
From 50a6f006ac432ffd47e956b8d2bf062f9801b1df Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Tue, 7 Apr 2026 23:49:56 -0500
Subject: [PATCH 02/18] Fonts, title alignment, clear rows, custom themes
(03.09.10)
Fonts:
- Roboto CSS updated to v51 paths (was v30)
- New CSS for Fredoka and Pacifico
- Remove orphan fira-sans.css and noto-sans.css (no woff2 files)
- Updated fonts README
CSS:
- clear: both on top-a, top-b, bottom-a, bottom-b, breadcrumbs
- .title-center and .title-right alignment classes
- Banner overlay height variable
Custom themes (for dev testing):
- light.custom.css: Warm Earth (brown/saddle brown palette)
- dark.custom.css: Deep Purple (purple/teal palette)
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/media/css/fonts/fira-sans.css | 56 --
src/media/css/fonts/fredoka.css | 10 +
src/media/css/fonts/noto-sans.css | 56 --
src/media/css/fonts/pacifico.css | 6 +
src/media/css/fonts/roboto.css | 75 +-
src/media/css/template.css | 8 +-
src/media/css/theme/dark.custom.css | 1143 +++++++++++++++++++++++
src/media/css/theme/light.custom.css | 1152 ++++++++++++++++++++++++
src/media/fonts/GOOGLE_FONTS_README.md | 142 +--
src/templateDetails.xml | 2 +-
10 files changed, 2361 insertions(+), 289 deletions(-)
delete mode 100644 src/media/css/fonts/fira-sans.css
create mode 100644 src/media/css/fonts/fredoka.css
delete mode 100644 src/media/css/fonts/noto-sans.css
create mode 100644 src/media/css/fonts/pacifico.css
create mode 100644 src/media/css/theme/dark.custom.css
create mode 100644 src/media/css/theme/light.custom.css
diff --git a/src/media/css/fonts/fira-sans.css b/src/media/css/fonts/fira-sans.css
deleted file mode 100644
index 729f88a..0000000
--- a/src/media/css/fonts/fira-sans.css
+++ /dev/null
@@ -1,56 +0,0 @@
-/* Copyright (C) 2025 Moko Consulting
-
- This file is part of a Moko Consulting project.
-
- SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-/*
- * IMPORTANT: Font files must be downloaded separately
- *
- * This CSS file references Fira Sans font files that must be manually downloaded
- * and placed in the fonts directory. See GOOGLE_FONTS_README.md in the fonts
- * directory for download instructions.
- *
- * Required files:
- * - fira-sans-v17-latin-100.woff2
- * - fira-sans-v17-latin-300.woff2
- * - fira-sans-v17-latin-regular.woff2
- * - fira-sans-v17-latin-700.woff2
- */
-
-/* Fira Sans Thin (100) */
-@font-face {
- font-family: 'Fira Sans';
- font-style: normal;
- font-weight: 100;
- font-display: swap;
- src: url('../../fonts/fira-sans-v17-latin-100.woff2') format('woff2');
-}
-
-/* Fira Sans Light (300) */
-@font-face {
- font-family: 'Fira Sans';
- font-style: normal;
- font-weight: 300;
- font-display: swap;
- src: url('../../fonts/fira-sans-v17-latin-300.woff2') format('woff2');
-}
-
-/* Fira Sans Regular (400) */
-@font-face {
- font-family: 'Fira Sans';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url('../../fonts/fira-sans-v17-latin-regular.woff2') format('woff2');
-}
-
-/* Fira Sans Bold (700) */
-@font-face {
- font-family: 'Fira Sans';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: url('../../fonts/fira-sans-v17-latin-700.woff2') format('woff2');
-}
diff --git a/src/media/css/fonts/fredoka.css b/src/media/css/fonts/fredoka.css
new file mode 100644
index 0000000..206ceb3
--- /dev/null
+++ b/src/media/css/fonts/fredoka.css
@@ -0,0 +1,10 @@
+/* Copyright (C) 2026 Moko Consulting
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Fredoka — self-hosted from src/media/fonts/
+ */
+
+@font-face { font-family: 'Fredoka'; font-style: normal; font-weight: 300; font-display: swap; src: url('../../fonts/fredoka-v17-latin-300.woff2') format('woff2'); }
+@font-face { font-family: 'Fredoka'; font-style: normal; font-weight: 400; font-display: swap; src: url('../../fonts/fredoka-v17-latin-regular.woff2') format('woff2'); }
+@font-face { font-family: 'Fredoka'; font-style: normal; font-weight: 500; font-display: swap; src: url('../../fonts/fredoka-v17-latin-500.woff2') format('woff2'); }
+@font-face { font-family: 'Fredoka'; font-style: normal; font-weight: 600; font-display: swap; src: url('../../fonts/fredoka-v17-latin-600.woff2') format('woff2'); }
+@font-face { font-family: 'Fredoka'; font-style: normal; font-weight: 700; font-display: swap; src: url('../../fonts/fredoka-v17-latin-700.woff2') format('woff2'); }
diff --git a/src/media/css/fonts/noto-sans.css b/src/media/css/fonts/noto-sans.css
deleted file mode 100644
index d9549f5..0000000
--- a/src/media/css/fonts/noto-sans.css
+++ /dev/null
@@ -1,56 +0,0 @@
-/* Copyright (C) 2025 Moko Consulting
-
- This file is part of a Moko Consulting project.
-
- SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-/*
- * IMPORTANT: Font files must be downloaded separately
- *
- * This CSS file references Noto Sans font files that must be manually downloaded
- * and placed in the fonts directory. See GOOGLE_FONTS_README.md in the fonts
- * directory for download instructions.
- *
- * Required files:
- * - noto-sans-v36-latin-100.woff2
- * - noto-sans-v36-latin-300.woff2
- * - noto-sans-v36-latin-regular.woff2
- * - noto-sans-v36-latin-700.woff2
- */
-
-/* Noto Sans Thin (100) */
-@font-face {
- font-family: 'Noto Sans';
- font-style: normal;
- font-weight: 100;
- font-display: swap;
- src: url('../../fonts/noto-sans-v36-latin-100.woff2') format('woff2');
-}
-
-/* Noto Sans Light (300) */
-@font-face {
- font-family: 'Noto Sans';
- font-style: normal;
- font-weight: 300;
- font-display: swap;
- src: url('../../fonts/noto-sans-v36-latin-300.woff2') format('woff2');
-}
-
-/* Noto Sans Regular (400) */
-@font-face {
- font-family: 'Noto Sans';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url('../../fonts/noto-sans-v36-latin-regular.woff2') format('woff2');
-}
-
-/* Noto Sans Bold (700) */
-@font-face {
- font-family: 'Noto Sans';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: url('../../fonts/noto-sans-v36-latin-700.woff2') format('woff2');
-}
diff --git a/src/media/css/fonts/pacifico.css b/src/media/css/fonts/pacifico.css
new file mode 100644
index 0000000..ee29925
--- /dev/null
+++ b/src/media/css/fonts/pacifico.css
@@ -0,0 +1,6 @@
+/* Copyright (C) 2026 Moko Consulting
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Pacifico — self-hosted from src/media/fonts/
+ */
+
+@font-face { font-family: 'Pacifico'; font-style: normal; font-weight: 400; font-display: swap; src: url('../../fonts/pacifico-v23-latin-regular.woff2') format('woff2'); }
diff --git a/src/media/css/fonts/roboto.css b/src/media/css/fonts/roboto.css
index de14bfa..45ce183 100644
--- a/src/media/css/fonts/roboto.css
+++ b/src/media/css/fonts/roboto.css
@@ -1,56 +1,23 @@
-/* Copyright (C) 2025 Moko Consulting
-
- This file is part of a Moko Consulting project.
-
- SPDX-License-Identifier: GPL-3.0-or-later
+/* Copyright (C) 2026 Moko Consulting
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ * Roboto — self-hosted from src/media/fonts/
*/
-/*
- * IMPORTANT: Font files must be downloaded separately
- *
- * This CSS file references Roboto font files that must be manually downloaded
- * and placed in the fonts directory. See GOOGLE_FONTS_README.md in the fonts
- * directory for download instructions.
- *
- * Required files:
- * - roboto-v30-latin-100.woff2
- * - roboto-v30-latin-300.woff2
- * - roboto-v30-latin-regular.woff2
- * - roboto-v30-latin-700.woff2
- */
-
-/* Roboto Thin (100) */
-@font-face {
- font-family: 'Roboto';
- font-style: normal;
- font-weight: 100;
- font-display: swap;
- src: url('../../fonts/roboto-v30-latin-100.woff2') format('woff2');
-}
-
-/* Roboto Light (300) */
-@font-face {
- font-family: 'Roboto';
- font-style: normal;
- font-weight: 300;
- font-display: swap;
- src: url('../../fonts/roboto-v30-latin-300.woff2') format('woff2');
-}
-
-/* Roboto Regular (400) */
-@font-face {
- font-family: 'Roboto';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url('../../fonts/roboto-v30-latin-regular.woff2') format('woff2');
-}
-
-/* Roboto Bold (700) */
-@font-face {
- font-family: 'Roboto';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: url('../../fonts/roboto-v30-latin-700.woff2') format('woff2');
-}
+@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 100; font-display: swap; src: url('../../fonts/roboto-v51-latin-100.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: italic; font-weight: 100; font-display: swap; src: url('../../fonts/roboto-v51-latin-100italic.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 200; font-display: swap; src: url('../../fonts/roboto-v51-latin-200.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: italic; font-weight: 200; font-display: swap; src: url('../../fonts/roboto-v51-latin-200italic.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 300; font-display: swap; src: url('../../fonts/roboto-v51-latin-300.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: italic; font-weight: 300; font-display: swap; src: url('../../fonts/roboto-v51-latin-300italic.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; font-display: swap; src: url('../../fonts/roboto-v51-latin-regular.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: italic; font-weight: 400; font-display: swap; src: url('../../fonts/roboto-v51-latin-italic.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 500; font-display: swap; src: url('../../fonts/roboto-v51-latin-500.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: italic; font-weight: 500; font-display: swap; src: url('../../fonts/roboto-v51-latin-500italic.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 600; font-display: swap; src: url('../../fonts/roboto-v51-latin-600.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: italic; font-weight: 600; font-display: swap; src: url('../../fonts/roboto-v51-latin-600italic.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 700; font-display: swap; src: url('../../fonts/roboto-v51-latin-700.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: italic; font-weight: 700; font-display: swap; src: url('../../fonts/roboto-v51-latin-700italic.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 800; font-display: swap; src: url('../../fonts/roboto-v51-latin-800.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: italic; font-weight: 800; font-display: swap; src: url('../../fonts/roboto-v51-latin-800italic.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 900; font-display: swap; src: url('../../fonts/roboto-v51-latin-900.woff2') format('woff2'); }
+@font-face { font-family: 'Roboto'; font-style: italic; font-weight: 900; font-display: swap; src: url('../../fonts/roboto-v51-latin-900italic.woff2') format('woff2'); }
diff --git a/src/media/css/template.css b/src/media/css/template.css
index f9c9f5b..77723d8 100644
--- a/src/media/css/template.css
+++ b/src/media/css/template.css
@@ -14624,6 +14624,10 @@ iframe {
line-height: 1.3;
}
+/* Module title alignment — apply via module class suffix */
+.title-center [class*="__title"] { text-align: center; }
+.title-right [class*="__title"] { text-align: right; }
+
/* ── MODULE: Statistics ── */
.mod-stats__list {
margin: 0;
@@ -14853,8 +14857,10 @@ iframe {
.container-top-a,
.container-top-b,
.container-bottom-a,
-.container-bottom-b {
+.container-bottom-b,
+.mod-breadcrumbs {
position: relative;
+ clear: both;
}
.container-top-a>*,
diff --git a/src/media/css/theme/dark.custom.css b/src/media/css/theme/dark.custom.css
new file mode 100644
index 0000000..50f9782
--- /dev/null
+++ b/src/media/css/theme/dark.custom.css
@@ -0,0 +1,1143 @@
+@charset "UTF-8";
+/* Copyright (C) 2025 Moko Consulting
+
+ This file is part of a Moko Consulting project.
+
+ SPDX-License-Identifier: GPL-3.0-or-later
+
+/* -----------------------------------------------
+ * DARK CUSTOM THEME — Deep Purple
+ * --------------------------------------------- */
+
+:root[data-bs-theme='dark']{
+color-scheme: dark;
+
+/* ===== BRAND & THEME COLORS ===== */
+--color-primary: #4A0E4E;
+--accent-color-primary: #9B59B6;
+--accent-color-secondary: #6fb3ff;
+
+/* ===== NAVIGATION ===== */
+--mainmenu-nav-link-color: #fff;
+--nav-text-color: gray;
+--nav-bg-color: var(--color-primary);
+
+/* ===== LINKS ===== */
+--color-link: white;
+--color-hover: gray;
+--color-active: var(--mainmenu-nav-link-color);
+--link-color: #8ab4f8;
+--link-color-rgb: 138, 180, 248;
+--link-decoration: underline;
+--link-hover-color: #1ABC9C;
+--link-hover-color-rgb: 195, 214, 255;
+--link-active-color: var(--link-color);
+
+/* ===== NAVBAR ===== */
+--navbar-padding-x: 1rem;
+--navbar-padding-y: 0.5rem;
+--navbar-color: var(--nav-text-color);
+--navbar-active-color: var(--mainmenu-nav-link-color);
+--navbar-disabled-color: #6c757d;
+--navbar-brand-padding-y: 0.3125rem;
+--navbar-brand-margin-end: 1rem;
+--navbar-brand-font-size: 1.25rem;
+--navbar-brand-color: var(--nav-text-color);
+--navbar-brand-active-color: var(--mainmenu-nav-link-color);
+--navbar-nav-link-padding-x: 0.5rem;
+--navbar-toggler-padding-y: 0.25rem;
+--navbar-toggler-padding-x: 0.75rem;
+--navbar-toggler-font-size: 1.25rem;
+--navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
+--navbar-toggler-border-radius: 0.25rem;
+--navbar-toggler-focus-width: 0.25rem;
+--navbar-toggler-transition: box-shadow 0.15s ease-in-out;
+--nav-link-padding-x: 1rem;
+--nav-link-padding-y: 0.5rem;
+--nav-link-font-weight: 400;
+--nav-link-color: var(--nav-text-color);
+--nav-link-active-color: var(--mainmenu-nav-link-color);
+--nav-link-disabled-color: #6c757d;
+
+/* ===== TYPOGRAPHY & BODY ===== */
+--font-sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+--font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+--body-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+--body-font-size: 1rem;
+--body-font-weight: 400;
+--body-line-height: 1.5;
+--body-color: #e6ebf1;
+--body-color-rgb: 230, 235, 241;
+--body-bg: #0e1318;
+--body-bg-rgb: 14, 19, 24;
+--heading-color: #f1f5f9;
+--emphasis-color: #fff;
+--emphasis-color-rgb: 255, 255, 255;
+--secondary-color: #e6ebf1bf;
+--secondary-color-rgb: 230, 235, 241;
+--tertiary-color: #e6ebf180;
+--tertiary-color-rgb: 230, 235, 241;
+--muted-color: #6d757e;
+--code-color: black;
+--code-color-ink: var(--code-color, #e93f8e);
+--code-bg-color: lightgreen;
+--highlight-color: #111;
+--highlight-bg: #ffe28a1a;
+
+/* ===== LAYOUT & SPACING ===== */
+--padding-x: 0.15rem;
+--padding-y: 0.15rem;
+--bg-opacity: 1;
+--nav-toggle-size: 3rem;
+--gradient: linear-gradient(180deg, #ffffff26, #fff0);
+--secondary-bg: #151b22;
+--secondary-bg-rgb: 21, 27, 34;
+--tertiary-bg: #10151b;
+--tertiary-bg-rgb: 16, 21, 27;
+--hr-color: var(--border-color, #dfe3e7);
+--border-color-soft: var(--border-color, #dfe3e7);
+--kbd-bg: var(--secondary-bg, #eaedf0);
+--kbd-ink: var(--body-bg, #fff);
+--toc-bg: var(--secondary-bg, #eaedf0);
+--toc-ink: var(--color-primary, #112855);
+--selection-bg: var(--highlight-bg, #fbeea8);
+--selection-ink: var(--body-color, #22262a);
+--border: 5px;
+
+/* ===== BREAKPOINTS ===== */
+--bp-xs: 0;
+--bp-sm: 576px;
+--bp-md: 768px;
+--bp-lg: 992px;
+--bp-xl: 1200px;
+
+/* ===== BOOTSTRAP PALETTE ===== */
+--primary: #010156;
+--secondary: #48525d;
+--success: #4aa664;
+--info: #4f7aa0;
+--warning: #c77a00;
+--danger: #c23a31;
+--light: #1b2027;
+--dark: #0f1318;
+--primary-rgb: 1,1,86;
+--secondary-rgb: 72,82,93;
+--success-rgb: 74,166,100;
+--info-rgb: 79,122,160;
+--warning-rgb: 199,122,0;
+--danger-rgb: 194,58,49;
+--light-rgb: 27,32,39;
+--dark-rgb: 15,19,24;
+--primary-text-emphasis: #c7ccff;
+--secondary-text-emphasis: #cfd6de;
+--success-text-emphasis: #bde8c9;
+--info-text-emphasis: #bcd6ee;
+--warning-text-emphasis: #ffd9a6;
+--danger-text-emphasis: #ffb7b2;
+--light-text-emphasis: #d2d8df;
+--dark-text-emphasis: #d2d8df;
+--primary-bg-subtle: #0b1030;
+--secondary-bg-subtle: #1e2430;
+--success-bg-subtle: #0f2a1b;
+--info-bg-subtle: #0d2232;
+--warning-bg-subtle: #2a1e06;
+--danger-bg-subtle: #2d1110;
+--light-bg-subtle: #12161d;
+--dark-bg-subtle: #1e2430;
+--primary-border-subtle: #2b3a7a;
+--secondary-border-subtle: #2b323b;
+--success-border-subtle: #2b5b40;
+--info-border-subtle: #254861;
+--warning-border-subtle: #5a3c0e;
+--danger-border-subtle: #5c2723;
+--light-border-subtle: #222831;
+--dark-border-subtle: #2b323b;
+
+/* ===== ALERT LINK COLORS ===== */
+--alert-primary-link-color: #b3c1ff;
+--alert-secondary-link-color: #9fa6ad;
+--alert-success-link-color: #a0e5b3;
+--alert-info-link-color: #8eccf2;
+--alert-warning-link-color: #ffe4a0;
+--alert-danger-link-color: #ffa8a3;
+--alert-light-link-color: #f0f4f8;
+--alert-dark-link-color: #9fa6ad;
+
+/* ===== LIST GROUP ITEM COLORS ===== */
+--list-group-item-primary-color: #8ca3ff;
+--list-group-item-primary-bg: #1a2550;
+--list-group-item-primary-active-bg: #223066;
+--list-group-item-secondary-color: #9fa6ad;
+--list-group-item-secondary-bg: #2b323b;
+--list-group-item-secondary-active-bg: #363d47;
+--list-group-item-success-color: #a0e5b3;
+--list-group-item-success-bg: #1e3d2d;
+--list-group-item-success-active-bg: #275538;
+--list-group-item-info-color: #8eccf2;
+--list-group-item-info-bg: #1a3448;
+--list-group-item-info-active-bg: #234459;
+--list-group-item-warning-color: #ffe4a0;
+--list-group-item-warning-bg: #4a3410;
+--list-group-item-warning-active-bg: #5c4216;
+--list-group-item-danger-color: #ffa8a3;
+--list-group-item-danger-bg: #4a1e1c;
+--list-group-item-danger-active-bg: #5c2823;
+--list-group-item-light-color: #e9ecef;
+--list-group-item-light-bg: #1e2430;
+--list-group-item-light-active-bg: #282f3d;
+--list-group-item-dark-color: #48525d;
+--list-group-item-dark-bg: #0e1318;
+--list-group-item-dark-active-bg: #161b22;
+
+/* ===== LINK UTILITY COLORS ===== */
+--link-primary-color: hsl(240, 98%, 50%);
+--link-primary-hover-color: hsl(240, 98%, 45%);
+--link-secondary-color: hsl(210, 15%, 70%);
+--link-secondary-hover-color: hsl(210, 15%, 65%);
+--link-success-color: hsl(120, 40%, 60%);
+--link-success-hover-color: hsl(120, 40%, 55%);
+--link-info-color: hsl(207, 60%, 65%);
+--link-info-hover-color: hsl(207, 60%, 60%);
+--link-warning-color: hsl(38, 100%, 65%);
+--link-warning-hover-color: hsl(38, 100%, 60%);
+--link-danger-color: hsl(3, 85%, 65%);
+--link-danger-hover-color: hsl(3, 85%, 60%);
+--link-light-color: hsl(210, 20%, 90%);
+--link-light-hover-color: hsl(210, 20%, 85%);
+--link-dark-color: hsl(210, 10%, 35%);
+--link-dark-hover-color: hsl(210, 10%, 30%);
+
+/* ===== COMPONENT-SPECIFIC COLORS ===== */
+--mod-finder-link-hover: #5a6470;
+--form-legend-color: #9fa6ad;
+--border-gray: #3a4250;
+--subhead-color: #9fa6ad;
+--box-shadow-gray: #1a2027;
+--btn-active-text-gray: #7a8490;
+--indicator-success-bg: var(--success);
+--item-list-color: #2a2f34;
+--notification-badge-bg: var(--danger);
+--content-bg-gray: #2b323b;
+--taba-btn-green: #5a9c2f;
+--taba-btn-blue: #3d75a8;
+--taba-btn-red: #c43620;
+--taba-btn-gray: #6a7080;
+--taba-msg-bg: #1e2430;
+--toc-link-color: #9fa6ad;
+--toc-link-active-color: #91a4ff;
+--choices-disabled-bg: #2b323b;
+--choices-input-bg: var(--body-bg);
+--choices-border-light: #48525d;
+--choices-arrow-color: #9fa6ad;
+--choices-inner-bg: #1a2027;
+--choices-focused-border: #5472ff;
+--choices-dropdown-bg: var(--body-bg);
+--choices-item-bg: #1a5f75;
+--choices-item-border: #1a748f;
+--choices-item-hover-bg: #1a748f;
+--choices-item-hover-border: #1a8aa8;
+--choices-item-disabled-bg: #48525d;
+--choices-item-disabled-border: #36404a;
+--choices-item-highlighted: #2b323b;
+--choices-input-inner-bg: #1a2027;
+
+/* ===== STANDARD COLORS ===== */
+--blue: #91a4ff;
+--indigo: #b19cff;
+--purple: #c0a5ff;
+--pink: #ff8fc0;
+--red: #ff7a73;
+--orange: #ff9c4d;
+--yellow: #ffd166;
+--green: #78d694;
+--teal: #76e3ff;
+--cyan: #6fb7ff;
+--black: #000;
+--white: #fff;
+
+/* ===== GRAY SCALE ===== */
+--gray-100: #161a20;
+--gray-200: #1b2027;
+--gray-300: #222831;
+--gray-400: #2b323b;
+--gray-500: #36404a;
+--gray-600: #48525d;
+--gray-700: #5b6672;
+--gray-800: #cfd6de;
+--gray-900: #e6ebf1;
+--white-rgb: 255, 255, 255;
+--black-rgb: 0, 0, 0;
+
+/* ===== OPACITY UTILITIES ===== */
+--opacity-0: 0;
+--opacity-5: 0.05;
+--opacity-10: 0.1;
+--opacity-15: 0.15;
+--opacity-20: 0.2;
+--opacity-25: 0.25;
+--opacity-30: 0.3;
+--opacity-50: 0.5;
+--opacity-75: 0.75;
+--opacity-100: 1;
+
+/* ===== COMMON SHADOW COLORS ===== */
+--shadow-color-light: rgba(var(--black-rgb), var(--opacity-30));
+--shadow-color-medium: rgba(var(--black-rgb), var(--opacity-50));
+--shadow-color-dark: rgba(var(--black-rgb), var(--opacity-75));
+--border-color-translucent: rgba(var(--white-rgb), var(--opacity-10));
+--highlight-translucent: rgba(var(--white-rgb), var(--opacity-5));
+
+/* ===== HEADER BACKGROUND ===== */
+--header-background-image: url('../../../../../../media/templates/site/mokocassiopeia/images/bg.svg');
+--header-background-attachment: fixed;
+--header-background-repeat: repeat;
+--header-background-size: auto;
+
+/* ===== CONTAINER BACKGROUNDS ===== */
+/* Below Topbar Container */
+--container-below-topbar-bg-image: none;
+--container-below-topbar-bg-color: transparent;
+--container-below-topbar-bg-position: center;
+--container-below-topbar-bg-attachment: fixed;
+--container-below-topbar-bg-repeat: no-repeat;
+--container-below-topbar-bg-size: cover;
+--container-below-topbar-border: none;
+--container-below-topbar-border-radius: 0;
+
+/* Top A Container */
+--container-top-a-bg-image: none;
+--container-top-a-bg-color: transparent;
+--container-top-a-bg-position: center;
+--container-top-a-bg-attachment: fixed;
+--container-top-a-bg-repeat: no-repeat;
+--container-top-a-bg-size: cover;
+--container-top-a-border: none;
+--container-top-a-border-radius: 0;
+
+/* Top B Container */
+--container-top-b-bg-image: none;
+--container-top-b-bg-color: transparent;
+--container-top-b-bg-position: center;
+--container-top-b-bg-attachment: fixed;
+--container-top-b-bg-repeat: no-repeat;
+--container-top-b-bg-size: cover;
+--container-top-b-border: none;
+--container-top-b-border-radius: 0;
+
+/* TOC Container */
+--container-toc-bg: var(--secondary-bg);
+--container-toc-color: #dbe3ff;
+
+/* Sidebar Container */
+--container-sidebar-bg-image: none;
+--container-sidebar-bg-color: transparent;
+--container-sidebar-bg-position: center;
+--container-sidebar-bg-attachment: scroll;
+--container-sidebar-bg-repeat: repeat;
+--container-sidebar-bg-size: auto;
+--container-sidebar-border: none;
+--container-sidebar-border-radius: 0;
+
+/* Bottom A Container */
+--container-bottom-a-bg-image: none;
+--container-bottom-a-bg-color: transparent;
+--container-bottom-a-bg-position: center;
+--container-bottom-a-bg-attachment: fixed;
+--container-bottom-a-bg-repeat: no-repeat;
+--container-bottom-a-bg-size: cover;
+--container-bottom-a-border: none;
+--container-bottom-a-border-radius: 5px;
+
+/* Bottom B Container */
+--container-bottom-b-bg-image: none;
+--container-bottom-b-bg-color: transparent;
+--container-bottom-b-bg-position: center;
+--container-bottom-b-bg-attachment: fixed;
+--container-bottom-b-bg-repeat: no-repeat;
+--container-bottom-b-bg-size: cover;
+--container-bottom-b-border: none;
+--container-bottom-b-border-radius: 0;
+
+/* ===== BORDERS ===== */
+--border-width: 1px;
+--border-style: solid;
+--border-color: #2b323b;
+--border-color-translucent: #ffffff26;
+--border-radius: .25rem;
+--border-radius-sm: .2rem;
+--border-radius-lg: .3rem;
+--border-radius-xl: .3rem;
+--border-radius-xxl: 2rem;
+--border-radius-2xl: var(--border-radius-xxl);
+--border-radius-pill: 50rem;
+
+/* ===== SHADOWS ===== */
+--box-shadow: 0 .5rem 1rem #00000066;
+--box-shadow-sm: 0 .125rem .25rem #00000040;
+--box-shadow-lg: 0 1rem 3rem #00000080;
+--box-shadow-inset: inset 0 1px 2px #00000040;
+
+/* ===== FOCUS & FORMS ===== */
+--focus-ring-width: .25rem;
+--focus-ring-opacity: .6;
+--focus-ring-color: #5472ff66;
+--input-color: #e6ebf1;
+--input-bg: #1a2332;
+--input-border-color: #3a4250;
+--input-focus-border-color: #5472ff;
+--input-focus-box-shadow: 0 0 0 0.25rem rgba(84, 114, 255, 0.25);
+--input-placeholder-color: #8894aa;
+--input-disabled-bg: #0f1318;
+--input-disabled-border-color: #2b323b;
+--input-file-button-active-bg: #2b3441;
+--form-range-thumb-active-bg: #4a5766;
+--form-valid-color: #78d694;
+--form-valid-border-color: #78d694;
+--form-invalid-color: #ff8e86;
+--form-invalid-border-color: #ff8e86;
+
+/* ===== BUTTONS ===== */
+--btn-border-radius: var(--border-radius);
+--btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 1px 1px rgba(0, 0, 0, 0.3);
+
+/* ===== CARDS ===== */
+--card-spacer-y: 1rem;
+--card-spacer-x: 1rem;
+--card-title-spacer-y: 0.5rem;
+--card-border-width: 1px;
+--card-border-color: var(--border-color);
+--card-border-radius: var(--border-radius);
+--card-box-shadow: none;
+--card-inner-border-radius: calc(var(--border-radius) - 1px);
+--card-cap-padding-y: 0.5rem;
+--card-cap-padding-x: 1rem;
+--card-cap-bg: rgba(255, 255, 255, 0.03);
+--card-cap-color: var(--body-color);
+--card-height: auto;
+--card-color: var(--body-color);
+--card-bg: var(--secondary-bg);
+--card-img-overlay-padding: 1rem;
+--card-group-margin: 0.75rem;
+
+/* ===== VIRTUEMART (VM) ===== */
+/* VM Surfaces */
+--vm-surface: var(--secondary-bg);
+--vm-surface-2: var(--tertiary-bg);
+--vm-text: var(--body-color);
+--vm-text-strong: #ffffff;
+--vm-text-muted: var(--gray-700);
+--vm-border: var(--border-color);
+--vm-price-color: var(--success);
+
+/* VM Layout and Density */
+--vm-container-max-width: 1200px;
+--vm-section-gap: 2rem;
+--vm-block-radius: var(--border-radius);
+--vm-block-shadow: var(--box-shadow-sm);
+
+/* VM Typography */
+--vm-category-title-size: 2rem;
+--vm-subcategory-title-size: 1.5rem;
+--vm-page-title-size: 1.75rem;
+--vm-products-type-title-size: 1.25rem;
+--vm-product-title-size: 1.125rem;
+--vm-product-title-weight: 500;
+--vm-products-type-title-weight: 600;
+--vm-price-size: 1.5rem;
+--vm-price-detail-size: 1.125rem;
+--vm-price-desc-size: 0.875rem;
+
+/* VM Controls */
+--vm-input-radius: var(--border-radius);
+--vm-input-shadow: var(--box-shadow-sm);
+--vm-qty-width: 80px;
+--vm-cart-dropdown-min-width: 300px;
+
+/* VM Alerts */
+--vm-alert-radius: var(--border-radius);
+--vm-alert-shadow: var(--box-shadow-sm);
+--vm-availability-bg: var(--success-bg-subtle);
+--vm-availability-text: var(--success);
+
+/* VM Buttons */
+--vm-btn-padding-x: 1rem;
+--vm-btn-padding-y: 0.5rem;
+--vm-btn-radius: var(--border-radius);
+--vm-btn-shadow: var(--box-shadow-sm);
+--vm-btn-primary-bg: var(--primary);
+--vm-btn-primary-text: #ffffff;
+--vm-btn-primary-border: var(--primary);
+--vm-btn-secondary-bg: var(--secondary);
+--vm-btn-secondary-text: #ffffff;
+--vm-btn-secondary-border: var(--secondary);
+
+/* VM Image Overlay Controls */
+--vm-image-overlay-gap-x: 0.5rem;
+--vm-image-overlay-gap-y: 0.5rem;
+--vm-image-overlay-raise: 0.25rem;
+--vm-image-overlay-btn-size: 2.5rem;
+--vm-image-overlay-btn-radius: 50%;
+--vm-image-overlay-btn-bg: rgba(0, 0, 0, 0.7);
+--vm-image-overlay-btn-bg-hover: rgba(0, 0, 0, 0.85);
+--vm-image-overlay-btn-border-color: rgba(255, 255, 255, 0.2);
+--vm-image-overlay-btn-border-width: 1px;
+--vm-image-overlay-btn-color: var(--body-color);
+--vm-image-overlay-btn-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+
+/* VM Vendor Menu */
+--vm-vendor-menu-bg: var(--secondary-bg);
+--vm-vendor-menu-border: var(--border-color);
+--vm-vendor-menu-radius: var(--border-radius);
+--vm-vendor-menu-shadow: var(--box-shadow-sm);
+--vm-vendor-menu-item-gap: 0.25rem;
+--vm-vendor-menu-item-padding-x: 1rem;
+--vm-vendor-menu-item-padding-y: 0.5rem;
+--vm-vendor-menu-pill-radius: 50rem;
+--vm-vendor-menu-link: var(--link-color);
+--vm-vendor-menu-link-hover: var(--link-hover-color);
+--vm-vendor-menu-link-active: var(--primary);
+--vm-vendor-menu-hover-bg: var(--tertiary-bg);
+
+/* ===== GABLE ===== */
+--gab-blue: #4d9fff;
+--gab-green: #5cb85c;
+--gab-red: #ff6b6b;
+--gab-orange: #ff9f5a;
+--gab-gray1: #868e96;
+--gab-gray2: #adb5bd;
+--gab-gray3: #ced4da;
+
+/* ===== HERO / BANNER OVERLAY ===== */
+--hero-height: 70vh;
+--hero-color: var(--body-color);
+--hero-bg-repeat: no-repeat;
+--hero-bg-attachment: fixed;
+--hero-bg-position: top center;
+--hero-bg-size: cover;
+--hero-border-bottom: solid var(--accent-color-secondary);
+--hero-overlay-bg: hsla(0, 0%, 0%, 0.3);
+--hero-overlay-bg-position: center;
+--hero-overlay-bg-size: cover;
+--hero-overlay-padding: 1em;
+--hero-overlay-text-align: center;
+--hero-overlay-text-color: var(--body-color);
+
+/* ===== 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;
+
+/* ===== HERO CARD (inner .hero element) ===== */
+/* Default card — uses primary variant values */
+--hero-card-bg: var(--hero-primary-bg-color);
+--hero-card-color: white;
+--hero-card-overlay: var(--hero-primary-overlay);
+--hero-card-border-radius: .5rem;
+--hero-card-padding-x: 2rem;
+--hero-card-padding-y: 3rem;
+--hero-card-max-width: 800px;
+
+/* Alternative card — uses secondary variant values */
+--hero-alt-card-bg: var(--hero-secondary-bg-color);
+--hero-alt-card-color: var(--hero-secondary-color);
+--hero-alt-card-overlay: var(--hero-secondary-overlay);
+--hero-alt-card-border-radius: .5rem;
+--hero-alt-card-padding-x: 2rem;
+--hero-alt-card-padding-y: 3rem;
+--hero-alt-card-max-width: 600px;
+
+/* ===== 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;
+
+/* ===== OFFCANVAS ===== */
+--offcanvas-zindex: 1045;
+--offcanvas-width: 400px;
+--offcanvas-height: 30vh;
+--offcanvas-padding-x: 1rem;
+--offcanvas-padding-y: 1rem;
+--offcanvas-color: var(--body-color);
+--offcanvas-bg: var(--body-bg);
+--offcanvas-border-width: 1px;
+--offcanvas-border-color: var(--border-color-translucent);
+--offcanvas-box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.3);
+
+/* ===== ACCORDION ===== */
+--accordion-color: var(--body-color);
+--accordion-bg: var(--body-bg);
+--accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;
+--accordion-border-color: var(--border-color);
+--accordion-border-width: 1px;
+--accordion-border-radius: 0.25rem;
+--accordion-inner-border-radius: calc(0.25rem - 1px);
+--accordion-btn-padding-x: 1.25rem;
+--accordion-btn-padding-y: 1rem;
+--accordion-btn-color: var(--body-color);
+--accordion-btn-bg: var(--accordion-bg);
+--accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23e6ebf1'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+--accordion-btn-icon-width: 1.25rem;
+--accordion-btn-icon-transform: rotate(-180deg);
+--accordion-btn-icon-transition: transform 0.2s ease-in-out;
+--accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%238ab4f8'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+--accordion-btn-focus-border-color: var(--input-focus-border-color);
+--accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(84, 114, 255, 0.25);
+--accordion-body-padding-x: 1.25rem;
+--accordion-body-padding-y: 1rem;
+--accordion-active-color: var(--link-color);
+--accordion-active-bg: var(--secondary-bg);
+
+/* ===== BREADCRUMB ===== */
+--breadcrumb-padding-x: 0;
+--breadcrumb-padding-y: 0;
+--breadcrumb-margin-bottom: 1rem;
+--breadcrumb-bg: ;
+--breadcrumb-border-radius: ;
+--breadcrumb-divider-color: var(--gray-600);
+--breadcrumb-item-padding-x: 0.5rem;
+--breadcrumb-item-active-color: var(--link-color);
+
+/* ===== PAGINATION ===== */
+--pagination-padding-x: 0.75rem;
+--pagination-padding-y: 0.375rem;
+--pagination-font-size: 1rem;
+--pagination-color: var(--link-color);
+--pagination-bg: var(--secondary-bg);
+--pagination-border-width: 1px;
+--pagination-border-color: var(--border-color);
+--pagination-border-radius: 0.25rem;
+--pagination-focus-color: var(--link-active-color);
+--pagination-focus-bg: var(--tertiary-bg);
+--pagination-focus-box-shadow: 0 0 0 0.25rem rgba(84, 114, 255, 0.25);
+--pagination-active-color: var(--body-color);
+--pagination-active-bg: hsl(240, 98%, 17%);
+--pagination-active-border-color: hsl(240, 98%, 17%);
+--pagination-disabled-color: var(--gray-600);
+--pagination-disabled-bg: var(--secondary-bg);
+--pagination-disabled-border-color: var(--border-color);
+
+/* ===== BADGE ===== */
+--badge-padding-x: 0.65em;
+--badge-padding-y: 0.35em;
+--badge-font-size: 0.75em;
+--badge-font-weight: 700;
+--badge-color: var(--body-color);
+--badge-border-radius: 0.25rem;
+
+/* ===== ALERT ===== */
+--alert-bg: transparent;
+--alert-padding-x: 1rem;
+--alert-padding-y: 1rem;
+--alert-margin-bottom: 1rem;
+--alert-color: inherit;
+--alert-border-color: transparent;
+--alert-border: 1px solid var(--alert-border-color);
+--alert-border-radius: 0.25rem;
+
+/* ===== PROGRESS ===== */
+--progress-height: 1rem;
+--progress-font-size: 0.75rem;
+--progress-bg: var(--secondary-bg);
+--progress-border-radius: 0.25rem;
+--progress-box-shadow: inset 0 1px 2px rgba(var(--black-rgb), 0.3);
+--progress-bar-color: var(--body-color);
+--progress-bar-bg: hsl(240, 98%, 40%);
+--progress-bar-transition: width 0.6s ease;
+
+/* ===== LIST GROUP ===== */
+--list-group-color: var(--body-color);
+--list-group-bg: var(--secondary-bg);
+--list-group-border-color: rgba(var(--white-rgb), 0.125);
+--list-group-border-width: 1px;
+--list-group-border-radius: 0.25rem;
+--list-group-item-padding-x: 1rem;
+--list-group-item-padding-y: 0.5rem;
+--list-group-action-color: var(--gray-800);
+--list-group-action-active-color: var(--body-color);
+--list-group-action-active-bg: var(--tertiary-bg);
+--list-group-disabled-color: var(--gray-600);
+--list-group-disabled-bg: var(--secondary-bg);
+--list-group-active-color: var(--body-color);
+--list-group-active-bg: hsl(240, 98%, 17%);
+--list-group-active-border-color: hsl(240, 98%, 17%);
+
+/* ===== DROPDOWN ===== */
+--dropdown-zindex: 1000;
+--dropdown-min-width: 10rem;
+--dropdown-padding-x: 0;
+--dropdown-padding-y: 0.5rem;
+--dropdown-spacer: 0.125rem;
+--dropdown-font-size: 1rem;
+--dropdown-color: var(--body-color);
+--dropdown-bg: var(--secondary-bg);
+--dropdown-border-color: var(--border-color-translucent);
+--dropdown-border-radius: 0.25rem;
+--dropdown-border-width: 1px;
+--dropdown-inner-border-radius: calc(0.25rem - 1px);
+--dropdown-divider-bg: var(--border-color-translucent);
+--dropdown-divider-margin-y: 0.5rem;
+--dropdown-box-shadow: 0 0.5rem 1rem var(--shadow-color-medium);
+--dropdown-link-color: var(--body-color);
+--dropdown-link-active-color: var(--body-color);
+--dropdown-link-active-bg: hsl(240, 98%, 17%);
+--dropdown-link-disabled-color: var(--gray-600);
+--dropdown-item-padding-x: 1rem;
+--dropdown-item-padding-y: 0.25rem;
+--dropdown-header-color: var(--gray-600);
+--dropdown-header-padding-x: 1rem;
+--dropdown-header-padding-y: 0.5rem;
+
+/* ===== TOAST ===== */
+--toast-zindex: 1090;
+--toast-padding-x: 0.75rem;
+--toast-padding-y: 0.5rem;
+--toast-spacing: 1em;
+--toast-max-width: 350px;
+--toast-font-size: 0.875rem;
+--toast-color: ;
+--toast-bg: rgba(21, 27, 34, 0.9);
+--toast-border-width: 1px;
+--toast-border-color: var(--border-color-translucent);
+--toast-border-radius: 0.25rem;
+--toast-box-shadow: 0 0.5rem 1rem var(--shadow-color-medium);
+--toast-header-color: var(--gray-600);
+--toast-header-bg: rgba(21, 27, 34, 0.85);
+--toast-header-border-color: rgba(var(--white-rgb), var(--opacity-10));
+
+/* ===== MODAL ===== */
+--modal-zindex: 1050;
+--modal-width: 500px;
+--modal-padding: 1rem;
+--modal-margin: 0.5rem;
+--modal-color: ;
+--modal-bg: var(--secondary-bg);
+--modal-border-color: var(--border-color-translucent);
+--modal-border-width: 1px;
+--modal-border-radius: 0.3rem;
+--modal-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.3);
+--modal-inner-border-radius: calc(0.3rem - 1px);
+--modal-header-padding-x: 1rem;
+--modal-header-padding-y: 1rem;
+--modal-header-padding: 1rem 1rem;
+--modal-header-border-color: var(--border-color);
+--modal-header-border-width: 1px;
+--modal-title-line-height: 1.5;
+--modal-footer-gap: 0.5rem;
+--modal-footer-bg: ;
+--modal-footer-border-color: var(--border-color);
+--modal-footer-border-width: 1px;
+
+/* ===== TOOLTIP ===== */
+--tooltip-zindex: 1070;
+--tooltip-max-width: 200px;
+--tooltip-padding-x: 0.5rem;
+--tooltip-padding-y: 0.25rem;
+--tooltip-margin: ;
+--tooltip-font-size: 0.875rem;
+--tooltip-color: var(--body-color);
+--tooltip-bg: hsl(0, 0%, 0%);
+--tooltip-border-radius: 0.25rem;
+--tooltip-opacity: 0.9;
+--tooltip-arrow-width: 0.8rem;
+--tooltip-arrow-height: 0.4rem;
+
+/* ===== POPOVER ===== */
+--popover-zindex: 1060;
+--popover-max-width: 276px;
+--popover-font-size: 0.875rem;
+--popover-bg: var(--secondary-bg);
+--popover-border-width: 1px;
+--popover-border-color: var(--border-color-translucent);
+--popover-border-radius: 0.3rem;
+--popover-inner-border-radius: calc(0.3rem - 1px);
+--popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4);
+--popover-header-padding-x: 1rem;
+--popover-header-padding-y: 0.5rem;
+--popover-header-font-size: 1rem;
+--popover-header-color: ;
+--popover-header-bg: var(--tertiary-bg);
+--popover-body-padding-x: 1rem;
+--popover-body-padding-y: 1rem;
+--popover-body-color: var(--body-color);
+--popover-arrow-width: 1rem;
+--popover-arrow-height: 0.5rem;
+--popover-arrow-border: var(--popover-border-color);
+
+/* ===== SPINNER ===== */
+--spinner-width: 2rem;
+--spinner-height: 2rem;
+--spinner-vertical-align: -0.125em;
+--spinner-border-width: 0.25em;
+--spinner-animation-speed: 0.75s;
+
+/* ===== NAV TABS ===== */
+--nav-tabs-border-width: 1px;
+--nav-tabs-border-color: var(--border-color);
+--nav-tabs-border-radius: 0.25rem;
+--nav-tabs-link-active-color: var(--body-color);
+--nav-tabs-link-active-bg: var(--secondary-bg);
+--nav-tabs-link-active-border-color: var(--border-color) var(--border-color) var(--secondary-bg);
+
+/* ===== NAV PILLS ===== */
+--nav-pills-border-radius: 0.25rem;
+--nav-pills-link-active-color: var(--body-color);
+--nav-pills-link-active-bg: hsl(240, 98%, 17%);
+
+/* ===== TABLE ===== */
+--table-color: var(--body-color);
+--table-bg: transparent;
+--table-border-color: var(--border-color);
+--table-accent-bg: transparent;
+--table-striped-color: var(--body-color);
+--table-striped-bg: rgba(var(--white-rgb), var(--opacity-5));
+--table-active-color: var(--body-color);
+--table-active-bg: rgba(var(--white-rgb), 0.1);
+
+/* ===== BACKDROP ===== */
+--backdrop-zindex: 1040;
+--backdrop-bg: hsl(0, 0%, 0%);
+--backdrop-opacity: 0.5;
+}
+
+.btn {
+ --btn-padding-x: 1rem;
+ --btn-padding-y: 0.6rem;
+ --btn-font-family: inherit;
+ --btn-font-size: 1rem;
+ --btn-font-weight: 400;
+ --btn-line-height: 1.5;
+ --btn-color: var(--white);
+ --btn-bg: var(--body-bg);
+ --btn-border-width: 1px;
+ --btn-border-color: transparent;
+ --btn-border-radius: 0.25rem;
+ --btn-active-border-color: transparent;
+ --btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+ --btn-disabled-opacity: 0.65;
+ --btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--btn-focus-shadow-rgb), .5);
+ display: inline-block;
+ padding: var(--btn-padding-y) var(--btn-padding-x);
+ font-family: var(--btn-font-family);
+ font-size: var(--btn-font-size);
+ font-weight: var(--btn-font-weight);
+ line-height: var(--btn-line-height);
+ color: var(--btn-color);
+ text-align: center;
+ text-decoration: none;
+ vertical-align: middle;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ border: var(--btn-border-width) solid var(--btn-border-color);
+ border-radius: var(--btn-border-radius);
+ background-color: var(--btn-bg);
+ -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+ -o-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+}
+
+/* Buttons — inherit brand hues; ensure strong contrast on dark bg */
+.btn-primary {
+ --btn-color: hsl(0, 0%, 100%);
+ --btn-bg: hsl(240, 98%, 17%);
+ --btn-border-color: hsl(240, 98%, 17%);
+ --btn-hover-color: hsl(0, 0%, 100%);
+ --btn-hover-bg: #010149;
+ --btn-hover-border-color: #010145;
+ --btn-focus-shadow-rgb: 84, 114, 255;
+ --btn-active-color: hsl(0, 0%, 100%);
+ --btn-active-bg: #010145;
+ --btn-active-border-color: #010141;
+}
+
+.btn-secondary {
+ --btn-color: var(--nav-text-color);
+ --btn-bg: var(--nav-bg-color);
+ --btn-border-color: #3a4250;
+ --btn-hover-color: #fff;
+ --btn-hover-bg: #1b2a55;
+ --btn-hover-border-color: #162448;
+ --btn-focus-shadow-rgb: 84, 114, 255;
+ --btn-active-color: #fff;
+ --btn-active-bg: #162448;
+ --btn-active-border-color: #12203f;
+}
+
+.btn-success {
+ --btn-color: hsl(0, 0%, 100%);
+ --btn-bg: hsl(120, 35%, 45%);
+ --btn-border-color: hsl(120, 35%, 45%);
+ --btn-hover-color: hsl(0, 0%, 100%);
+ --btn-hover-bg: hsl(120, 35%, 40%);
+ --btn-hover-border-color: hsl(120, 35%, 38%);
+ --btn-focus-shadow-rgb: 96, 180, 96;
+ --btn-active-color: hsl(0, 0%, 100%);
+ --btn-active-bg: hsl(120, 35%, 38%);
+ --btn-active-border-color: hsl(120, 35%, 36%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(0, 0%, 100%);
+ --btn-disabled-bg: hsl(120, 35%, 45%);
+ --btn-disabled-border-color: hsl(120, 35%, 45%);
+}
+
+.btn-info {
+ --btn-color: hsl(0, 0%, 100%);
+ --btn-bg: hsl(207, 55%, 55%);
+ --btn-border-color: hsl(207, 55%, 55%);
+ --btn-hover-color: hsl(0, 0%, 100%);
+ --btn-hover-bg: hsl(207, 55%, 50%);
+ --btn-hover-border-color: hsl(207, 55%, 48%);
+ --btn-focus-shadow-rgb: 100, 160, 210);
+ --btn-active-color: hsl(0, 0%, 100%);
+ --btn-active-bg: hsl(207, 55%, 48%);
+ --btn-active-border-color: hsl(207, 55%, 46%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(0, 0%, 100%);
+ --btn-disabled-bg: hsl(207, 55%, 55%);
+ --btn-disabled-border-color: hsl(207, 55%, 55%);
+}
+
+.btn-warning {
+ --btn-color: hsl(0, 0%, 0%);
+ --btn-bg: hsl(38, 100%, 50%);
+ --btn-border-color: hsl(38, 100%, 50%);
+ --btn-hover-color: hsl(0, 0%, 0%);
+ --btn-hover-bg: hsl(38, 100%, 45%);
+ --btn-hover-border-color: hsl(38, 100%, 43%);
+ --btn-focus-shadow-rgb: 220, 170, 40;
+ --btn-active-color: hsl(0, 0%, 0%);
+ --btn-active-bg: hsl(38, 100%, 43%);
+ --btn-active-border-color: hsl(38, 100%, 41%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(0, 0%, 0%);
+ --btn-disabled-bg: hsl(38, 100%, 50%);
+ --btn-disabled-border-color: hsl(38, 100%, 50%);
+}
+
+.btn-danger {
+ --btn-color: hsl(0, 0%, 100%);
+ --btn-bg: hsl(3, 82%, 50%);
+ --btn-border-color: hsl(3, 82%, 50%);
+ --btn-hover-color: hsl(0, 0%, 100%);
+ --btn-hover-bg: hsl(3, 82%, 45%);
+ --btn-hover-border-color: hsl(3, 82%, 43%);
+ --btn-focus-shadow-rgb: 220, 80, 80;
+ --btn-active-color: hsl(0, 0%, 100%);
+ --btn-active-bg: hsl(3, 82%, 43%);
+ --btn-active-border-color: hsl(3, 82%, 41%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(0, 0%, 100%);
+ --btn-disabled-bg: hsl(3, 82%, 50%);
+ --btn-disabled-border-color: hsl(3, 82%, 50%);
+}
+
+.btn-light {
+ --btn-color: hsl(0, 0%, 0%);
+ --btn-bg: hsl(210, 17%, 85%);
+ --btn-border-color: hsl(210, 17%, 85%);
+ --btn-hover-color: hsl(0, 0%, 0%);
+ --btn-hover-bg: hsl(210, 17%, 80%);
+ --btn-hover-border-color: hsl(210, 17%, 78%);
+ --btn-focus-shadow-rgb: 200, 205, 210;
+ --btn-active-color: hsl(0, 0%, 0%);
+ --btn-active-bg: hsl(210, 17%, 78%);
+ --btn-active-border-color: hsl(210, 17%, 76%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(0, 0%, 0%);
+ --btn-disabled-bg: hsl(210, 17%, 85%);
+ --btn-disabled-border-color: hsl(210, 17%, 85%);
+}
+
+.btn-dark {
+ --btn-color: hsl(0, 0%, 100%);
+ --btn-bg: hsl(210, 10%, 20%);
+ --btn-border-color: hsl(210, 10%, 20%);
+ --btn-hover-color: hsl(0, 0%, 100%);
+ --btn-hover-bg: hsl(210, 10%, 18%);
+ --btn-hover-border-color: hsl(210, 10%, 16%);
+ --btn-focus-shadow-rgb: 60, 65, 70;
+ --btn-active-color: hsl(0, 0%, 100%);
+ --btn-active-bg: hsl(210, 10%, 16%);
+ --btn-active-border-color: hsl(210, 10%, 14%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(0, 0%, 100%);
+ --btn-disabled-bg: hsl(210, 10%, 20%);
+ --btn-disabled-border-color: hsl(210, 10%, 20%);
+}
+
+.btn-outline-primary {
+ --btn-color: hsl(240, 98%, 40%);
+ --btn-border-color: hsl(240, 98%, 40%);
+ --btn-hover-color: hsl(0, 0%, 100%);
+ --btn-hover-bg: hsl(240, 98%, 40%);
+ --btn-hover-border-color: hsl(240, 98%, 40%);
+ --btn-focus-shadow-rgb: 80, 80, 180;
+ --btn-active-color: hsl(0, 0%, 100%);
+ --btn-active-bg: hsl(240, 98%, 40%);
+ --btn-active-border-color: hsl(240, 98%, 40%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(240, 98%, 40%);
+ --btn-disabled-bg: transparent;
+ --btn-disabled-border-color: hsl(240, 98%, 40%);
+ --gradient: none;
+}
+
+.btn-outline-secondary {
+ --btn-color: hsl(210, 20%, 60%);
+ --btn-border-color: hsl(210, 20%, 60%);
+ --btn-hover-color: hsl(0, 0%, 0%);
+ --btn-hover-bg: hsl(210, 20%, 60%);
+ --btn-hover-border-color: hsl(210, 20%, 60%);
+ --btn-focus-shadow-rgb: 120, 140, 160;
+ --btn-active-color: hsl(0, 0%, 0%);
+ --btn-active-bg: hsl(210, 20%, 60%);
+ --btn-active-border-color: hsl(210, 20%, 60%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(210, 20%, 60%);
+ --btn-disabled-bg: transparent;
+ --btn-disabled-border-color: hsl(210, 20%, 60%);
+ --gradient: none;
+}
+
+.btn-outline-success {
+ --btn-color: hsl(120, 35%, 55%);
+ --btn-border-color: hsl(120, 35%, 55%);
+ --btn-hover-color: hsl(0, 0%, 0%);
+ --btn-hover-bg: hsl(120, 35%, 55%);
+ --btn-hover-border-color: hsl(120, 35%, 55%);
+ --btn-focus-shadow-rgb: 100, 190, 100;
+ --btn-active-color: hsl(0, 0%, 0%);
+ --btn-active-bg: hsl(120, 35%, 55%);
+ --btn-active-border-color: hsl(120, 35%, 55%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(120, 35%, 55%);
+ --btn-disabled-bg: transparent;
+ --btn-disabled-border-color: hsl(120, 35%, 55%);
+ --gradient: none;
+}
+
+.btn-outline-info {
+ --btn-color: hsl(207, 55%, 65%);
+ --btn-border-color: hsl(207, 55%, 65%);
+ --btn-hover-color: hsl(0, 0%, 0%);
+ --btn-hover-bg: hsl(207, 55%, 65%);
+ --btn-hover-border-color: hsl(207, 55%, 65%);
+ --btn-focus-shadow-rgb: 110, 170, 220;
+ --btn-active-color: hsl(0, 0%, 0%);
+ --btn-active-bg: hsl(207, 55%, 65%);
+ --btn-active-border-color: hsl(207, 55%, 65%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(207, 55%, 65%);
+ --btn-disabled-bg: transparent;
+ --btn-disabled-border-color: hsl(207, 55%, 65%);
+ --gradient: none;
+}
+
+.btn-outline-warning {
+ --btn-color: hsl(38, 100%, 60%);
+ --btn-border-color: hsl(38, 100%, 60%);
+ --btn-hover-color: hsl(0, 0%, 0%);
+ --btn-hover-bg: hsl(38, 100%, 60%);
+ --btn-hover-border-color: hsl(38, 100%, 60%);
+ --btn-focus-shadow-rgb: 240, 190, 70;
+ --btn-active-color: hsl(0, 0%, 0%);
+ --btn-active-bg: hsl(38, 100%, 60%);
+ --btn-active-border-color: hsl(38, 100%, 60%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(38, 100%, 60%);
+ --btn-disabled-bg: transparent;
+ --btn-disabled-border-color: hsl(38, 100%, 60%);
+ --gradient: none;
+}
+
+.btn-outline-danger {
+ --btn-color: hsl(3, 82%, 60%);
+ --btn-border-color: hsl(3, 82%, 60%);
+ --btn-hover-color: hsl(0, 0%, 0%);
+ --btn-hover-bg: hsl(3, 82%, 60%);
+ --btn-hover-border-color: hsl(3, 82%, 60%);
+ --btn-focus-shadow-rgb: 240, 100, 100;
+ --btn-active-color: hsl(0, 0%, 0%);
+ --btn-active-bg: hsl(3, 82%, 60%);
+ --btn-active-border-color: hsl(3, 82%, 60%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(3, 82%, 60%);
+ --btn-disabled-bg: transparent;
+ --btn-disabled-border-color: hsl(3, 82%, 60%);
+ --gradient: none;
+}
+
+/* Outline buttons on dark: keep readable borders */
+.btn-outline-light {
+ --btn-color: #e6ebf1;
+ --btn-border-color: #e6ebf1;
+ --btn-hover-color: #111;
+ --btn-hover-bg: #e6ebf1;
+ --btn-hover-border-color: #e6ebf1;
+ --btn-active-color: #111;
+ --btn-active-bg: #d7dce2;
+ --btn-active-border-color: #d7dce2;
+ --gradient: none;
+}
+
+.btn-outline-dark {
+ --btn-color: hsl(210, 10%, 30%);
+ --btn-border-color: hsl(210, 10%, 30%);
+ --btn-hover-color: hsl(0, 0%, 100%);
+ --btn-hover-bg: hsl(210, 10%, 30%);
+ --btn-hover-border-color: hsl(210, 10%, 30%);
+ --btn-focus-shadow-rgb: 70, 75, 80;
+ --btn-active-color: hsl(0, 0%, 100%);
+ --btn-active-bg: hsl(210, 10%, 30%);
+ --btn-active-border-color: hsl(210, 10%, 30%);
+ --btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+ --btn-disabled-color: hsl(210, 10%, 30%);
+ --btn-disabled-bg: transparent;
+ --btn-disabled-border-color: hsl(210, 10%, 30%);
+ --gradient: none;
+}
+
+/* Links as buttons */
+.btn-link {
+ --btn-font-weight: 400;
+ --btn-color: var(--color-link);
+ --btn-bg: transparent;
+ --btn-border-color: transparent;
+ --btn-hover-color: var(--link-hover-color);
+ --btn-hover-border-color: transparent;
+ --btn-active-color: var(--link-hover-color);
+ --btn-active-border-color: transparent;
+ --btn-disabled-color: #6d7781;
+ --btn-disabled-border-color: transparent;
+ --btn-box-shadow: none;
+ --btn-focus-shadow-rgb: 84, 114, 255;
+ text-decoration: underline;
+}
diff --git a/src/media/css/theme/light.custom.css b/src/media/css/theme/light.custom.css
new file mode 100644
index 0000000..53dc8c4
--- /dev/null
+++ b/src/media/css/theme/light.custom.css
@@ -0,0 +1,1152 @@
+@charset "UTF-8";
+/* Copyright (C) 2025 Moko Consulting
+
+ This file is part of a Moko Consulting project.
+
+ SPDX-License-Identifier: GPL-3.0-or-later
+
+/* -----------------------------------------------
+ * LIGHT CUSTOM THEME — Warm Earth
+ * --------------------------------------------- */
+
+:root[data-bs-theme="light"] {
+color-scheme: light;
+
+/* ===== BRAND & THEME COLORS ===== */
+--color-primary: #8B4513;
+--accent-color-primary: #D2691E;
+--accent-color-secondary: #CD853F;
+
+/* ===== NAVIGATION ===== */
+--mainmenu-nav-link-color: white;
+--nav-text-color: white;
+--nav-bg-color: #5C3317;
+
+/* ===== LINKS ===== */
+--color-link: #8B4513;
+--color-hover: var(--accent-color-primary);
+--link-color: #8B4513;
+--link-color-rgb: 139, 69, 19;
+--link-decoration: underline;
+--link-hover-color: #D2691E;
+--link-hover-color-rgb: 210, 105, 30;
+--link-active-color: var(--link-color);
+
+/* ===== NAVBAR ===== */
+--navbar-padding-x: 1rem;
+--navbar-padding-y: 0.5rem;
+--navbar-color: var(--nav-text-color);
+--navbar-active-color: var(--mainmenu-nav-link-color);
+--navbar-disabled-color: #6c757d;
+--navbar-brand-padding-y: 0.3125rem;
+--navbar-brand-margin-end: 1rem;
+--navbar-brand-font-size: 1.25rem;
+--navbar-brand-color: var(--nav-text-color);
+--navbar-brand-active-color: var(--mainmenu-nav-link-color);
+--navbar-nav-link-padding-x: 0.5rem;
+--navbar-toggler-padding-y: 0.25rem;
+--navbar-toggler-padding-x: 0.75rem;
+--navbar-toggler-font-size: 1.25rem;
+--navbar-toggler-border-color: rgba(0, 0, 0, 0.1);
+--navbar-toggler-border-radius: 0.25rem;
+--navbar-toggler-focus-width: 0.25rem;
+--navbar-toggler-transition: box-shadow 0.15s ease-in-out;
+--nav-link-padding-x: 1rem;
+--nav-link-padding-y: 0.5rem;
+--nav-link-font-weight: 400;
+--nav-link-color: var(--nav-text-color);
+--nav-link-active-color: var(--mainmenu-nav-link-color);
+--nav-link-disabled-color: #6c757d;
+
+/* ===== TYPOGRAPHY & BODY ===== */
+--font-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+--font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+--body-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
+--body-font-size: 1rem;
+--body-font-weight: 400;
+--body-line-height: 1.5;
+--body-color: #22262a;
+--body-color-rgb: 34, 38, 42;
+--body-bg: #fff;
+--body-bg-rgb: 255, 255, 255;
+--heading-color: inherit;
+--emphasis-color: #000;
+--emphasis-color-rgb: 0, 0, 0;
+--secondary-color: #22262abf;
+--secondary-color-rgb: 34, 38, 42;
+--tertiary-color: #22262a80;
+--tertiary-color-rgb: 34, 38, 42;
+--muted-color: #6d757e;
+--code-color: black;
+--code-color-ink: var(--code-color, #e93f8e);
+--code-bg-color: lightgreen;
+--highlight-color: #22262a;
+--highlight-bg: #fbeea8;
+
+/* ===== LAYOUT & SPACING ===== */
+--padding-x: 0.15rem;
+--padding-y: 0.15rem;
+--bg-opacity: 1;
+--nav-toggle-size: 3rem;
+--gradient: linear-gradient(180deg, #ffffff26, #fff0);
+--secondary-bg: #eaedf0;
+--secondary-bg-rgb: 234, 237, 240;
+--tertiary-bg: #f9fafb;
+--tertiary-bg-rgb: 249, 250, 251;
+--hr-color: var(--border-color, #dfe3e7);
+--border-color-soft: var(--border-color, #dfe3e7);
+--kbd-bg: var(--secondary-bg, #eaedf0);
+--kbd-ink: var(--body-bg, #fff);
+--toc-bg: var(--secondary-bg, #eaedf0);
+--toc-ink: var(--color-primary, #112855);
+--selection-bg: var(--highlight-bg, #fbeea8);
+--selection-ink: var(--body-color, #22262a);
+--border: 5px;
+
+/* ===== BREAKPOINTS ===== */
+--bp-xs: 0;
+--bp-sm: 576px;
+--bp-md: 768px;
+--bp-lg: 992px;
+--bp-xl: 1200px;
+
+/* ===== BOOTSTRAP PALETTE ===== */
+--primary: #010156;
+--secondary: #6d757e;
+--success: #448344;
+--info: #30638d;
+--warning: #ad6200;
+--danger: #a51f18;
+--light: #f9fafb;
+--dark: #353b41;
+--primary-rgb: 1, 1, 86;
+--secondary-rgb: 109, 117, 126;
+--success-rgb: 68, 131, 68;
+--info-rgb: 48, 99, 141;
+--warning-rgb: 173, 98, 0;
+--danger-rgb: 165, 31, 24;
+--light-rgb: 249, 250, 251;
+--dark-rgb: 53, 59, 65;
+--primary-text-emphasis: #002;
+--secondary-text-emphasis: #2c2f32;
+--success-text-emphasis: #1b351b;
+--info-text-emphasis: #132838;
+--warning-text-emphasis: #452700;
+--danger-text-emphasis: #420c09;
+--light-text-emphasis: #484f56;
+--dark-text-emphasis: #484f56;
+--primary-bg-subtle: #ccd;
+--secondary-bg-subtle: #e2e3e5;
+--success-bg-subtle: #dae6da;
+--info-bg-subtle: #d6e0e8;
+--warning-bg-subtle: #efe0cc;
+--danger-bg-subtle: #edd2d1;
+--light-bg-subtle: #fcfcfd;
+--dark-bg-subtle: #ced4da;
+--primary-border-subtle: #99b;
+--secondary-border-subtle: #c5c8cb;
+--success-border-subtle: #b4ceb4;
+--info-border-subtle: #acc1d1;
+--warning-border-subtle: #dec099;
+--danger-border-subtle: #dba5a2;
+--light-border-subtle: #eaedf0;
+--dark-border-subtle: #adb5bd;
+
+/* ===== ALERT LINK COLORS ===== */
+--alert-primary-link-color: #01012a;
+--alert-secondary-link-color: #34383d;
+--alert-success-link-color: #213f21;
+--alert-info-link-color: #172f44;
+--alert-warning-link-color: #532f00;
+--alert-danger-link-color: #4f0f0b;
+--alert-light-link-color: #505050;
+--alert-dark-link-color: #1a1c1f;
+
+/* ===== LIST GROUP ITEM COLORS ===== */
+--list-group-item-primary-color: #010134;
+--list-group-item-primary-bg: #ccccdd;
+--list-group-item-primary-active-bg: #b8b8c7;
+--list-group-item-secondary-color: #41464c;
+--list-group-item-secondary-bg: #e2e3e5;
+--list-group-item-secondary-active-bg: #cbccce;
+--list-group-item-success-color: #294f29;
+--list-group-item-success-bg: #dae6da;
+--list-group-item-success-active-bg: #c4cfc4;
+--list-group-item-info-color: #1d3b55;
+--list-group-item-info-bg: #d6e0e8;
+--list-group-item-info-active-bg: #c1cad1;
+--list-group-item-warning-color: #683b00;
+--list-group-item-warning-bg: #efe0cc;
+--list-group-item-warning-active-bg: #d7cab8;
+--list-group-item-danger-color: #63130e;
+--list-group-item-danger-bg: #edd2d1;
+--list-group-item-danger-active-bg: #d5bdbc;
+--list-group-item-light-color: #646464;
+--list-group-item-light-bg: #fefefe;
+--list-group-item-light-active-bg: #e5e5e5;
+--list-group-item-dark-color: #202327;
+--list-group-item-dark-bg: #d7d8d9;
+--list-group-item-dark-active-bg: #c2c2c3;
+
+/* ===== LINK UTILITY COLORS ===== */
+--link-primary-color: hsl(240, 98%, 17%);
+--link-primary-hover-color: #010145;
+--link-secondary-color: hsl(210, 7%, 46%);
+--link-secondary-hover-color: #575e65;
+--link-success-color: hsl(120, 32%, 39%);
+--link-success-hover-color: #366936;
+--link-info-color: hsl(207, 49%, 37%);
+--link-info-hover-color: #264f71;
+--link-warning-color: hsl(34, 100%, 34%);
+--link-warning-hover-color: #8a4e00;
+--link-danger-color: hsl(3, 75%, 37%);
+--link-danger-hover-color: #841913;
+--link-light-color: hsl(210, 17%, 98%);
+--link-light-hover-color: #fafbfc;
+--link-dark-color: hsl(210, 10%, 23%);
+--link-dark-hover-color: #2a2f34;
+
+/* ===== COMPONENT-SPECIFIC COLORS ===== */
+--mod-finder-link-hover: #e6e6e6;
+--form-legend-color: #495057;
+--border-gray: #b2bfcd;
+--subhead-color: #495057;
+--box-shadow-gray: #ddd;
+--btn-active-text-gray: #A0A0A0;
+--indicator-success-bg: var(--success);
+--item-list-color: #F5F5F5;
+--notification-badge-bg: var(--danger);
+--content-bg-gray: #DDD;
+--taba-btn-green: #7ac143;
+--taba-btn-blue: #5091cd;
+--taba-btn-red: #f44321;
+--taba-btn-gray: #AAA;
+--taba-msg-bg: #f5f5f5;
+--toc-link-color: #767676;
+--toc-link-active-color: #563d7c;
+--choices-disabled-bg: #eaeaea;
+--choices-input-bg: var(--white);
+--choices-border-light: #ddd;
+--choices-arrow-color: #333;
+--choices-inner-bg: #f9f9f9;
+--choices-focused-border: #b7b7b7;
+--choices-dropdown-bg: var(--white);
+--choices-item-bg: #00bcd4;
+--choices-item-border: #00a5bb;
+--choices-item-hover-bg: #00a5bb;
+--choices-item-hover-border: #008fa1;
+--choices-item-disabled-bg: #aaaaaa;
+--choices-item-disabled-border: #919191;
+--choices-item-highlighted: #f2f2f2;
+--choices-input-inner-bg: #f9f9f9;
+
+/* ===== STANDARD COLORS ===== */
+--blue: #010156;
+--indigo: #6812f3;
+--purple: #6f42c2;
+--pink: #e93f8e;
+--red: #a51f18;
+--orange: #fd7e17;
+--yellow: #ad6200;
+--green: #448344;
+--teal: #5abfdd;
+--cyan: #30638d;
+--black: #000;
+--white: #fff;
+
+/* ===== GRAY SCALE ===== */
+--gray-100: #f9fafb;
+--gray-200: #eaedf0;
+--gray-300: #dfe3e7;
+--gray-400: #ced4da;
+--gray-500: #adb5bd;
+--gray-600: #6d757e;
+--gray-700: #484f56;
+--gray-800: #353b41;
+--gray-900: #22262a;
+--white-rgb: 255, 255, 255;
+--black-rgb: 0, 0, 0;
+
+/* ===== OPACITY UTILITIES ===== */
+--opacity-0: 0;
+--opacity-5: 0.05;
+--opacity-10: 0.1;
+--opacity-15: 0.15;
+--opacity-20: 0.2;
+--opacity-25: 0.25;
+--opacity-30: 0.3;
+--opacity-50: 0.5;
+--opacity-75: 0.75;
+--opacity-100: 1;
+
+/* ===== COMMON SHADOW COLORS ===== */
+--shadow-color-light: rgba(var(--black-rgb), var(--opacity-15));
+--shadow-color-medium: rgba(var(--black-rgb), var(--opacity-25));
+--shadow-color-dark: rgba(var(--black-rgb), var(--opacity-30));
+--border-color-translucent: rgba(var(--black-rgb), var(--opacity-10));
+--highlight-translucent: rgba(var(--white-rgb), var(--opacity-15));
+
+/* ===== HEADER BACKGROUND ===== */
+--header-background-image: url('../../../../../../media/templates/site/mokocassiopeia/images/bg.svg');
+--header-background-attachment: fixed;
+--header-background-repeat: repeat;
+--header-background-size: auto;
+
+/* ===== CONTAINER BACKGROUNDS ===== */
+/* Below Topbar Container */
+--container-below-topbar-bg-image: none;
+--container-below-topbar-bg-color: transparent;
+--container-below-topbar-bg-position: auto;
+--container-below-topbar-bg-attachment: fixed;
+--container-below-topbar-bg-repeat: repeat;
+--container-below-topbar-bg-size: auto;
+--container-below-topbar-border: none;
+--container-below-topbar-border-radius: 0;
+
+/* Top A Container */
+--container-top-a-bg-image: none;
+--container-top-a-bg-color: transparent;
+--container-top-a-bg-position: auto;
+--container-top-a-bg-attachment: fixed;
+--container-top-a-bg-repeat: repeat;
+--container-top-a-bg-size: auto;
+--container-top-a-border: none;
+--container-top-a-border-radius: 0;
+
+/* Top B Container */
+--container-top-b-bg-image: none;
+--container-top-b-bg-color: transparent;
+--container-top-b-bg-position: auto;
+--container-top-b-bg-attachment: fixed;
+--container-top-b-bg-repeat: repeat;
+--container-top-b-bg-size: auto;
+--container-top-b-border: none;
+--container-top-b-border-radius: 0;
+
+/* TOC Container */
+--container-toc-bg: var(--mainmenu-nav-link-color);
+--container-toc-color: var(--color-primary);
+
+/* Sidebar Container */
+--container-sidebar-bg-image: none;
+--container-sidebar-bg-color: transparent;
+--container-sidebar-bg-position: auto;
+--container-sidebar-bg-attachment: scroll;
+--container-sidebar-bg-repeat: repeat;
+--container-sidebar-bg-size: auto;
+--container-sidebar-border: none;
+--container-sidebar-border-radius: 0;
+
+/* Bottom A Container */
+--container-bottom-a-bg-image: none;
+--container-bottom-a-bg-color: transparent;
+--container-bottom-a-bg-position: auto;
+--container-bottom-a-bg-attachment: fixed;
+--container-bottom-a-bg-repeat: repeat;
+--container-bottom-a-bg-size: auto;
+--container-bottom-a-border: none;
+--container-bottom-a-border-radius: 0;
+
+/* Bottom B Container */
+--container-bottom-b-bg-image: none;
+--container-bottom-b-bg-color: transparent;
+--container-bottom-b-bg-position: auto;
+--container-bottom-b-bg-attachment: fixed;
+--container-bottom-b-bg-repeat: repeat;
+--container-bottom-b-bg-size: auto;
+--container-bottom-b-border: none;
+--container-bottom-b-border-radius: 0;
+
+/* ===== BORDERS ===== */
+--border-width: 1px;
+--border-style: solid;
+--border-color: #dfe3e7;
+--border-color-translucent: #0000002d;
+--border-radius: .25rem;
+--border-radius-sm: .2rem;
+--border-radius-lg: .3rem;
+--border-radius-xl: .3rem;
+--border-radius-xxl: 2rem;
+--border-radius-2xl: var(--border-radius-xxl)*2;
+--border-radius-pill: 50rem;
+
+/* ===== SHADOWS ===== */
+--box-shadow: 0 .5rem 1rem #00000026;
+--box-shadow-sm: 0 .125rem .25rem #00000013;
+--box-shadow-lg: 0 1rem 3rem #0000002d;
+--box-shadow-inset: inset 0 1px 2px #00000013;
+
+/* ===== FOCUS & FORMS ===== */
+--focus-ring-width: .25rem;
+--focus-ring-opacity: .25;
+--focus-ring-color: #01015640;
+--input-color: hsl(210, 11%, 15%);
+--input-bg: hsl(210, 20%, 98%);
+--input-border-color: hsl(210, 14%, 83%);
+--input-focus-border-color: #8894aa;
+--input-focus-box-shadow: 0 0 0 0.25rem rgba(1, 1, 86, 0.25);
+--input-placeholder-color: hsl(210, 7%, 46%);
+--input-disabled-bg: hsl(210, 16%, 93%);
+--input-disabled-border-color: hsl(210, 14%, 83%);
+--input-file-button-active-bg: #dee1e4;
+--form-range-thumb-active-bg: #b8bfcc;
+--form-valid-color: #448344;
+--form-valid-border-color: #448344;
+--form-invalid-color: #a51f18;
+--form-invalid-border-color: #a51f18;
+
+/* ===== BUTTONS ===== */
+--btn-border-radius: var(--border-radius);
+--btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+
+/* ===== CARDS ===== */
+--card-spacer-y: 1rem;
+--card-spacer-x: 1rem;
+--card-title-spacer-y: 0.5rem;
+--card-border-width: 1px;
+--card-border-color: var(--border-color);
+--card-border-radius: var(--border-radius);
+--card-box-shadow: none;
+--card-inner-border-radius: calc(var(--border-radius) - 1px);
+--card-cap-padding-y: 0.5rem;
+--card-cap-padding-x: 1rem;
+--card-cap-bg: rgba(0, 0, 0, 0.03);
+--card-cap-color: var(--body-color);
+--card-height: auto;
+--card-color: var(--body-color);
+--card-bg: var(--body-bg);
+--card-img-overlay-padding: 1rem;
+--card-group-margin: 0.75rem;
+
+/* ===== VIRTUEMART (VM) ===== */
+/* VM Surfaces */
+--vm-surface: #ffffff;
+--vm-surface-2: #f8f9fa;
+--vm-text: var(--body-color);
+--vm-text-strong: #000000;
+--vm-text-muted: #6c757d;
+--vm-border: var(--border-color);
+--vm-price-color: var(--success);
+
+/* VM Layout and Density */
+--vm-container-max-width: 1200px;
+--vm-section-gap: 2rem;
+--vm-block-radius: var(--border-radius);
+--vm-block-shadow: var(--box-shadow-sm);
+
+/* VM Typography */
+--vm-category-title-size: 2rem;
+--vm-subcategory-title-size: 1.5rem;
+--vm-page-title-size: 1.75rem;
+--vm-products-type-title-size: 1.25rem;
+--vm-product-title-size: 1.125rem;
+--vm-product-title-weight: 500;
+--vm-products-type-title-weight: 600;
+--vm-price-size: 1.5rem;
+--vm-price-detail-size: 1.125rem;
+--vm-price-desc-size: 0.875rem;
+
+/* VM Controls */
+--vm-input-radius: var(--border-radius);
+--vm-input-shadow: var(--box-shadow-sm);
+--vm-qty-width: 80px;
+--vm-cart-dropdown-min-width: 300px;
+
+/* VM Alerts */
+--vm-alert-radius: var(--border-radius);
+--vm-alert-shadow: var(--box-shadow-sm);
+--vm-availability-bg: var(--success-bg-subtle);
+--vm-availability-text: var(--success);
+
+/* VM Buttons */
+--vm-btn-padding-x: 1rem;
+--vm-btn-padding-y: 0.5rem;
+--vm-btn-radius: var(--border-radius);
+--vm-btn-shadow: var(--box-shadow-sm);
+--vm-btn-primary-bg: var(--primary);
+--vm-btn-primary-text: #ffffff;
+--vm-btn-primary-border: var(--primary);
+--vm-btn-secondary-bg: var(--secondary);
+--vm-btn-secondary-text: #ffffff;
+--vm-btn-secondary-border: var(--secondary);
+
+/* VM Image Overlay Controls */
+--vm-image-overlay-gap-x: 0.5rem;
+--vm-image-overlay-gap-y: 0.5rem;
+--vm-image-overlay-raise: 0.25rem;
+--vm-image-overlay-btn-size: 2.5rem;
+--vm-image-overlay-btn-radius: 50%;
+--vm-image-overlay-btn-bg: rgba(255, 255, 255, 0.9);
+--vm-image-overlay-btn-bg-hover: rgba(255, 255, 255, 1);
+--vm-image-overlay-btn-border-color: rgba(0, 0, 0, 0.1);
+--vm-image-overlay-btn-border-width: 1px;
+--vm-image-overlay-btn-color: var(--body-color);
+--vm-image-overlay-btn-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+/* VM Vendor Menu */
+--vm-vendor-menu-bg: var(--body-bg);
+--vm-vendor-menu-border: var(--border-color);
+--vm-vendor-menu-radius: var(--border-radius);
+--vm-vendor-menu-shadow: var(--box-shadow-sm);
+--vm-vendor-menu-item-gap: 0.25rem;
+--vm-vendor-menu-item-padding-x: 1rem;
+--vm-vendor-menu-item-padding-y: 0.5rem;
+--vm-vendor-menu-pill-radius: 50rem;
+--vm-vendor-menu-link: var(--link-color);
+--vm-vendor-menu-link-hover: var(--link-hover-color);
+--vm-vendor-menu-link-active: var(--primary);
+--vm-vendor-menu-hover-bg: var(--secondary-bg);
+
+/* ===== GABLE ===== */
+--gab-blue: #0066cc;
+--gab-green: #28a745;
+--gab-red: #dc3545;
+--gab-orange: #fd7e14;
+--gab-gray1: #495057;
+--gab-gray2: #6c757d;
+--gab-gray3: #adb5bd;
+
+/* ===== HERO / BANNER OVERLAY ===== */
+--hero-height: 70vh;
+--hero-color: var(--body-color);
+--hero-bg-repeat: no-repeat;
+--hero-bg-attachment: fixed;
+--hero-bg-position: top center;
+--hero-bg-size: cover;
+--hero-border-bottom: solid var(--accent-color-secondary);
+--hero-overlay-bg: hsla(0, 0%, 0%, 0.1);
+--hero-overlay-bg-position: center;
+--hero-overlay-bg-size: cover;
+--hero-overlay-padding: 1em;
+--hero-overlay-text-align: center;
+--hero-overlay-text-color: var(--body-color);
+
+/* ===== 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;
+
+/* ===== HERO CARD (inner .hero element) ===== */
+/* Default card — uses primary variant values */
+--hero-card-bg: var(--hero-primary-bg-color);
+--hero-card-color: white;
+--hero-card-overlay: var(--hero-primary-overlay);
+--hero-card-border-radius: .5rem;
+--hero-card-padding-x: 2rem;
+--hero-card-padding-y: 3rem;
+--hero-card-max-width: 800px;
+
+/* Alternative card — uses secondary variant values */
+--hero-alt-card-bg: var(--hero-secondary-bg-color);
+--hero-alt-card-color: var(--hero-secondary-color);
+--hero-alt-card-overlay: var(--hero-secondary-overlay);
+--hero-alt-card-border-radius: .5rem;
+--hero-alt-card-padding-x: 2rem;
+--hero-alt-card-padding-y: 3rem;
+--hero-alt-card-max-width: 600px;
+
+/* ===== 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;
+
+/* ===== OFFCANVAS ===== */
+--offcanvas-zindex: 1045;
+--offcanvas-width: 400px;
+--offcanvas-height: 30vh;
+--offcanvas-padding-x: 1rem;
+--offcanvas-padding-y: 1rem;
+--offcanvas-color: var(--body-color);
+--offcanvas-bg: var(--body-bg);
+--offcanvas-border-width: 1px;
+--offcanvas-border-color: var(--border-color-translucent);
+--offcanvas-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+
+/* ===== ACCORDION ===== */
+--accordion-color: hsl(210, 11%, 15%);
+--accordion-bg: var(--body-bg);
+--accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;
+--accordion-border-color: var(--border-color);
+--accordion-border-width: 1px;
+--accordion-border-radius: 0.25rem;
+--accordion-inner-border-radius: calc(0.25rem - 1px);
+--accordion-btn-padding-x: 1.25rem;
+--accordion-btn-padding-y: 1rem;
+--accordion-btn-color: hsl(210, 11%, 15%);
+--accordion-btn-bg: var(--accordion-bg);
+--accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='hsl%28210, 11%25, 15%25%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+--accordion-btn-icon-width: 1.25rem;
+--accordion-btn-icon-transform: rotate(-180deg);
+--accordion-btn-icon-transition: transform 0.2s ease-in-out;
+--accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230f244d'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+--accordion-btn-focus-border-color: var(--input-focus-border-color);
+--accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(1, 1, 86, 0.25);
+--accordion-body-padding-x: 1.25rem;
+--accordion-body-padding-y: 1rem;
+--accordion-active-color: #0f244d;
+--accordion-active-bg: #e7eaee;
+
+/* ===== BREADCRUMB ===== */
+--breadcrumb-padding-x: 0;
+--breadcrumb-padding-y: 0;
+--breadcrumb-margin-bottom: 1rem;
+--breadcrumb-bg: ;
+--breadcrumb-border-radius: ;
+--breadcrumb-divider-color: hsl(210, 7%, 46%);
+--breadcrumb-item-padding-x: 0.5rem;
+--breadcrumb-item-active-color: var(--link-color);
+
+/* ===== PAGINATION ===== */
+--pagination-padding-x: 0.75rem;
+--pagination-padding-y: 0.375rem;
+--pagination-font-size: 1rem;
+--pagination-color: var(--link-color);
+--pagination-bg: var(--body-bg);
+--pagination-border-width: 1px;
+--pagination-border-color: hsl(210, 14%, 89%);
+--pagination-border-radius: 0.25rem;
+--pagination-focus-color: var(--link-active-color);
+--pagination-focus-bg: hsl(210, 16%, 93%);
+--pagination-focus-box-shadow: 0 0 0 0.25rem rgba(1, 1, 86, 0.25);
+--pagination-active-color: var(--body-bg);
+--pagination-active-bg: hsl(240, 98%, 17%);
+--pagination-active-border-color: hsl(240, 98%, 17%);
+--pagination-disabled-color: hsl(210, 7%, 46%);
+--pagination-disabled-bg: var(--body-bg);
+--pagination-disabled-border-color: hsl(210, 14%, 89%);
+
+/* ===== BADGE ===== */
+--badge-padding-x: 0.65em;
+--badge-padding-y: 0.35em;
+--badge-font-size: 0.75em;
+--badge-font-weight: 700;
+--badge-color: var(--body-bg);
+--badge-border-radius: 0.25rem;
+
+/* ===== ALERT ===== */
+--alert-bg: transparent;
+--alert-padding-x: 1rem;
+--alert-padding-y: 1rem;
+--alert-margin-bottom: 1rem;
+--alert-color: inherit;
+--alert-border-color: transparent;
+--alert-border: 1px solid var(--alert-border-color);
+--alert-border-radius: 0.25rem;
+
+/* ===== PROGRESS ===== */
+--progress-height: 1rem;
+--progress-font-size: 0.75rem;
+--progress-bg: hsl(210, 16%, 93%);
+--progress-border-radius: 0.25rem;
+--progress-box-shadow: inset 0 1px 2px rgba(var(--black-rgb), 0.075);
+--progress-bar-color: var(--body-bg);
+--progress-bar-bg: hsl(240, 98%, 17%);
+--progress-bar-transition: width 0.6s ease;
+
+/* ===== LIST GROUP ===== */
+--list-group-color: hsl(210, 11%, 15%);
+--list-group-bg: var(--body-bg);
+--list-group-border-color: rgba(var(--black-rgb), 0.125);
+--list-group-border-width: 1px;
+--list-group-border-radius: 0.25rem;
+--list-group-item-padding-x: 1rem;
+--list-group-item-padding-y: 0.5rem;
+--list-group-action-color: hsl(210, 9%, 31%);
+--list-group-action-active-color: hsl(210, 11%, 15%);
+--list-group-action-active-bg: hsl(210, 16%, 93%);
+--list-group-disabled-color: hsl(210, 7%, 46%);
+--list-group-disabled-bg: var(--body-bg);
+--list-group-active-color: var(--body-bg);
+--list-group-active-bg: hsl(240, 98%, 17%);
+--list-group-active-border-color: hsl(240, 98%, 17%);
+
+/* ===== DROPDOWN ===== */
+--dropdown-zindex: 1000;
+--dropdown-min-width: 10rem;
+--dropdown-padding-x: 0;
+--dropdown-padding-y: 0.5rem;
+--dropdown-spacer: 0.125rem;
+--dropdown-font-size: 1rem;
+--dropdown-color: hsl(210, 11%, 15%);
+--dropdown-bg: var(--body-bg);
+--dropdown-border-color: var(--border-color-translucent);
+--dropdown-border-radius: 0.25rem;
+--dropdown-border-width: 1px;
+--dropdown-inner-border-radius: calc(0.25rem - 1px);
+--dropdown-divider-bg: var(--border-color-translucent);
+--dropdown-divider-margin-y: 0.5rem;
+--dropdown-box-shadow: 0 0.5rem 1rem var(--shadow-color-light);
+--dropdown-link-color: hsl(210, 11%, 15%);
+--dropdown-link-active-color: var(--body-bg);
+--dropdown-link-active-bg: hsl(240, 98%, 17%);
+--dropdown-link-disabled-color: hsl(210, 11%, 71%);
+--dropdown-item-padding-x: 1rem;
+--dropdown-item-padding-y: 0.25rem;
+--dropdown-header-color: hsl(210, 7%, 46%);
+--dropdown-header-padding-x: 1rem;
+--dropdown-header-padding-y: 0.5rem;
+
+/* ===== TOAST ===== */
+--toast-zindex: 1090;
+--toast-padding-x: 0.75rem;
+--toast-padding-y: 0.5rem;
+--toast-spacing: 1em;
+--toast-max-width: 350px;
+--toast-font-size: 0.875rem;
+--toast-color: ;
+--toast-bg: rgba(255, 255, 255, 0.85);
+--toast-border-width: 1px;
+--toast-border-color: var(--border-color-translucent);
+--toast-border-radius: 0.25rem;
+--toast-box-shadow: 0 0.5rem 1rem var(--shadow-color-light);
+--toast-header-color: hsl(210, 7%, 46%);
+--toast-header-bg: rgba(var(--white-rgb), 0.85);
+--toast-header-border-color: rgba(var(--black-rgb), var(--opacity-5));
+
+/* ===== MODAL ===== */
+--modal-zindex: 1050;
+--modal-width: 500px;
+--modal-padding: 1rem;
+--modal-margin: 0.5rem;
+--modal-color: ;
+--modal-bg: var(--body-bg);
+--modal-border-color: var(--border-color-translucent);
+--modal-border-width: 1px;
+--modal-border-radius: 0.3rem;
+--modal-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+--modal-inner-border-radius: calc(0.3rem - 1px);
+--modal-header-padding-x: 1rem;
+--modal-header-padding-y: 1rem;
+--modal-header-padding: 1rem 1rem;
+--modal-header-border-color: var(--border-color);
+--modal-header-border-width: 1px;
+--modal-title-line-height: 1.5;
+--modal-footer-gap: 0.5rem;
+--modal-footer-bg: ;
+--modal-footer-border-color: var(--border-color);
+--modal-footer-border-width: 1px;
+
+/* ===== TOOLTIP ===== */
+--tooltip-zindex: 1070;
+--tooltip-max-width: 200px;
+--tooltip-padding-x: 0.5rem;
+--tooltip-padding-y: 0.25rem;
+--tooltip-margin: ;
+--tooltip-font-size: 0.875rem;
+--tooltip-color: var(--body-bg);
+--tooltip-bg: hsl(0, 0%, 0%);
+--tooltip-border-radius: 0.25rem;
+--tooltip-opacity: 0.9;
+--tooltip-arrow-width: 0.8rem;
+--tooltip-arrow-height: 0.4rem;
+
+/* ===== POPOVER ===== */
+--popover-zindex: 1060;
+--popover-max-width: 276px;
+--popover-font-size: 0.875rem;
+--popover-bg: var(--body-bg);
+--popover-border-width: 1px;
+--popover-border-color: var(--border-color-translucent);
+--popover-border-radius: 0.3rem;
+--popover-inner-border-radius: calc(0.3rem - 1px);
+--popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
+--popover-header-padding-x: 1rem;
+--popover-header-padding-y: 0.5rem;
+--popover-header-font-size: 1rem;
+--popover-header-color: ;
+--popover-header-bg: #f0f0f0;
+--popover-body-padding-x: 1rem;
+--popover-body-padding-y: 1rem;
+--popover-body-color: hsl(210, 11%, 15%);
+--popover-arrow-width: 1rem;
+--popover-arrow-height: 0.5rem;
+--popover-arrow-border: var(--popover-border-color);
+
+/* ===== SPINNER ===== */
+--spinner-width: 2rem;
+--spinner-height: 2rem;
+--spinner-vertical-align: -0.125em;
+--spinner-border-width: 0.25em;
+--spinner-animation-speed: 0.75s;
+
+/* ===== NAV TABS ===== */
+--nav-tabs-border-width: 1px;
+--nav-tabs-border-color: hsl(210, 14%, 89%);
+--nav-tabs-border-radius: 0.25rem;
+--nav-tabs-link-active-color: hsl(210, 9%, 31%);
+--nav-tabs-link-active-bg: var(--body-bg);
+--nav-tabs-link-active-border-color: hsl(210, 14%, 89%) hsl(210, 14%, 89%) var(--body-bg);
+
+/* ===== NAV PILLS ===== */
+--nav-pills-border-radius: 0.25rem;
+--nav-pills-link-active-color: var(--body-bg);
+--nav-pills-link-active-bg: hsl(240, 98%, 17%);
+
+/* ===== TABLE ===== */
+--table-color: var(--body-color);
+--table-bg: transparent;
+--table-border-color: var(--border-color);
+--table-accent-bg: transparent;
+--table-striped-color: var(--body-color);
+--table-striped-bg: rgba(var(--black-rgb), var(--opacity-5));
+--table-active-color: var(--body-color);
+--table-active-bg: rgba(var(--black-rgb), 0.075);
+
+/* ===== BACKDROP ===== */
+--backdrop-zindex: 1040;
+--backdrop-bg: hsl(0, 0%, 0%);
+--backdrop-opacity: 0.5;
+}
+
+.btn {
+--btn-padding-x: 1rem;
+--btn-padding-y: 0.6rem;
+--btn-font-family: inherit;
+--btn-font-size: 1rem;
+--btn-font-weight: 400;
+--btn-line-height: 1.5;
+--btn-color: hsl(210, 11%, 15%);
+--btn-bg: var(--body-bg);
+--btn-border-width: 1px;
+--btn-border-color: transparent;
+--btn-border-radius: 0.25rem;
+--btn-active-border-color: transparent;
+--btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
+--btn-disabled-opacity: 0.65;
+--btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--btn-focus-shadow-rgb), .5);
+display: inline-block;
+padding: var(--btn-padding-y) var(--btn-padding-x);
+font-family: var(--btn-font-family);
+font-size: var(--btn-font-size);
+font-weight: var(--btn-font-weight);
+line-height: var(--btn-line-height);
+color: var(--btn-color);
+text-align: center;
+text-decoration: none;
+vertical-align: middle;
+cursor: pointer;
+-webkit-user-select: none;
+-moz-user-select: none;
+-ms-user-select: none;
+user-select: none;
+border: var(--btn-border-width) solid var(--btn-border-color);
+border-radius: var(--btn-border-radius);
+background-color: var(--btn-bg);
+-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+-o-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
+}
+
+.btn-primary {
+--btn-color: hsl(0, 0%, 100%);
+--btn-bg: hsl(240, 98%, 17%);
+--btn-border-color: hsl(240, 98%, 17%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: #010149;
+--btn-hover-border-color: #010145;
+--btn-focus-shadow-rgb: 39, 39, 111;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: #010145;
+--btn-active-border-color: #010141;
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(0, 0%, 100%);
+--btn-disabled-bg: hsl(240, 98%, 17%);
+--btn-disabled-border-color: hsl(240, 98%, 17%);
+}
+
+.btn-secondary {
+--btn-color: var(--body-bg);
+--btn-bg: var(--nav-bg-color);
+--btn-border-color: hsl(210, 7%, 46%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: #5d636b;
+--btn-hover-border-color: #575e65;
+--btn-focus-shadow-rgb: gray;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: #575e65;
+--btn-active-border-color: #52585f;
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(0, 0%, 100%);
+--btn-disabled-bg: hsl(210, 7%, 46%);
+--btn-disabled-border-color: hsl(210, 7%, 46%);
+}
+
+.btn-success {
+--btn-color: hsl(0, 0%, 100%);
+--btn-bg: hsl(120, 32%, 39%);
+--btn-border-color: hsl(120, 32%, 39%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: #3a6f3a;
+--btn-hover-border-color: #366936;
+--btn-focus-shadow-rgb: 96, 150, 96;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: #366936;
+--btn-active-border-color: #336233;
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(0, 0%, 100%);
+--btn-disabled-bg: hsl(120, 32%, 39%);
+--btn-disabled-border-color: hsl(120, 32%, 39%);
+}
+
+.btn-info {
+--btn-color: hsl(0, 0%, 100%);
+--btn-bg: hsl(207, 49%, 37%);
+--btn-border-color: hsl(207, 49%, 37%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: #295478;
+--btn-hover-border-color: #264f71;
+--btn-focus-shadow-rgb: 79, 122, 158;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: #264f71;
+--btn-active-border-color: #244a6a;
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(0, 0%, 100%);
+--btn-disabled-bg: hsl(207, 49%, 37%);
+--btn-disabled-border-color: hsl(207, 49%, 37%);
+}
+
+.btn-warning {
+--btn-color: hsl(0, 0%, 100%);
+--btn-bg: hsl(34, 100%, 34%);
+--btn-border-color: hsl(34, 100%, 34%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: #935300;
+--btn-hover-border-color: #8a4e00;
+--btn-focus-shadow-rgb: 185, 122, 38;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: #8a4e00;
+--btn-active-border-color: #824a00;
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(0, 0%, 100%);
+--btn-disabled-bg: hsl(34, 100%, 34%);
+--btn-disabled-border-color: hsl(34, 100%, 34%);
+}
+
+.btn-danger {
+--btn-color: hsl(0, 0%, 100%);
+--btn-bg: hsl(3, 75%, 37%);
+--btn-border-color: hsl(3, 75%, 37%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: #8c1a14;
+--btn-hover-border-color: #841913;
+--btn-focus-shadow-rgb: 179, 65, 59;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: #841913;
+--btn-active-border-color: #7c1712;
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(0, 0%, 100%);
+--btn-disabled-bg: hsl(3, 75%, 37%);
+--btn-disabled-border-color: hsl(3, 75%, 37%);
+}
+
+.btn-light {
+--btn-color: hsl(0, 0%, 0%);
+--btn-bg: hsl(210, 17%, 98%);
+--btn-border-color: hsl(210, 17%, 98%);
+--btn-hover-color: hsl(0, 0%, 0%);
+--btn-hover-bg: #d4d5d5;
+--btn-hover-border-color: #c7c8c9;
+--btn-focus-shadow-rgb: 212, 213, 213;
+--btn-active-color: hsl(0, 0%, 0%);
+--btn-active-bg: #c7c8c9;
+--btn-active-border-color: #bbbcbc;
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(0, 0%, 0%);
+--btn-disabled-bg: hsl(210, 17%, 98%);
+--btn-disabled-border-color: hsl(210, 17%, 98%);
+}
+
+.btn-dark {
+--btn-color: hsl(0, 0%, 100%);
+--btn-bg: hsl(210, 10%, 23%);
+--btn-border-color: hsl(210, 10%, 23%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: #53585e;
+--btn-hover-border-color: #494f54;
+--btn-focus-shadow-rgb: 83, 88, 94;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: #5d6267;
+--btn-active-border-color: #494f54;
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(0, 0%, 100%);
+--btn-disabled-bg: hsl(210, 10%, 23%);
+--btn-disabled-border-color: hsl(210, 10%, 23%);
+}
+
+.btn-outline-primary {
+--btn-color: hsl(240, 98%, 17%);
+--btn-border-color: hsl(240, 98%, 17%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: hsl(240, 98%, 17%);
+--btn-hover-border-color: hsl(240, 98%, 17%);
+--btn-focus-shadow-rgb: 1, 1, 86;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: hsl(240, 98%, 17%);
+--btn-active-border-color: hsl(240, 98%, 17%);
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(240, 98%, 17%);
+--btn-disabled-bg: transparent;
+--btn-disabled-border-color: hsl(240, 98%, 17%);
+--gradient: none;
+}
+
+.btn-outline-secondary {
+--btn-color: hsl(210, 7%, 46%);
+--btn-border-color: hsl(210, 7%, 46%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: hsl(210, 7%, 46%);
+--btn-hover-border-color: hsl(210, 7%, 46%);
+--btn-focus-shadow-rgb: 109, 117, 126;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: hsl(210, 7%, 46%);
+--btn-active-border-color: hsl(210, 7%, 46%);
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(210, 7%, 46%);
+--btn-disabled-bg: transparent;
+--btn-disabled-border-color: hsl(210, 7%, 46%);
+--gradient: none;
+}
+
+.btn-outline-success {
+--btn-color: hsl(120, 32%, 39%);
+--btn-border-color: hsl(120, 32%, 39%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: hsl(120, 32%, 39%);
+--btn-hover-border-color: hsl(120, 32%, 39%);
+--btn-focus-shadow-rgb: 68, 131, 68;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: hsl(120, 32%, 39%);
+--btn-active-border-color: hsl(120, 32%, 39%);
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(120, 32%, 39%);
+--btn-disabled-bg: transparent;
+--btn-disabled-border-color: hsl(120, 32%, 39%);
+--gradient: none;
+}
+
+.btn-outline-info {
+--btn-color: hsl(207, 49%, 37%);
+--btn-border-color: hsl(207, 49%, 37%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: hsl(207, 49%, 37%);
+--btn-hover-border-color: hsl(207, 49%, 37%);
+--btn-focus-shadow-rgb: 48, 99, 141;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: hsl(207, 49%, 37%);
+--btn-active-border-color: hsl(207, 49%, 37%);
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(207, 49%, 37%);
+--btn-disabled-bg: transparent;
+--btn-disabled-border-color: hsl(207, 49%, 37%);
+--gradient: none;
+}
+
+.btn-outline-warning {
+--btn-color: hsl(34, 100%, 34%);
+--btn-border-color: hsl(34, 100%, 34%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: hsl(34, 100%, 34%);
+--btn-hover-border-color: hsl(34, 100%, 34%);
+--btn-focus-shadow-rgb: 173, 98, 0;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: hsl(34, 100%, 34%);
+--btn-active-border-color: hsl(34, 100%, 34%);
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(34, 100%, 34%);
+--btn-disabled-bg: transparent;
+--btn-disabled-border-color: hsl(34, 100%, 34%);
+--gradient: none;
+}
+
+.btn-outline-danger {
+--btn-color: hsl(3, 75%, 37%);
+--btn-border-color: hsl(3, 75%, 37%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: hsl(3, 75%, 37%);
+--btn-hover-border-color: hsl(3, 75%, 37%);
+--btn-focus-shadow-rgb: 165, 31, 24;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: hsl(3, 75%, 37%);
+--btn-active-border-color: hsl(3, 75%, 37%);
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(3, 75%, 37%);
+--btn-disabled-bg: transparent;
+--btn-disabled-border-color: hsl(3, 75%, 37%);
+--gradient: none;
+}
+
+.btn-outline-light {
+--btn-color: hsl(210, 17%, 98%);
+--btn-border-color: hsl(210, 17%, 98%);
+--btn-hover-color: hsl(0, 0%, 0%);
+--btn-hover-bg: hsl(210, 17%, 98%);
+--btn-hover-border-color: hsl(210, 17%, 98%);
+--btn-focus-shadow-rgb: 249, 250, 251;
+--btn-active-color: hsl(0, 0%, 0%);
+--btn-active-bg: hsl(210, 17%, 98%);
+--btn-active-border-color: hsl(210, 17%, 98%);
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(210, 17%, 98%);
+--btn-disabled-bg: transparent;
+--btn-disabled-border-color: hsl(210, 17%, 98%);
+--gradient: none;
+}
+
+.btn-outline-dark {
+--btn-color: hsl(210, 10%, 23%);
+--btn-border-color: hsl(210, 10%, 23%);
+--btn-hover-color: hsl(0, 0%, 100%);
+--btn-hover-bg: hsl(210, 10%, 23%);
+--btn-hover-border-color: hsl(210, 10%, 23%);
+--btn-focus-shadow-rgb: 53, 59, 65;
+--btn-active-color: hsl(0, 0%, 100%);
+--btn-active-bg: hsl(210, 10%, 23%);
+--btn-active-border-color: hsl(210, 10%, 23%);
+--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+--btn-disabled-color: hsl(210, 10%, 23%);
+--btn-disabled-bg: transparent;
+--btn-disabled-border-color: hsl(210, 10%, 23%);
+--gradient: none;
+}
+
+.btn-link {
+--btn-font-weight: 400;
+--btn-color: var(--link-color);
+--btn-bg: transparent;
+--btn-border-color: transparent;
+--btn-hover-color: var(--link-hover-color);
+--btn-hover-border-color: transparent;
+--btn-active-color: var(--link-hover-color);
+--btn-active-border-color: transparent;
+--btn-disabled-color: hsl(210, 7%, 46%);
+--btn-disabled-border-color: transparent;
+--btn-box-shadow: none;
+--btn-focus-shadow-rgb: 39, 39, 111;
+text-decoration: underline;
+}
diff --git a/src/media/fonts/GOOGLE_FONTS_README.md b/src/media/fonts/GOOGLE_FONTS_README.md
index ec64e9c..878cca2 100644
--- a/src/media/fonts/GOOGLE_FONTS_README.md
+++ b/src/media/fonts/GOOGLE_FONTS_README.md
@@ -1,131 +1,31 @@
-
+ VERSION: 03.09.10
+ -->
-# Google Fonts - Download Instructions
+# Self-Hosted Google Fonts
-This directory should contain self-hosted Google Font files to eliminate CDN dependencies.
+Fonts are served locally to avoid external CDN dependencies and improve privacy/performance.
-## ⚠️ Manual Download Required
+## Available Fonts
-The Google Font `.woff2` files are **NOT included** in the repository and must be downloaded manually before using non-default font schemes.
+| Font | Weights | Styles | CSS File |
+|------|---------|--------|----------|
+| Roboto | 100–900 | Normal + Italic | `css/fonts/roboto.css` |
+| Fredoka | 300–700 | Normal | `css/fonts/fredoka.css` |
+| Pacifico | 400 | Normal | `css/fonts/pacifico.css` |
+| Osaka | — | — | `css/fonts/osaka.css` |
-**Currently Available:**
-- ✅ Osaka font (local TTF file, included)
+## Adding New Fonts
-**Requires Manual Download:**
-- ❌ Roboto fonts (4 weight variants)
-- ❌ Noto Sans fonts (4 weight variants)
-- ❌ Fira Sans fonts (4 weight variants)
+1. Run `php scripts/download-google-fonts.php` or manually download woff2 files
+2. Place files in `src/media/fonts/`
+3. Create a CSS file in `src/media/css/fonts/` with `@font-face` declarations
+4. Register the CSS in `joomla.asset.json`
+5. Add the font as an option in `templateDetails.xml` font selector
-## Required Font Files
+## File Naming Convention
-Download the following `.woff2` font files and place them in this directory:
+`{font-name}-v{version}-latin-{weight}.woff2`
-### Roboto Font Files
-- `roboto-v30-latin-100.woff2` (Thin)
-- `roboto-v30-latin-300.woff2` (Light)
-- `roboto-v30-latin-regular.woff2` (Regular)
-- `roboto-v30-latin-700.woff2` (Bold)
-
-### Noto Sans Font Files
-- `noto-sans-v36-latin-100.woff2` (Thin)
-- `noto-sans-v36-latin-300.woff2` (Light)
-- `noto-sans-v36-latin-regular.woff2` (Regular)
-- `noto-sans-v36-latin-700.woff2` (Bold)
-
-### Fira Sans Font Files
-- `fira-sans-v17-latin-100.woff2` (Thin)
-- `fira-sans-v17-latin-300.woff2` (Light)
-- `fira-sans-v17-latin-regular.woff2` (Regular)
-- `fira-sans-v17-latin-700.woff2` (Bold)
-
-## How to Download
-
-### Option 1: Using google-webfonts-helper (Recommended)
-
-1. Visit https://gwfh.mranftl.com/
-2. Search for each font (Roboto, Noto Sans, Fira Sans)
-3. Select character sets: **latin** (or add latin-ext if needed)
-4. Select styles:
- - ☑ 100 (thin)
- - ☑ 300 (light)
- - ☑ 400 (regular)
- - ☑ 700 (bold)
-5. In step 3, ensure **Modern Browsers** is selected (woff2 format)
-6. In step 4, click **Download files**
-7. Extract the `.woff2` files to this directory
-
-### Option 2: Using google-font-installer (Node.js)
-
-```bash
-npm install -g google-font-installer
-cd src/media/fonts/
-
-# Download Roboto
-google-font-installer Roboto:100,300,400,700
-
-# Download Noto Sans
-google-font-installer "Noto Sans:100,300,400,700"
-
-# Download Fira Sans
-google-font-installer "Fira Sans:100,300,400,700"
-```
-
-### Option 3: Manual Download Script (Linux/macOS)
-
-```bash
-#!/bin/bash
-# Run this from src/media/fonts/ directory
-
-download_font() {
- local font_url="$1"
- local output_dir="."
-
- # Download CSS
- css=$(curl -s -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" "$font_url")
-
- # Extract and download woff2 files
- echo "$css" | grep -oP 'https://fonts\.gstatic\.com[^\)]*\.woff2' | while read url; do
- filename=$(basename "$url")
- echo "Downloading $filename..."
- curl -s "$url" -o "$output_dir/$filename"
- done
-}
-
-download_font "https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;700&display=swap"
-download_font "https://fonts.googleapis.com/css2?family=Noto+Sans:wght@100;300;400;700&display=swap"
-download_font "https://fonts.googleapis.com/css2?family=Fira+Sans:wght@100;300;400;700&display=swap"
-```
-
-## Font CSS Files
-
-The corresponding CSS files with `@font-face` declarations are located in:
-- `../css/fonts/roboto.css`
-- `../css/fonts/noto-sans.css`
-- `../css/fonts/fira-sans.css`
-
-These CSS files reference the `.woff2` files in this directory.
-
-## License
-
-All Google Fonts are open source and licensed under the SIL Open Font License (OFL).
-- Roboto: Apache License 2.0
-- Noto Sans: SIL Open Font License 1.1
-- Fira Sans: SIL Open Font License 1.1
-
-## References
-
-- Google Fonts: https://fonts.google.com/
-- google-webfonts-helper: https://gwfh.mranftl.com/
-- Font Licensing: https://fonts.google.com/attribution
+Examples: `roboto-v51-latin-regular.woff2`, `fredoka-v17-latin-700.woff2`
diff --git a/src/templateDetails.xml b/src/templateDetails.xml
index aac0826..5e423c1 100644
--- a/src/templateDetails.xml
+++ b/src/templateDetails.xml
@@ -36,7 +36,7 @@
MokoCassiopeia
- 03.09.09
+ 03.09.10
script.php
2026-03-26
Jonathan Miller || Moko Consulting
From ce8ae0eec5c71522a99deb8f8d0b72c47bdc39e4 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Tue, 7 Apr 2026 23:53:29 -0500
Subject: [PATCH 03/18] Update dev channel to 03.09.10
Co-Authored-By: Claude Opus 4.6 (1M context)
---
updates.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/updates.xml b/updates.xml
index 0ba260d..7f2ac13 100644
--- a/updates.xml
+++ b/updates.xml
@@ -101,9 +101,9 @@
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/development
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.08-dev.zip
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.10-dev.zip
- ecff187531e65a40ae958ae91fff74da0c8856d1cc13e17a6e3d6905806b189e
+ eb7cc1760823e7811066e9d0ac4f2353987252a6a454f753451e8d493c67431e
development
From cd812ecbf414e0792b407a4731ffa830d22ba323 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:00:14 -0500
Subject: [PATCH 04/18] Fix breadcrumbs: stop double-filtering, add showHere
support
Joomla core already handles showHome/showLast/homeText before
passing $list to the template. Removed duplicate filtering that
was breaking those settings. Added showHere param to control
"You are here" label visibility. Bump to 03.09.11.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/html/mod_breadcrumbs/default.php | 44 ++++++----------------------
src/templateDetails.xml | 2 +-
2 files changed, 10 insertions(+), 36 deletions(-)
diff --git a/src/html/mod_breadcrumbs/default.php b/src/html/mod_breadcrumbs/default.php
index 96e741e..b81fc8c 100644
--- a/src/html/mod_breadcrumbs/default.php
+++ b/src/html/mod_breadcrumbs/default.php
@@ -10,49 +10,20 @@
/**
* Default layout override for mod_breadcrumbs.
* Bootstrap 5 breadcrumb with schema.org BreadcrumbList markup.
- * Respects showHome, showLast, homeText module settings.
+ * Module settings (showHome, showLast, homeText) are handled by Joomla core
+ * before $list reaches this template.
*/
defined('_JEXEC') or die;
-use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
-use Joomla\CMS\Router\Route;
-use Joomla\CMS\Uri\Uri;
-
-Factory::getApplication()->getLanguage()->load('mod_breadcrumbs', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
-$showHome = $params->get('showHome', 1);
-$showLast = $params->get('showLast', 1);
-$homeText = $params->get('homeText', '') ?: Text::_('MOD_BREADCRUMBS_HOME');
+$showHere = $params->get('showHere', 1);
-// Build filtered list respecting module settings
-$items = [];
-$count = count($list);
-
-foreach ($list as $key => $item) {
- // Skip Home item if showHome is off
- if ($key === 0 && !$showHome) {
- continue;
- }
-
- // Replace Home text if custom homeText is set
- if ($key === 0 && $showHome) {
- $item->name = $homeText;
- }
-
- // Skip last item if showLast is off
- if ($key === $count - 1 && !$showLast) {
- continue;
- }
-
- $items[] = $item;
-}
-
-if (empty($items)) {
+if (empty($list)) {
return;
}
?>
@@ -60,9 +31,12 @@ if (empty($items)) {
showtitle) : ?>
< class="mod-breadcrumbs__title">title; ?>>
+
+
+
- $item) : ?>
-
+ $item) : ?>
+
>
link)) : ?>
diff --git a/src/templateDetails.xml b/src/templateDetails.xml
index 5e423c1..9b9ad98 100644
--- a/src/templateDetails.xml
+++ b/src/templateDetails.xml
@@ -36,7 +36,7 @@
MokoCassiopeia
- 03.09.10
+ 03.09.11
script.php
2026-03-26
Jonathan Miller || Moko Consulting
From fd186fea09e067b129a5531266665df23ba214f2 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:01:09 -0500
Subject: [PATCH 05/18] Update dev channel to 03.09.11
Co-Authored-By: Claude Opus 4.6 (1M context)
---
updates.xml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/updates.xml b/updates.xml
index 32ea4b3..5ffa88f 100644
--- a/updates.xml
+++ b/updates.xml
@@ -92,8 +92,8 @@
template
site
- 03.09.10
- 2026-04-07
+ 03.09.11
+ 2026-04-08
Jonathan Miller || Moko Consulting
hello@mokoconsulting.tech
(C)GNU General Public License Version 3 - 2026 Moko Consulting
@@ -101,9 +101,9 @@
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/development
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.10-dev.zip
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.11-dev.zip
- eb7cc1760823e7811066e9d0ac4f2353987252a6a454f753451e8d493c67431e
+ a758354a54f64bcca8ab7eaefa4ffe2e324b7af604b859937291396a39e04ecc
development
From 0b557c54acd0c4830e53d03b2862afb31c092c37 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:04:18 -0500
Subject: [PATCH 06/18] Reorder updates.xml to match release pipeline
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Order: development → alpha → beta → rc → stable
Each section annotated with its pipeline stage.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
updates.xml | 147 +++++++++++++++++++++++++++-------------------------
1 file changed, 76 insertions(+), 71 deletions(-)
diff --git a/updates.xml b/updates.xml
index 5ffa88f..808f91f 100644
--- a/updates.xml
+++ b/updates.xml
@@ -10,80 +10,15 @@
INGROUP: MokoCassiopeia
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
PATH: ./updates.xml
- VERSION: 03.09.05
- BRIEF: Update manifest XML — stable, rc, and development channels
+ VERSION: 03.09.11
+ BRIEF: Update manifest XML — release channels ordered by pipeline
-->
-
- MokoCassiopeia
- Moko Consulting's site template based on Cassiopeia.
- mokocassiopeia
- template
- site
-
- 03.09.09
- 2026-04-07
- Jonathan Miller || Moko Consulting
- hello@mokoconsulting.tech
- (C)GNU General Public License Version 3 - 2026 Moko Consulting
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03
-
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.09.zip
-
- 7a507a3219f67c864fceef7da2e5ef871d89ca9f098572c7ba6121f478869e9b
-
-
- stable
-
-
- Moko Consulting
- https://mokoconsulting.tech
-
-
-
-
-
-
- MokoCassiopeia
- MokoCassiopeia release candidate — testing only.
- mokocassiopeia
- template
- site
-
- 03.09.09
- 2026-04-07
- Jonathan Miller || Moko Consulting
- hello@mokoconsulting.tech
- (C)GNU General Public License Version 3 - 2026 Moko Consulting
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/release-candidate
-
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-rc.zip
-
- 31e660078e728e8c9177b5a2d75efd89fea1fd4e9320d77444ab8fe28d3b354d
-
-
- rc
-
-
- Moko Consulting
- https://mokoconsulting.tech
-
-
-
-
-
MokoCassiopeia
@@ -116,7 +51,8 @@
MokoCassiopeia
@@ -149,7 +85,8 @@
MokoCassiopeia
@@ -181,4 +118,72 @@
+
+
+ MokoCassiopeia
+ MokoCassiopeia release candidate — testing only.
+ mokocassiopeia
+ template
+ site
+
+ 03.09.09
+ 2026-04-07
+ Jonathan Miller || Moko Consulting
+ hello@mokoconsulting.tech
+ (C)GNU General Public License Version 3 - 2026 Moko Consulting
+
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/release-candidate
+
+
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-rc.zip
+
+ 31e660078e728e8c9177b5a2d75efd89fea1fd4e9320d77444ab8fe28d3b354d
+
+
+ rc
+
+
+ Moko Consulting
+ https://mokoconsulting.tech
+
+
+
+
+
+
+ MokoCassiopeia
+ Moko Consulting's site template based on Cassiopeia.
+ mokocassiopeia
+ template
+ site
+
+ 03.09.09
+ 2026-04-07
+ Jonathan Miller || Moko Consulting
+ hello@mokoconsulting.tech
+ (C)GNU General Public License Version 3 - 2026 Moko Consulting
+
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03
+
+
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.09.zip
+
+ 7a507a3219f67c864fceef7da2e5ef871d89ca9f098572c7ba6121f478869e9b
+
+
+ stable
+
+
+ Moko Consulting
+ https://mokoconsulting.tech
+
+
+
+
From e1e0087414004d0f832732f06d1fceb61caed215 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:07:19 -0500
Subject: [PATCH 07/18] Update dev channel SHA for 03.09.11
Co-Authored-By: Claude Opus 4.6 (1M context)
---
updates.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/updates.xml b/updates.xml
index 808f91f..0779162 100644
--- a/updates.xml
+++ b/updates.xml
@@ -38,7 +38,7 @@
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.11-dev.zip
- a758354a54f64bcca8ab7eaefa4ffe2e324b7af604b859937291396a39e04ecc
+ 281e7b9315e8b1589422057edf01f0bf62aaad04502d5f1424104aec0d8caad1
development
From a70e74c72a5c0d065f9cf91e2d57bfc0ed7ac8ef Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:13:01 -0500
Subject: [PATCH 08/18] ci: restrict workflow triggers to main and version/*
only
Save GitHub Actions minutes by not running CI on every dev/rc
commit. All workflows now trigger only on push to main or
version/* branches. Manual dispatch still available.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.github/workflows/changelog-validation.yml | 4 +---
.github/workflows/ci-joomla.yml | 9 +--------
.github/workflows/codeql-analysis.yml | 9 +--------
.github/workflows/standards-compliance.yml | 4 +---
.github/workflows/update-server.yml | 6 ++----
5 files changed, 6 insertions(+), 26 deletions(-)
diff --git a/.github/workflows/changelog-validation.yml b/.github/workflows/changelog-validation.yml
index e2ec667..bfd1be3 100644
--- a/.github/workflows/changelog-validation.yml
+++ b/.github/workflows/changelog-validation.yml
@@ -19,9 +19,7 @@ on:
push:
branches:
- main
- pull_request:
- branches:
- - main
+ - version/*
workflow_dispatch:
permissions:
diff --git a/.github/workflows/ci-joomla.yml b/.github/workflows/ci-joomla.yml
index 01e1edb..fa1b81a 100644
--- a/.github/workflows/ci-joomla.yml
+++ b/.github/workflows/ci-joomla.yml
@@ -19,14 +19,7 @@ on:
push:
branches:
- main
- - dev/**
- - rc/**
- - version/**
- pull_request:
- branches:
- - main
- - dev/**
- - rc/**
+ - version/*
workflow_dispatch:
permissions:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 1639497..72cacae 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -21,14 +21,7 @@ on:
push:
branches:
- main
- - dev/**
- - rc/**
- - version/**
- pull_request:
- branches:
- - main
- - dev/**
- - rc/**
+ - version/*
schedule:
# Weekly on Monday at 06:00 UTC
- cron: '0 6 * * 1'
diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml
index 79aaedd..418a297 100644
--- a/.github/workflows/standards-compliance.yml
+++ b/.github/workflows/standards-compliance.yml
@@ -89,9 +89,7 @@ env:
on:
push:
- branches: [main, dev/**, rc/**, version/**]
- pull_request:
- branches: [main, dev/**, rc/**]
+ branches: [main, version/*]
workflow_dispatch:
permissions:
diff --git a/.github/workflows/update-server.yml b/.github/workflows/update-server.yml
index 90482e5..8becae8 100644
--- a/.github/workflows/update-server.yml
+++ b/.github/workflows/update-server.yml
@@ -22,10 +22,8 @@ name: Update Joomla Update Server XML Feed
on:
push:
branches:
- - 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- - 'rc/**'
+ - main
+ - version/*
paths:
- 'src/**'
- 'htdocs/**'
From b6e7c6478665b67a8c182b281c6cdb6a67edf67a Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:16:54 -0500
Subject: [PATCH 09/18] Breadcrumb label and items on single line
Add flex layout to .mod-breadcrumbs so the "You are here" label
and breadcrumb list sit inline. Label floats left with margin.
Bump to 03.09.12.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/media/css/template.css | 15 +++++++++++++++
src/templateDetails.xml | 2 +-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/src/media/css/template.css b/src/media/css/template.css
index 77723d8..ad84336 100644
--- a/src/media/css/template.css
+++ b/src/media/css/template.css
@@ -14764,6 +14764,21 @@ iframe {
}
/* Breadcrumbs module */
+.mod-breadcrumbs {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.mod-breadcrumbs__here {
+ float: left;
+ font-weight: 600;
+ white-space: nowrap;
+ color: var(--body-font-color, #444);
+ margin-right: 0.5rem;
+}
+
.mod-breadcrumbs .breadcrumb {
background: transparent;
padding: 0;
diff --git a/src/templateDetails.xml b/src/templateDetails.xml
index 9b9ad98..5759c42 100644
--- a/src/templateDetails.xml
+++ b/src/templateDetails.xml
@@ -36,7 +36,7 @@
MokoCassiopeia
- 03.09.11
+ 03.09.12
script.php
2026-03-26
Jonathan Miller || Moko Consulting
From 4fdd0ca9ae5de9a5d76e985621905642a55ff708 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:18:24 -0500
Subject: [PATCH 10/18] Breadcrumb padding fix, version in template description
Change __here margin-right to padding-right: .15rem. Add version
number to template description so it shows in Joomla admin.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/media/css/template.css | 2 +-
src/templateDetails.xml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/media/css/template.css b/src/media/css/template.css
index ad84336..3db19a6 100644
--- a/src/media/css/template.css
+++ b/src/media/css/template.css
@@ -14776,7 +14776,7 @@ iframe {
font-weight: 600;
white-space: nowrap;
color: var(--body-font-color, #444);
- margin-right: 0.5rem;
+ padding-right: .15rem;
}
.mod-breadcrumbs .breadcrumb {
diff --git a/src/templateDetails.xml b/src/templateDetails.xml
index 5759c42..fd50947 100644
--- a/src/templateDetails.xml
+++ b/src/templateDetails.xml
@@ -36,13 +36,13 @@
MokoCassiopeia
- 03.09.12
+ 03.09.11
script.php
2026-03-26
Jonathan Miller || Moko Consulting
hello@mokoconsulting.tech
(C)GNU General Public License Version 3 - 2026 Moko Consulting
- MokoCassiopeia Template Description MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).
This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.
Custom Colour Themes Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.
Custom CSS & JavaScript For site-specific styles and scripts that should survive template updates, create the following files:
media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides. media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript. These files are gitignored and will not be overwritten by template updates.
Code Attribution This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.
Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.
It includes integration with Bootstrap TOC , an open-source table of contents generator by A. Feld, licensed under the MIT License.
All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.
]]>
+ Version: 03.09.11
MokoCassiopeia Template Description MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).
This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.
Custom Colour Themes Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.
Custom CSS & JavaScript For site-specific styles and scripts that should survive template updates, create the following files:
media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides. media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript. These files are gitignored and will not be overwritten by template updates.
Code Attribution This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.
Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.
It includes integration with Bootstrap TOC , an open-source table of contents generator by A. Feld, licensed under the MIT License.
All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.
]]>
1
component.php
From 33bf79bc65e5348ad3b1d402b0011e794b210ef3 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:19:04 -0500
Subject: [PATCH 11/18] Show shields.io badges in template description
Version, license, Joomla, and PHP badges rendered as img tags
in the template description, matching the README badge style.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/templateDetails.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/templateDetails.xml b/src/templateDetails.xml
index fd50947..50e2328 100644
--- a/src/templateDetails.xml
+++ b/src/templateDetails.xml
@@ -42,7 +42,7 @@
Jonathan Miller || Moko Consulting
hello@mokoconsulting.tech
(C)GNU General Public License Version 3 - 2026 Moko Consulting
- Version: 03.09.11 MokoCassiopeia Template Description MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).
This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.
Custom Colour Themes Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.
Custom CSS & JavaScript For site-specific styles and scripts that should survive template updates, create the following files:
media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides. media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript. These files are gitignored and will not be overwritten by template updates.
Code Attribution This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.
Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.
It includes integration with Bootstrap TOC , an open-source table of contents generator by A. Feld, licensed under the MIT License.
All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.
]]>
+ MokoCassiopeia Template Description MokoCassiopeia continues Joomla's tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).
This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.
Custom Colour Themes Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokocassiopeia/templates/light.custom.css to media/templates/site/mokocassiopeia/css/theme/light.custom.css, or templates/mokocassiopeia/templates/dark.custom.css to media/templates/site/mokocassiopeia/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoCassiopeia → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.
Custom CSS & JavaScript For site-specific styles and scripts that should survive template updates, create the following files:
media/templates/site/mokocassiopeia/css/user.css — loaded on every page for custom CSS overrides. media/templates/site/mokocassiopeia/js/user.js — loaded on every page for custom JavaScript. These files are gitignored and will not be overwritten by template updates.
Code Attribution This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.
Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.
It includes integration with Bootstrap TOC , an open-source table of contents generator by A. Feld, licensed under the MIT License.
All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.
]]>
1
component.php
From c03db0a099b60ace6226e33c5a68df831b4e7281 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:19:59 -0500
Subject: [PATCH 12/18] Update dev channel SHA for 03.09.11
Co-Authored-By: Claude Opus 4.6 (1M context)
---
updates.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/updates.xml b/updates.xml
index 0779162..f030a4e 100644
--- a/updates.xml
+++ b/updates.xml
@@ -38,7 +38,7 @@
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.11-dev.zip
- 281e7b9315e8b1589422057edf01f0bf62aaad04502d5f1424104aec0d8caad1
+ d2927ff464248bd52ecac62c60bd009dd7d7fec14d827c4a6ee27ae036228d35
development
From 4ccc972e88a1bade8b582a639ab13018c56866e6 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 00:34:25 -0500
Subject: [PATCH 13/18] fix: sync updates.xml from main [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context)
---
updates.xml | 192 ++++------------------------------------------------
1 file changed, 12 insertions(+), 180 deletions(-)
diff --git a/updates.xml b/updates.xml
index f030a4e..3709a8d 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,189 +1,21 @@
-
-
-
+
-
-
- MokoCassiopeia
- MokoCassiopeia development build — unstable.
- mokocassiopeia
- template
- site
-
- 03.09.11
- 2026-04-08
- Jonathan Miller || Moko Consulting
- hello@mokoconsulting.tech
- (C)GNU General Public License Version 3 - 2026 Moko Consulting
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/development
-
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.11-dev.zip
-
- d2927ff464248bd52ecac62c60bd009dd7d7fec14d827c4a6ee27ae036228d35
-
-
- development
-
-
- Moko Consulting
- https://mokoconsulting.tech
-
-
-
-
-
-
- MokoCassiopeia
- MokoCassiopeia alpha build — early testing.
- mokocassiopeia
- template
- site
-
- 03.09.09
- 2026-04-07
- Jonathan Miller || Moko Consulting
- hello@mokoconsulting.tech
- (C)GNU General Public License Version 3 - 2026 Moko Consulting
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/alpha
-
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.09.09-alpha.zip
-
- 1a32180f8b26749bf5daf0602262e33464bcb3a042a8ff51ec2844cdeef2f9e5
-
-
- alpha
-
-
- Moko Consulting
- https://mokoconsulting.tech
-
-
-
-
-
-
- MokoCassiopeia
- MokoCassiopeia beta build — feature complete, stability testing.
- mokocassiopeia
- template
- site
-
- 03.09.09
- 2026-04-07
- Jonathan Miller || Moko Consulting
- hello@mokoconsulting.tech
- (C)GNU General Public License Version 3 - 2026 Moko Consulting
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/beta
-
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.09.09-beta.zip
-
- 1a32180f8b26749bf5daf0602262e33464bcb3a042a8ff51ec2844cdeef2f9e5
-
-
- beta
-
-
- Moko Consulting
- https://mokoconsulting.tech
-
-
-
-
-
-
- MokoCassiopeia
- MokoCassiopeia release candidate — testing only.
- mokocassiopeia
- template
- site
-
- 03.09.09
- 2026-04-07
- Jonathan Miller || Moko Consulting
- hello@mokoconsulting.tech
- (C)GNU General Public License Version 3 - 2026 Moko Consulting
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/release-candidate
-
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-rc.zip
-
- 31e660078e728e8c9177b5a2d75efd89fea1fd4e9320d77444ab8fe28d3b354d
-
-
- rc
-
-
- Moko Consulting
- https://mokoconsulting.tech
-
-
-
-
-
-
- MokoCassiopeia
- Moko Consulting's site template based on Cassiopeia.
- mokocassiopeia
- template
- site
-
- 03.09.09
- 2026-04-07
- Jonathan Miller || Moko Consulting
- hello@mokoconsulting.tech
- (C)GNU General Public License Version 3 - 2026 Moko Consulting
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03
-
-
- https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.09.zip
-
- 7a507a3219f67c864fceef7da2e5ef871d89ca9f098572c7ba6121f478869e9b
-
+
+ update
+
+ component
+
stable
-
+ https://github.com/mokoconsulting-tech/MokoCassiopeia
+
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v/-.zip
+ https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v/-.tar.gz
+
+
Moko Consulting
https://mokoconsulting.tech
-
-
-
From 211dbefb8b888a91adba1f21ba3fb604301ea875 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 01:30:47 -0500
Subject: [PATCH 14/18] CSS and Overrides fix
---
.../mod_menu/{mainmenu.php => horizontal.php} | 0
...u_component.php => horizontal_component.php} | 0
...nmenu_heading.php => horizontal_heading.php} | 0
...u_separator.php => horizontal_separator.php} | 0
.../{mainmenu_url.php => horizontal_url.php} | 0
src/media/css/template.css | 17 +++--------------
6 files changed, 3 insertions(+), 14 deletions(-)
rename src/html/mod_menu/{mainmenu.php => horizontal.php} (100%)
rename src/html/mod_menu/{mainmenu_component.php => horizontal_component.php} (100%)
rename src/html/mod_menu/{mainmenu_heading.php => horizontal_heading.php} (100%)
rename src/html/mod_menu/{mainmenu_separator.php => horizontal_separator.php} (100%)
rename src/html/mod_menu/{mainmenu_url.php => horizontal_url.php} (100%)
diff --git a/src/html/mod_menu/mainmenu.php b/src/html/mod_menu/horizontal.php
similarity index 100%
rename from src/html/mod_menu/mainmenu.php
rename to src/html/mod_menu/horizontal.php
diff --git a/src/html/mod_menu/mainmenu_component.php b/src/html/mod_menu/horizontal_component.php
similarity index 100%
rename from src/html/mod_menu/mainmenu_component.php
rename to src/html/mod_menu/horizontal_component.php
diff --git a/src/html/mod_menu/mainmenu_heading.php b/src/html/mod_menu/horizontal_heading.php
similarity index 100%
rename from src/html/mod_menu/mainmenu_heading.php
rename to src/html/mod_menu/horizontal_heading.php
diff --git a/src/html/mod_menu/mainmenu_separator.php b/src/html/mod_menu/horizontal_separator.php
similarity index 100%
rename from src/html/mod_menu/mainmenu_separator.php
rename to src/html/mod_menu/horizontal_separator.php
diff --git a/src/html/mod_menu/mainmenu_url.php b/src/html/mod_menu/horizontal_url.php
similarity index 100%
rename from src/html/mod_menu/mainmenu_url.php
rename to src/html/mod_menu/horizontal_url.php
diff --git a/src/media/css/template.css b/src/media/css/template.css
index 3db19a6..2251e3f 100644
--- a/src/media/css/template.css
+++ b/src/media/css/template.css
@@ -13987,21 +13987,10 @@ meter {
}
.footer {
- margin-top: 1em;
+ padding-top: 1rem;
color: var(--body-bg, #e6ebf1);
background-color: var(--nav-bg-color);
- padding-left: 100px;
- padding-right: 60px;
- padding-bottom: 60px;
-}
-
-/* Increase footer right padding when floating controls are present */
-body[data-theme-fab-enabled="1"] .footer {
- padding-right: 220px;
-}
-
-body[data-theme-fab-enabled="1"][data-a11y-toolbar="1"] .footer {
- padding-right: 420px;
+ padding-bottom: 80px;
}
.footer .grid-child {
@@ -15792,7 +15781,7 @@ body.wrapper-fluid header>.grid-child {
}
footer .grid-child>div {
- padding: 1rem 4em;
+ padding: 1rem 0 0;
}
header .grid-child .navbar-brand {
From 35b1c55d6f0604d60cbbc256ecc7bbf8f67a728a Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 01:41:17 -0500
Subject: [PATCH 15/18] Backward compatibility
---
src/html/mod_menu/mainmenu.php | 106 +++++++++++++++++++++++
src/html/mod_menu/mainmenu_component.php | 66 ++++++++++++++
src/html/mod_menu/mainmenu_heading.php | 39 +++++++++
src/html/mod_menu/mainmenu_separator.php | 33 +++++++
src/html/mod_menu/mainmenu_url.php | 71 +++++++++++++++
5 files changed, 315 insertions(+)
create mode 100644 src/html/mod_menu/mainmenu.php
create mode 100644 src/html/mod_menu/mainmenu_component.php
create mode 100644 src/html/mod_menu/mainmenu_heading.php
create mode 100644 src/html/mod_menu/mainmenu_separator.php
create mode 100644 src/html/mod_menu/mainmenu_url.php
diff --git a/src/html/mod_menu/mainmenu.php b/src/html/mod_menu/mainmenu.php
new file mode 100644
index 0000000..e664177
--- /dev/null
+++ b/src/html/mod_menu/mainmenu.php
@@ -0,0 +1,106 @@
+
+ *
+ * This file is part of a Moko Consulting project.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Main Menu - Mobile responsive collapsible dropdown menu override
+ * Bootstrap 5 responsive navbar with hamburger menu
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Helper\ModuleHelper;
+
+$id = '';
+
+if ($tagId = $params->get('tag_id', '')) {
+ $id = ' id="' . $tagId . '"';
+}
+
+// Get module class suffix
+$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
+
+// The menu class is deprecated. Use mod-menu instead
+?>
+>
+
+
+
+
+
+
+
+
+
+ &$item) :
+ $itemParams = $item->getParams();
+ $class = 'nav-item mod-menu-main__item item-' . $item->id;
+
+ if ($item->id == $default_id) {
+ $class .= ' default';
+ }
+
+ if ($item->id == $active_id || ($item->type === 'alias' && $itemParams->get('aliasoptions') == $active_id)) {
+ $class .= ' current';
+ }
+
+ if (in_array($item->id, $path)) {
+ $class .= ' active';
+ } elseif ($item->type === 'alias') {
+ $aliasToId = $itemParams->get('aliasoptions');
+
+ if (count($path) > 0 && $aliasToId == $path[count($path) - 1]) {
+ $class .= ' active';
+ } elseif (in_array($aliasToId, $path)) {
+ $class .= ' alias-parent-active';
+ }
+ }
+
+ if ($item->type === 'separator') {
+ $class .= ' divider';
+ }
+
+ if ($item->deeper) {
+ $class .= ' deeper dropdown';
+ }
+
+ if ($item->parent) {
+ $class .= ' parent';
+ }
+
+ echo '';
+
+ switch ($item->type) :
+ case 'separator':
+ case 'component':
+ case 'heading':
+ case 'url':
+ require ModuleHelper::getLayoutPath('mod_menu', 'mainmenu_' . $item->type);
+ break;
+
+ default:
+ require ModuleHelper::getLayoutPath('mod_menu', 'mainmenu_url');
+ break;
+ endswitch;
+
+ // The next item is deeper.
+ if ($item->deeper) {
+ echo '';
+ } elseif ($item->shallower) {
+ // The next item is shallower.
+ echo ' ';
+ echo str_repeat(' ', $item->level_diff);
+ } else {
+ // The next item is on the same level.
+ echo '';
+ }
+endforeach;
+?>
+
+
+
diff --git a/src/html/mod_menu/mainmenu_component.php b/src/html/mod_menu/mainmenu_component.php
new file mode 100644
index 0000000..dfcfa7f
--- /dev/null
+++ b/src/html/mod_menu/mainmenu_component.php
@@ -0,0 +1,66 @@
+
+ *
+ * This file is part of a Moko Consulting project.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Main Menu - Component item layout
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Filter\OutputFilter;
+use Joomla\CMS\HTML\HTMLHelper;
+
+$attributes = [];
+
+if ($item->anchor_title) {
+ $attributes['title'] = $item->anchor_title;
+}
+
+if ($item->anchor_css) {
+ $attributes['class'] = $item->anchor_css;
+}
+
+if ($item->anchor_rel) {
+ $attributes['rel'] = $item->anchor_rel;
+}
+
+$linktype = $item->title;
+
+if ($item->menu_icon) {
+ // The link is an icon
+ if ($itemParams->get('menu_text', 1)) {
+ // If the link text is to be displayed, the icon is added with aria-hidden
+ $linktype = '' . $item->title;
+ } else {
+ // If the icon itself is the link, it needs a visually hidden text
+ $linktype = '' . $item->title . ' ';
+ }
+}
+
+if ($item->browserNav == 1) {
+ $attributes['target'] = '_blank';
+ $attributes['rel'] = 'noopener noreferrer';
+} elseif ($item->browserNav == 2) {
+ $options = 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,' . $params->get('window_open');
+
+ $attributes['onclick'] = "window.open(this.href, 'targetWindow', '" . $options . "'); return false;";
+}
+
+// Add dropdown toggle for items with children
+$linkClass = 'nav-link mod-menu-main__link';
+if ($item->deeper) {
+ $linkClass .= ' dropdown-toggle';
+ $attributes['data-bs-toggle'] = 'dropdown';
+ $attributes['role'] = 'button';
+ $attributes['aria-expanded'] = 'false';
+}
+
+$attributes['class'] = $linkClass;
+
+echo HTMLHelper::_('link', OutputFilter::ampReplace(htmlspecialchars($item->flink, ENT_COMPAT, 'UTF-8', false)), $linktype, $attributes);
diff --git a/src/html/mod_menu/mainmenu_heading.php b/src/html/mod_menu/mainmenu_heading.php
new file mode 100644
index 0000000..990077e
--- /dev/null
+++ b/src/html/mod_menu/mainmenu_heading.php
@@ -0,0 +1,39 @@
+
+ *
+ * This file is part of a Moko Consulting project.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Main Menu - Heading item layout
+ */
+
+defined('_JEXEC') or die;
+
+$title = $item->anchor_title ? ' title="' . $item->anchor_title . '"' : '';
+$anchor_css = $item->anchor_css ?: '';
+
+$linktype = $item->title;
+
+if ($item->menu_icon) {
+ // The link is an icon
+ if ($itemParams->get('menu_text', 1)) {
+ // If the link text is to be displayed, the icon is added with aria-hidden
+ $linktype = '' . $item->title;
+ } else {
+ // If the icon itself is the link, it needs a visually hidden text
+ $linktype = '' . $item->title . ' ';
+ }
+}
+
+// Add dropdown toggle for items with children
+$headingClass = 'nav-link mod-menu-main__heading';
+if ($item->deeper) {
+ $headingClass .= ' dropdown-toggle';
+}
+
+?>
+>
diff --git a/src/html/mod_menu/mainmenu_separator.php b/src/html/mod_menu/mainmenu_separator.php
new file mode 100644
index 0000000..2c0c587
--- /dev/null
+++ b/src/html/mod_menu/mainmenu_separator.php
@@ -0,0 +1,33 @@
+
+ *
+ * This file is part of a Moko Consulting project.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Main Menu - Separator item layout
+ */
+
+defined('_JEXEC') or die;
+
+$title = $item->anchor_title ? ' title="' . $item->anchor_title . '"' : '';
+$anchor_css = $item->anchor_css ?: '';
+
+$linktype = $item->title;
+
+if ($item->menu_icon) {
+ // The link is an icon
+ if ($itemParams->get('menu_text', 1)) {
+ // If the link text is to be displayed, the icon is added with aria-hidden
+ $linktype = '' . $item->title;
+ } else {
+ // If the icon itself is the link, it needs a visually hidden text
+ $linktype = '' . $item->title . ' ';
+ }
+}
+
+?>
+>
diff --git a/src/html/mod_menu/mainmenu_url.php b/src/html/mod_menu/mainmenu_url.php
new file mode 100644
index 0000000..10b380b
--- /dev/null
+++ b/src/html/mod_menu/mainmenu_url.php
@@ -0,0 +1,71 @@
+
+ *
+ * This file is part of a Moko Consulting project.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * Main Menu - URL item layout
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Filter\OutputFilter;
+use Joomla\CMS\HTML\HTMLHelper;
+
+$attributes = [];
+
+if ($item->anchor_title) {
+ $attributes['title'] = $item->anchor_title;
+}
+
+if ($item->anchor_css) {
+ $attributes['class'] = $item->anchor_css;
+}
+
+if ($item->anchor_rel) {
+ $attributes['rel'] = $item->anchor_rel;
+}
+
+$linktype = $item->title;
+
+if ($item->menu_icon) {
+ // The link is an icon
+ if ($itemParams->get('menu_text', 1)) {
+ // If the link text is to be displayed, the icon is added with aria-hidden
+ $linktype = '' . $item->title;
+ } else {
+ // If the icon itself is the link, it needs a visually hidden text
+ $linktype = '' . $item->title . ' ';
+ }
+}
+
+if ($item->browserNav == 1) {
+ $attributes['target'] = '_blank';
+ $attributes['rel'] = 'noopener noreferrer';
+} elseif ($item->browserNav == 2) {
+ $options = 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,' . $params->get('window_open');
+
+ $attributes['onclick'] = "window.open(this.href, 'targetWindow', '" . $options . "'); return false;";
+}
+
+// Add dropdown toggle for items with children
+$linkClass = 'nav-link mod-menu-main__link';
+if ($item->deeper) {
+ $linkClass .= ' dropdown-toggle';
+ $attributes['data-bs-toggle'] = 'dropdown';
+ $attributes['role'] = 'button';
+ $attributes['aria-expanded'] = 'false';
+}
+
+// Merge existing class with our class
+if (isset($attributes['class'])) {
+ $attributes['class'] .= ' ' . $linkClass;
+} else {
+ $attributes['class'] = $linkClass;
+}
+
+echo HTMLHelper::_('link', OutputFilter::ampReplace(htmlspecialchars($item->flink, ENT_COMPAT, 'UTF-8', false)), $linktype, $attributes);
From 8d6a8df15e849163e511d6e2ce37522d8ea3a5de Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Wed, 8 Apr 2026 05:14:05 -0500
Subject: [PATCH 16/18] Fix horizontal menu: use own sub-layouts, unique
collapse ID
horizontal.php was referencing mainmenu_ sub-layouts instead of
horizontal_ ones. Also use module ID in collapse target to avoid
conflicts when multiple menus exist on the same page.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/html/mod_menu/horizontal.php | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/html/mod_menu/horizontal.php b/src/html/mod_menu/horizontal.php
index e664177..d24ffcb 100644
--- a/src/html/mod_menu/horizontal.php
+++ b/src/html/mod_menu/horizontal.php
@@ -30,12 +30,12 @@ $moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COM
>