feat(branding): re-theme Bootstrap to the brand colour; 3D neon button hover; hide Mensalidades link without plans

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Sonnet 5.5 committed 2026-10-13 17:16:50 +01:00
1 parent 4b2823d1e5
commit 32f8d7a613
6 files changed
+307 -1

No files matched your search

+3
View File
@@ -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.
+253
View File
@@ -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; }
+4 -1
View File
@@ -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);
+1
View File
@@ -29,6 +29,7 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
})();
</script>
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
<link href="<?= htmlspecialchars(_asset_versioned('assets/css/bs-brand.css'), ENT_QUOTES) ?>" rel="stylesheet">
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
<style>
:root {
+1
View File
@@ -32,6 +32,7 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
var g=JSON.parse(localStorage.getItem('bo-nav-groups')||'{}');d.setAttribute('data-bo-groups',Object.keys(g).filter(function(k){return g[k]}).join(' '));}catch(e){}
</script>
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
<link href="<?= htmlspecialchars(_asset_versioned('assets/css/bs-brand.css'), ENT_QUOTES) ?>" rel="stylesheet">
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
<link href="<?= base_url('assets/vendor/summernote/summernote-bs5.min.css') ?>" rel="stylesheet">
<link href="<?= htmlspecialchars(_asset_versioned('assets/css/backoffice.css'), ENT_QUOTES) ?>" rel="stylesheet">
+45
View File
@@ -353,6 +353,51 @@ section[id] {
color: var(--fitme-accent);
}
/* ── 3D neon extrusion on hover ────────────────────────────────────────────────
The button lifts off the "wall" toward the viewer; the faces it leaves behind
(the extruded sides) are drawn with stacked hard shadows in the brand colour
and lit like neon tubes (glow on top). Colours come from the gym's brand
tokens, so each gym glows in its own colour. */
.site-btn,
.gym-switch-btn {
--neon: color-mix(in srgb, var(--fitme-accent) 70%, #fff);
--neon-edge: var(--fitme-accent-dark);
--depth: 7px;
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
.site-btn,
.gym-switch-btn {
transition: transform .18s ease, box-shadow .18s ease, background var(--fitme-transition), color var(--fitme-transition);
}
.site-btn:hover,
.site-btn:focus-visible,
.gym-switch-btn:hover,
.gym-switch-btn:focus-visible {
transform: translate(calc(var(--depth) * -1), calc(var(--depth) * -1));
box-shadow:
1px 1px 0 var(--neon-edge),
2px 2px 0 var(--neon),
3px 3px 0 var(--neon-edge),
4px 4px 0 var(--neon),
5px 5px 0 var(--neon-edge),
6px 6px 0 var(--neon),
7px 7px 0 var(--neon-edge),
7px 7px 14px 2px rgba(var(--fitme-accent-rgb), .75),
10px 10px 30px 4px rgba(var(--fitme-accent-rgb), .45);
}
.site-btn:active,
.gym-switch-btn:active {
transform: translate(-2px, -2px);
box-shadow:
1px 1px 0 var(--neon-edge),
2px 2px 0 var(--neon),
2px 2px 10px 1px rgba(var(--fitme-accent-rgb), .6);
}
}
/* ── Bootstrap Nav Overrides ───────────────────────────────────────────────── */
.nav-link {
font-weight: 700;