diff --git a/CLAUDE.md b/CLAUDE.md
index 889963b..78fd058 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -153,3 +153,6 @@ or adapted for production, and these accounts must never exist in a production d
- `includes/navbar.php` (barra antiga) foi apagado; `system_settings.php` já não o inclui.
- `views/staff/dashboard.php`: saudação mínima (o staff ainda não alcança nada em `views/admin/`).
- Gotcha de dev: a tabela `trial_leads` não existe na BD de dev, por isso `views/admin/trials/index.php` dá 500 (migração por aplicar; anterior a esta mudança).
+
+## Bootstrap re-tematizado (2026-10-13)
+- `assets/css/bs-brand.css` (carregado depois do Bootstrap em `includes/header.php` e `includes/auth_header.php`) sobrescreve as cores do Bootstrap só com variáveis `--bs-*` e as variáveis de componente (`.btn-*`, `.form-check-input`, `.page-link`…), claro e escuro. A cor de marca é `--fitme-accent` (fallback `#C2D500`); todos os tons derivam dela com `color-mix()`; `--brand-on` é o texto sobre a cor. Secondary/success/danger/warning/info têm valores próprios. Verificação: `platform.php?tool=branding` (Montra da marca) não deve mostrar nenhum azul por omissão. O site público ainda não carrega este ficheiro.
diff --git a/assets/css/bs-brand.css b/assets/css/bs-brand.css
new file mode 100644
index 0000000..126c6d9
--- /dev/null
+++ b/assets/css/bs-brand.css
@@ -0,0 +1,253 @@
+/* ── Bootstrap re-themed to the FitMe brand ──────────────────────────────────
+ Loaded after bootstrap.min.css (and before backoffice.css). Bootstrap's source
+ is never edited: everything here is --bs-* variables plus the component-scoped
+ variables Bootstrap 5.3 exposes (.btn-*, .form-check-input, .page-link …).
+ The brand colour is a single token, --fitme-accent (a gym's own colour on the
+ public site, see gym_brand_css()); every shade below is derived from it with
+ color-mix(), so changing the brand colour re-themes all of Bootstrap at once.
+ The other semantic colours (success/danger/warning/info) are tuned to sit next
+ to the brand without being confused with it. */
+:root,
+[data-bs-theme="light"],
+[data-bs-theme="dark"] {
+ --brand: var(--fitme-accent, #C2D500);
+ --brand-ink: color-mix(in srgb, var(--brand) 55%, #000); /* text on light surfaces */
+ --brand-on: var(--fitme-on-accent, #0B0B0D); /* text on the brand colour */
+
+ --bs-primary: var(--brand);
+ --bs-primary-rgb: var(--fitme-accent-rgb, 194, 213, 0);
+ --bs-secondary: #3D403E;
+ --bs-secondary-rgb: 61, 64, 62;
+ --bs-success: #2E9E5B;
+ --bs-success-rgb: 46, 158, 91;
+ --bs-danger: #D6322E;
+ --bs-danger-rgb: 214, 50, 46;
+ --bs-warning: #F2A900;
+ --bs-warning-rgb: 242, 169, 0;
+ --bs-info: #2C9FD0;
+ --bs-info-rgb: 44, 159, 208;
+ --bs-light: #F7F8FA;
+ --bs-light-rgb: 247, 248, 250;
+ --bs-dark: #0B0B0D;
+ --bs-dark-rgb: 11, 11, 13;
+
+ --bs-focus-ring-color: color-mix(in srgb, var(--brand) 40%, transparent);
+}
+
+/* Subtle / emphasis / border tints: light theme (tint 80 / 60, shade 60) … */
+:root,
+[data-bs-theme="light"] {
+ --bs-primary-text-emphasis: color-mix(in srgb, var(--brand) 40%, #000);
+ --bs-primary-bg-subtle: color-mix(in srgb, var(--brand) 20%, #fff);
+ --bs-primary-border-subtle: color-mix(in srgb, var(--brand) 40%, #fff);
+ --bs-secondary-text-emphasis: #191a19;
+ --bs-secondary-bg-subtle: #e5e6e5;
+ --bs-secondary-border-subtle: #c5c6c5;
+ --bs-success-text-emphasis: #124f2c;
+ --bs-success-bg-subtle: #d5ecde;
+ --bs-success-border-subtle: #acdabd;
+ --bs-danger-text-emphasis: #561411;
+ --bs-danger-bg-subtle: #f7d6d5;
+ --bs-danger-border-subtle: #efadac;
+ --bs-warning-text-emphasis: #613f00;
+ --bs-warning-bg-subtle: #fceecc;
+ --bs-warning-border-subtle: #fadf99;
+ --bs-info-text-emphasis: #11404e;
+ --bs-info-bg-subtle: #d5ecf6;
+ --bs-info-border-subtle: #abd9ec;
+ --bs-link-color: var(--brand-ink);
+ --bs-link-color-rgb: 0, 0, 0;
+ --bs-link-hover-color: color-mix(in srgb, var(--brand) 40%, #000);
+}
+
+/* … dark theme (tint 40, shade 80 / 60) */
+[data-bs-theme="dark"] {
+ --bs-primary-text-emphasis: color-mix(in srgb, var(--brand) 60%, #fff);
+ --bs-primary-bg-subtle: color-mix(in srgb, var(--brand) 20%, #000);
+ --bs-primary-border-subtle: color-mix(in srgb, var(--brand) 40%, #000);
+ --bs-secondary-text-emphasis: #b1b2b1;
+ --bs-secondary-bg-subtle: #131413;
+ --bs-secondary-border-subtle: #252726;
+ --bs-success-text-emphasis: #82c59d;
+ --bs-success-bg-subtle: #09200f;
+ --bs-success-border-subtle: #1c5f36;
+ --bs-danger-text-emphasis: #e88380;
+ --bs-danger-bg-subtle: #2b0a09;
+ --bs-danger-border-subtle: #801e1c;
+ --bs-warning-text-emphasis: #f7ca66;
+ --bs-warning-bg-subtle: #302200;
+ --bs-warning-border-subtle: #916800;
+ --bs-info-text-emphasis: #80c5e3;
+ --bs-info-bg-subtle: #0e2833;
+ --bs-info-border-subtle: #1a5f7d;
+ --bs-link-color: var(--brand);
+ --bs-link-color-rgb: var(--fitme-accent-rgb, 194, 213, 0);
+ --bs-link-hover-color: color-mix(in srgb, var(--brand) 75%, #fff);
+}
+
+/* ── Components: Bootstrap hard-codes white text on solid primary/semantic
+ fills; the brand colour is light, so the text must be dark. ─────────────── */
+.btn-primary {
+ --bs-btn-color: var(--brand-on);
+ --bs-btn-bg: var(--brand);
+ --bs-btn-border-color: var(--brand);
+ --bs-btn-hover-color: var(--brand-on);
+ --bs-btn-hover-bg: color-mix(in srgb, var(--brand) 86%, #000);
+ --bs-btn-hover-border-color: color-mix(in srgb, var(--brand) 80%, #000);
+ --bs-btn-focus-shadow-rgb: var(--fitme-accent-rgb, 194, 213, 0);
+ --bs-btn-active-color: var(--brand-on);
+ --bs-btn-active-bg: color-mix(in srgb, var(--brand) 78%, #000);
+ --bs-btn-active-border-color: color-mix(in srgb, var(--brand) 70%, #000);
+ --bs-btn-disabled-color: var(--brand-on);
+ --bs-btn-disabled-bg: var(--brand);
+ --bs-btn-disabled-border-color: var(--brand);
+}
+
+.btn-outline-primary {
+ --bs-btn-color: var(--brand-ink);
+ --bs-btn-border-color: var(--brand);
+ --bs-btn-hover-color: var(--brand-on);
+ --bs-btn-hover-bg: var(--brand);
+ --bs-btn-hover-border-color: var(--brand);
+ --bs-btn-focus-shadow-rgb: var(--fitme-accent-rgb, 194, 213, 0);
+ --bs-btn-active-color: var(--brand-on);
+ --bs-btn-active-bg: var(--brand);
+ --bs-btn-active-border-color: var(--brand);
+ --bs-btn-disabled-color: var(--brand-ink);
+ --bs-btn-disabled-border-color: var(--brand);
+}
+[data-bs-theme="dark"] .btn-outline-primary { --bs-btn-color: var(--brand); --bs-btn-disabled-color: var(--brand); }
+
+.btn-secondary {
+ --bs-btn-bg: var(--bs-secondary);
+ --bs-btn-border-color: var(--bs-secondary);
+ --bs-btn-hover-bg: #2f3230;
+ --bs-btn-hover-border-color: #2b2d2c;
+ --bs-btn-active-bg: #2b2d2c;
+ --bs-btn-active-border-color: #262826;
+ --bs-btn-disabled-bg: var(--bs-secondary);
+ --bs-btn-disabled-border-color: var(--bs-secondary);
+}
+.btn-outline-secondary {
+ --bs-btn-color: var(--bs-secondary);
+ --bs-btn-border-color: var(--bs-secondary);
+ --bs-btn-hover-bg: var(--bs-secondary);
+ --bs-btn-hover-border-color: var(--bs-secondary);
+ --bs-btn-active-bg: var(--bs-secondary);
+ --bs-btn-active-border-color: var(--bs-secondary);
+ --bs-btn-disabled-color: var(--bs-secondary);
+ --bs-btn-disabled-border-color: var(--bs-secondary);
+}
+[data-bs-theme="dark"] .btn-outline-secondary { --bs-btn-color: #9a9d9b; --bs-btn-border-color: #5b5f5d; }
+
+.btn-success { --bs-btn-bg: var(--bs-success); --bs-btn-border-color: var(--bs-success); --bs-btn-hover-bg: #27864d; --bs-btn-hover-border-color: #257d49; --bs-btn-active-bg: #257d49; --bs-btn-active-border-color: #227545; --bs-btn-disabled-bg: var(--bs-success); --bs-btn-disabled-border-color: var(--bs-success); }
+.btn-danger { --bs-btn-bg: var(--bs-danger); --bs-btn-border-color: var(--bs-danger); --bs-btn-hover-bg: #b62a27; --bs-btn-hover-border-color: #ab2825; --bs-btn-active-bg: #ab2825; --bs-btn-active-border-color: #a02622; --bs-btn-disabled-bg: var(--bs-danger); --bs-btn-disabled-border-color: var(--bs-danger); }
+.btn-warning { --bs-btn-bg: var(--bs-warning); --bs-btn-border-color: var(--bs-warning); --bs-btn-hover-bg: #f5b61f; --bs-btn-hover-border-color: #f4b212; --bs-btn-active-bg: #f6bb2b; --bs-btn-active-border-color: #f4b212; --bs-btn-disabled-bg: var(--bs-warning); --bs-btn-disabled-border-color: var(--bs-warning); }
+.btn-info { --bs-btn-bg: var(--bs-info); --bs-btn-border-color: var(--bs-info); --bs-btn-hover-bg: #52b0d9; --bs-btn-hover-border-color: #42a9d6; --bs-btn-active-bg: #5bb5dc; --bs-btn-active-border-color: #42a9d6; --bs-btn-disabled-bg: var(--bs-info); --bs-btn-disabled-border-color: var(--bs-info); }
+.btn-outline-success { --bs-btn-color: var(--bs-success); --bs-btn-border-color: var(--bs-success); --bs-btn-hover-bg: var(--bs-success); --bs-btn-hover-border-color: var(--bs-success); --bs-btn-active-bg: var(--bs-success); --bs-btn-active-border-color: var(--bs-success); --bs-btn-disabled-color: var(--bs-success); --bs-btn-disabled-border-color: var(--bs-success); }
+.btn-outline-danger { --bs-btn-color: var(--bs-danger); --bs-btn-border-color: var(--bs-danger); --bs-btn-hover-bg: var(--bs-danger); --bs-btn-hover-border-color: var(--bs-danger); --bs-btn-active-bg: var(--bs-danger); --bs-btn-active-border-color: var(--bs-danger); --bs-btn-disabled-color: var(--bs-danger); --bs-btn-disabled-border-color: var(--bs-danger); }
+.btn-outline-warning { --bs-btn-color: #b07a00; --bs-btn-border-color: var(--bs-warning); --bs-btn-hover-bg: var(--bs-warning); --bs-btn-hover-border-color: var(--bs-warning); --bs-btn-active-bg: var(--bs-warning); --bs-btn-active-border-color: var(--bs-warning); --bs-btn-disabled-color: #b07a00; --bs-btn-disabled-border-color: var(--bs-warning); }
+.btn-outline-info { --bs-btn-color: #1f7ea6; --bs-btn-border-color: var(--bs-info); --bs-btn-hover-bg: var(--bs-info); --bs-btn-hover-border-color: var(--bs-info); --bs-btn-active-bg: var(--bs-info); --bs-btn-active-border-color: var(--bs-info); --bs-btn-disabled-color: #1f7ea6; --bs-btn-disabled-border-color: var(--bs-info); }
+[data-bs-theme="dark"] .btn-outline-warning { --bs-btn-color: var(--bs-warning); --bs-btn-disabled-color: var(--bs-warning); }
+[data-bs-theme="dark"] .btn-outline-info { --bs-btn-color: var(--bs-info); --bs-btn-disabled-color: var(--bs-info); }
+.btn-success, .btn-danger, .btn-info, .btn-outline-success, .btn-outline-danger, .btn-outline-info { --bs-btn-hover-color: #fff; --bs-btn-active-color: #fff; }
+.btn-success, .btn-danger, .btn-info { --bs-btn-color: #fff; --bs-btn-disabled-color: #fff; }
+.btn-warning, .btn-outline-warning { --bs-btn-hover-color: #0B0B0D; --bs-btn-active-color: #0B0B0D; }
+.btn-warning { --bs-btn-color: #0B0B0D; --bs-btn-disabled-color: #0B0B0D; }
+
+/* ── Solid fills that use the colour as background ───────────────────────── */
+.text-bg-primary { color: var(--brand-on) !important; background-color: var(--brand) !important; }
+.text-bg-secondary { background-color: var(--bs-secondary) !important; }
+.text-bg-success { background-color: var(--bs-success) !important; }
+.text-bg-danger { background-color: var(--bs-danger) !important; }
+.text-bg-warning { color: #0B0B0D !important; background-color: var(--bs-warning) !important; }
+.text-bg-info { color: #fff !important; background-color: var(--bs-info) !important; }
+.link-primary { color: var(--brand-ink) !important; }
+[data-bs-theme="dark"] .link-primary { color: var(--brand) !important; }
+
+/* ── Forms ───────────────────────────────────────────────────────────────── */
+.form-control:focus, .form-select:focus {
+ border-color: color-mix(in srgb, var(--brand) 70%, #000);
+ box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color);
+}
+.form-check-input:checked,
+.form-check-input[type="checkbox"]:indeterminate {
+ background-color: var(--brand);
+ border-color: var(--brand);
+}
+/* the tick / dot / knob are inline SVGs with a hard-coded white fill */
+.form-check-input:checked[type="checkbox"] {
+ --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%230B0B0D' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
+}
+.form-check-input:checked[type="radio"] {
+ --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%230B0B0D'/%3e%3c/svg%3e");
+}
+.form-check-input:focus { border-color: var(--brand); box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
+.form-switch .form-check-input:focus {
+ --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23B9C65A'/%3e%3c/svg%3e");
+}
+.form-switch .form-check-input:checked {
+ --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%230B0B0D'/%3e%3c/svg%3e");
+}
+.form-range::-webkit-slider-thumb { background-color: var(--brand); }
+.form-range::-webkit-slider-thumb:active { background-color: color-mix(in srgb, var(--brand) 60%, #fff); }
+.form-range::-moz-range-thumb { background-color: var(--brand); }
+.form-range::-moz-range-thumb:active { background-color: color-mix(in srgb, var(--brand) 60%, #fff); }
+.form-range:focus::-webkit-slider-thumb { box-shadow: 0 0 0 1px #fff, 0 0 0 .25rem var(--bs-focus-ring-color); }
+.form-range:focus::-moz-range-thumb { box-shadow: 0 0 0 1px #fff, 0 0 0 .25rem var(--bs-focus-ring-color); }
+.was-validated .form-control:valid, .form-control.is-valid,
+.was-validated .form-select:valid, .form-select.is-valid { border-color: var(--bs-success); }
+.was-validated .form-control:invalid, .form-control.is-invalid,
+.was-validated .form-select:invalid, .form-select.is-invalid { border-color: var(--bs-danger); }
+
+/* ── Navigation & lists ──────────────────────────────────────────────────── */
+.nav {
+ --bs-nav-link-color: var(--bs-link-color);
+ --bs-nav-link-hover-color: var(--bs-link-hover-color);
+}
+.nav-pills {
+ --bs-nav-pills-link-active-color: var(--brand-on);
+ --bs-nav-pills-link-active-bg: var(--brand);
+}
+.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link { border-top: 2px solid var(--brand); }
+.pagination {
+ --bs-pagination-color: var(--bs-link-color);
+ --bs-pagination-hover-color: var(--bs-link-hover-color);
+ --bs-pagination-focus-color: var(--bs-link-hover-color);
+ --bs-pagination-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color);
+ --bs-pagination-active-color: var(--brand-on);
+ --bs-pagination-active-bg: var(--brand);
+ --bs-pagination-active-border-color: var(--brand);
+}
+.list-group { --bs-list-group-active-color: var(--brand-on); --bs-list-group-active-bg: var(--brand); --bs-list-group-active-border-color: var(--brand); }
+.dropdown-menu { --bs-dropdown-link-active-color: var(--brand-on); --bs-dropdown-link-active-bg: var(--brand); }
+.accordion {
+ --bs-accordion-active-color: var(--bs-primary-text-emphasis);
+ --bs-accordion-active-bg: var(--bs-primary-bg-subtle);
+ --bs-accordion-btn-focus-border-color: var(--brand);
+ --bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color);
+}
+
+/* ── Feedback ────────────────────────────────────────────────────────────── */
+.progress { --bs-progress-bar-bg: var(--brand); --bs-progress-bar-color: var(--brand-on); }
+.alert-primary {
+ --bs-alert-color: var(--bs-primary-text-emphasis);
+ --bs-alert-bg: var(--bs-primary-bg-subtle);
+ --bs-alert-border-color: var(--bs-primary-border-subtle);
+ --bs-alert-link-color: var(--bs-primary-text-emphasis);
+}
+.table-primary {
+ --bs-table-color: var(--bs-primary-text-emphasis);
+ --bs-table-bg: var(--bs-primary-bg-subtle);
+ --bs-table-border-color: var(--bs-primary-border-subtle);
+ --bs-table-striped-bg: color-mix(in srgb, var(--bs-primary-bg-subtle) 94%, #000);
+ --bs-table-hover-bg: color-mix(in srgb, var(--bs-primary-bg-subtle) 92%, #000);
+ --bs-table-active-bg: color-mix(in srgb, var(--bs-primary-bg-subtle) 90%, #000);
+}
+.border-primary { border-color: var(--brand) !important; }
+.bg-primary { background-color: var(--brand) !important; color: var(--brand-on); }
+.text-primary { color: var(--brand-ink) !important; }
+[data-bs-theme="dark"] .text-primary { color: var(--brand) !important; }
+.bg-primary-subtle { background-color: var(--bs-primary-bg-subtle) !important; }
+.text-primary-emphasis { color: var(--bs-primary-text-emphasis) !important; }
+.border-primary-subtle { border-color: var(--bs-primary-border-subtle) !important; }
diff --git a/config/functions_site.php b/config/functions_site.php
index f9d96c9..e9aeb3d 100644
--- a/config/functions_site.php
+++ b/config/functions_site.php
@@ -36,7 +36,10 @@ function site_navbar(?array $gym = null): array {
$items = ['Home' => ['#home', null]];
foreach (['Modalidades' => 'modalidades', 'Equipa' => 'equipa', 'Horário' => 'horario',
'Mensalidades' => 'mensalidades', 'Contactos' => 'contactos'] as $label => $sec) {
- if (gym_section_on($gym, $sec)) $items[$label] = ['#' . $sec, $sec];
+ if (!gym_section_on($gym, $sec)) continue;
+ // No active plans → the section isn't rendered, so no link to it either.
+ if ($sec === 'mensalidades' && !empty($GLOBALS['pdo']) && !site_memberships($GLOBALS['pdo'], (int) $gym['id'])) continue;
+ $items[$label] = ['#' . $sec, $sec];
}
$nav = [];
foreach ($items as $label => [$hash]) $nav[$label] = gym_url($gym, $hash);
diff --git a/includes/auth_header.php b/includes/auth_header.php
index 8c7ac88..2b6743e 100644
--- a/includes/auth_header.php
+++ b/includes/auth_header.php
@@ -29,6 +29,7 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
})();
+