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'; })(); +